using System;
using System.IO;
using System.Threading.Tasks;
using MailKit.Net.Smtp;
using MimeKit;
using Server.Accounting;
using Server.Configurations;
using Server.Engines.Help;
using Server.Logging;
namespace Server.Misc
{
public static class Email
{
private static readonly ILogger _logger = LogFactory.GetLogger(typeof(Email));
// How long SendCrashEmail holds up the exit after a crash: at the default settings, room for the first
// attempts (3 and 6 s apart; a fourth starts at 21 s when they fail fast), and no longer when the SMTP
// server is dead.
private static readonly TimeSpan CrashEmailTimeout = TimeSpan.FromSeconds(30);
// A retry wait stops doubling at an hour: no multi-day waits, and nothing left to overflow.
private const int MaxRetryDelaySeconds = 3600;
///
/// Sends Queue-Page request using Email
///
///
///
public static void SendQueueEmail(PageEntry entry, string pageType)
{
if (!EmailConfiguration.EmailEnabled)
{
return;
}
var sender = entry.Sender;
var time = Core.Now;
var message = new MimeMessage();
message.From.Add(EmailConfiguration.FromAddress);
message.To.Add(EmailConfiguration.SpeechLogPageAddress);
message.Subject = "ModernUO Speech Log Page Forwarding";
using (var writer = new StringWriter())
{
writer.WriteLine(
@$"
ModernUO Speech Log Page - {pageType}
From: '{sender.RawName}', Account: '{(sender.Account is Account accSend ? accSend.Username : " ??? ")}'
Location: {sender.Location} [{sender.Map}]
Sent on: {time.Year}/{time.Month:00}/{time.Day:00} {time.Hour}:{time.Minute:00}:{time.Second:00}
Message:
'{entry.Message}'
Speech Log
==========
"
);
foreach (var logEntry in entry.SpeechLog)
{
var from = logEntry.From;
var fromName = from.RawName;
var fromAccount = from.Account is Account accFrom ? accFrom.Username : "???";
var created = logEntry.Created;
var speech = logEntry.Speech;
writer.WriteLine(
$"{created.Hour}:{created.Minute:00}:{created.Second:00} - {fromName} ({fromAccount}): '{speech}'"
);
}
message.Body = new BodyBuilder
{
TextBody = writer.ToString(),
HtmlBody = null
}.ToMessageBody();
}
_ = SendAsync(message);
}
///
/// Sends crash email, blocking for up to the crash timeout (30 s) while it is sent: the core exits
/// as soon as the crash handlers return, and a send left running dies with it.
///
///
public static void SendCrashEmail(string filePath)
{
if (!EmailConfiguration.EmailEnabled)
{
return;
}
var message = new MimeMessage();
message.From.Add(EmailConfiguration.FromAddress);
message.To.Add(EmailConfiguration.CrashAddress);
message.Subject = "Automated ModernUO Crash Report";
var builder = new BodyBuilder
{
TextBody = "Automated ModernUO Crash Report. See attachment for details.",
HtmlBody = null
};
builder.Attachments.Add(filePath);
message.Body = builder.ToMessageBody();
try
{
if (!SendAsync(message).Wait(CrashEmailTimeout))
{
_logger.Warning("Crash email not sent after {Timeout}, giving up", CrashEmailTimeout);
}
}
catch (Exception ex)
{
_logger.Error(ex, "Sending crash email failed");
}
}
///
/// Sends emails async
///
///
private static async Task SendAsync(MimeMessage message)
{
if (!EmailConfiguration.EmailEnabled)
{
return;
}
var attempts = EmailConfiguration.EmailSendRetryCount;
var retryDelays = RetryDelays(attempts, EmailConfiguration.EmailSendRetryDelay);
Exception lastException = null;
// SendCrashEmail can block the game loop on this task: every await needs ConfigureAwait(false).
for (var i = 0; i < attempts; i++)
{
try
{
using var client = new SmtpClient();
await client.ConnectAsync(EmailConfiguration.EmailServer, EmailConfiguration.EmailPort, true).ConfigureAwait(false);
await client.AuthenticateAsync(
EmailConfiguration.EmailServerUsername,
EmailConfiguration.EmailServerPassword
).ConfigureAwait(false);
await client.SendAsync(message).ConfigureAwait(false);
await client.DisconnectAsync(true).ConfigureAwait(false);
return;
}
catch (Exception ex)
{
lastException = ex;
if (i == 0)
{
_logger.Warning(ex, "Sending email {Subject} failed", message.Subject);
}
if (i < retryDelays.Length)
{
await Task.Delay(retryDelays[i]).ConfigureAwait(false);
}
}
}
_logger.Error(lastException, "Email {Subject} not sent after {Attempts} attempts", message.Subject, attempts);
}
///
/// The waits between send attempts: the configured delay, doubling each time, and none after the last.
///
///
///
internal static TimeSpan[] RetryDelays(int attempts, int firstDelaySeconds)
{
var delays = new TimeSpan[Math.Max(attempts, 1) - 1];
var seconds = Math.Clamp(firstDelaySeconds, 0, MaxRetryDelaySeconds);
for (var i = 0; i < delays.Length; i++)
{
delays[i] = TimeSpan.FromSeconds(seconds);
seconds = Math.Min(seconds * 2, MaxRetryDelaySeconds);
}
return delays;
}
}
}