267 lines
11 KiB
C#
267 lines
11 KiB
C#
using System.Net;
|
|
using System.Net.Mail;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using LiveRecorder.Application.Abstractions.Notifications;
|
|
using LiveRecorder.Application.Abstractions.Settings;
|
|
using LiveRecorder.Application.Common;
|
|
using LiveRecorder.Application.Models.Settings;
|
|
using LiveRecorder.Domain.Entities;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace LiveRecorder.Infrastructure.Services;
|
|
|
|
public sealed class EmailNotificationService : IEmailNotificationService
|
|
{
|
|
private const string AppName = "LiveRecorder";
|
|
private static readonly Regex TemplateTokenRegex = new("""\{\{\s*(?<name>[a-zA-Z0-9_]+)\s*\}\}""", RegexOptions.Compiled);
|
|
|
|
private readonly ISystemSettingsService _systemSettingsService;
|
|
private readonly ILogger<EmailNotificationService> _logger;
|
|
|
|
public EmailNotificationService(
|
|
ISystemSettingsService systemSettingsService,
|
|
ILogger<EmailNotificationService> logger)
|
|
{
|
|
_systemSettingsService = systemSettingsService;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task SendLiveStartedAsync(LiveRoom liveRoom, CancellationToken cancellationToken = default)
|
|
{
|
|
var settings = await _systemSettingsService.GetAsync(cancellationToken);
|
|
if (!settings.EnableEmailNotification || !settings.NotifyOnLiveStarted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var tokens = CreateTokenMap(new Dictionary<string, string?>
|
|
{
|
|
["platform"] = liveRoom.Platform.ToString(),
|
|
["roomId"] = liveRoom.RoomId,
|
|
["title"] = liveRoom.Title,
|
|
["anchor"] = liveRoom.AnchorName,
|
|
["sourceUrl"] = liveRoom.SourceUrl,
|
|
["detectedAtUtc"] = ChinaTime.ToBeijingTime(DateTimeOffset.UtcNow).ToString("O")
|
|
});
|
|
|
|
var subject = RenderSubject(settings.EmailLiveStartedSubjectTemplate, tokens);
|
|
var body = RenderHtml(settings.EmailLiveStartedBodyTemplateHtml, tokens);
|
|
|
|
await SendAsync(settings, subject, body, cancellationToken);
|
|
}
|
|
|
|
public async Task SendExceptionAsync(
|
|
string source,
|
|
string summary,
|
|
string? detail = null,
|
|
LiveRoom? liveRoom = null,
|
|
RecordTask? recordTask = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var settings = await _systemSettingsService.GetAsync(cancellationToken);
|
|
if (!settings.EnableEmailNotification || !settings.NotifyOnException)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var tokens = CreateTokenMap(new Dictionary<string, string?>
|
|
{
|
|
["source"] = source,
|
|
["summary"] = summary,
|
|
["detail"] = detail,
|
|
["liveRoomId"] = liveRoom?.Id.ToString(),
|
|
["roomId"] = liveRoom?.RoomId,
|
|
["recordTaskId"] = recordTask?.Id.ToString(),
|
|
["taskStatus"] = recordTask?.Status.ToString(),
|
|
["occurredAtUtc"] = ChinaTime.ToBeijingTime(DateTimeOffset.UtcNow).ToString("O")
|
|
});
|
|
|
|
var subject = RenderSubject(settings.EmailExceptionSubjectTemplate, tokens);
|
|
var body = RenderHtml(settings.EmailExceptionBodyTemplateHtml, tokens);
|
|
|
|
await SendAsync(settings, subject, body, cancellationToken);
|
|
}
|
|
|
|
public async Task SendTestAsync(SendTestEmailRequest request, CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
var settings = new SystemSettingsDto
|
|
{
|
|
EnableEmailNotification = true,
|
|
NotifyOnLiveStarted = true,
|
|
NotifyOnException = true,
|
|
EmailSmtpHost = request.EmailSmtpHost.Trim(),
|
|
EmailSmtpPort = request.EmailSmtpPort,
|
|
EmailUseSsl = request.EmailUseSsl,
|
|
EmailUsername = request.EmailUsername.Trim(),
|
|
EmailPassword = request.EmailPassword,
|
|
EmailFromAddress = request.EmailFromAddress.Trim(),
|
|
EmailFromDisplayName = request.EmailFromDisplayName.Trim(),
|
|
EmailToAddresses = request.EmailToAddresses.Trim(),
|
|
EmailLiveStartedSubjectTemplate = request.EmailLiveStartedSubjectTemplate,
|
|
EmailLiveStartedBodyTemplateHtml = request.EmailLiveStartedBodyTemplateHtml,
|
|
EmailExceptionSubjectTemplate = request.EmailExceptionSubjectTemplate,
|
|
EmailExceptionBodyTemplateHtml = request.EmailExceptionBodyTemplateHtml
|
|
};
|
|
|
|
var sampleLiveTokens = CreateTokenMap(new Dictionary<string, string?>
|
|
{
|
|
["platform"] = "Douyin",
|
|
["roomId"] = "123456789",
|
|
["title"] = "Sample Live Title",
|
|
["anchor"] = "Sample Anchor",
|
|
["sourceUrl"] = "https://live.douyin.com/123456789",
|
|
["detectedAtUtc"] = ChinaTime.ToBeijingTime(DateTimeOffset.UtcNow).ToString("O")
|
|
});
|
|
var sampleExceptionTokens = CreateTokenMap(new Dictionary<string, string?>
|
|
{
|
|
["source"] = "Scheduler",
|
|
["summary"] = "Background polling failed for a live room.",
|
|
["detail"] = "Sample stack trace or diagnostic detail goes here.",
|
|
["liveRoomId"] = Guid.NewGuid().ToString(),
|
|
["roomId"] = "123456789",
|
|
["recordTaskId"] = Guid.NewGuid().ToString(),
|
|
["taskStatus"] = "Running",
|
|
["occurredAtUtc"] = ChinaTime.ToBeijingTime(DateTimeOffset.UtcNow).ToString("O")
|
|
});
|
|
|
|
var body = $$"""
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<body style="margin: 0; padding: 24px; background: #f5f7fb; font-family: 'Segoe UI', 'PingFang SC', sans-serif; color: #1f2937;">
|
|
<div style="max-width: 880px; margin: 0 auto;">
|
|
<h1 style="margin: 0 0 20px; color: #2f4c66;">LiveRecorder SMTP template test</h1>
|
|
<div style="margin-bottom: 18px; padding: 18px 20px; border-radius: 12px; background: #ffffff; border: 1px solid #d9e1ea;">
|
|
<h2 style="margin: 0 0 14px; color: #3e5f7c;">开播提醒示例</h2>
|
|
<div style="margin-bottom: 10px; font-size: 13px; color: #6b7280;"><strong>Subject:</strong> {{WebUtility.HtmlEncode(RenderSubject(settings.EmailLiveStartedSubjectTemplate, sampleLiveTokens))}}</div>
|
|
{{RenderHtml(settings.EmailLiveStartedBodyTemplateHtml, sampleLiveTokens)}}
|
|
</div>
|
|
<div style="padding: 18px 20px; border-radius: 12px; background: #ffffff; border: 1px solid #d9e1ea;">
|
|
<h2 style="margin: 0 0 14px; color: #8b5e3c;">异常提醒示例</h2>
|
|
<div style="margin-bottom: 10px; font-size: 13px; color: #6b7280;"><strong>Subject:</strong> {{WebUtility.HtmlEncode(RenderSubject(settings.EmailExceptionSubjectTemplate, sampleExceptionTokens))}}</div>
|
|
{{RenderHtml(settings.EmailExceptionBodyTemplateHtml, sampleExceptionTokens)}}
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
""";
|
|
|
|
await SendAsync(settings, "[LiveRecorder] SMTP template test", body, cancellationToken, swallowErrors: false);
|
|
}
|
|
|
|
private async Task SendAsync(
|
|
SystemSettingsDto settings,
|
|
string subject,
|
|
string body,
|
|
CancellationToken cancellationToken,
|
|
bool swallowErrors = true)
|
|
{
|
|
var recipients = ParseRecipients(settings.EmailToAddresses);
|
|
if (string.IsNullOrWhiteSpace(settings.EmailSmtpHost) ||
|
|
string.IsNullOrWhiteSpace(settings.EmailFromAddress) ||
|
|
recipients.Count == 0)
|
|
{
|
|
if (swallowErrors)
|
|
{
|
|
return;
|
|
}
|
|
|
|
throw new InvalidOperationException("SMTP host, sender address or recipient list is missing.");
|
|
}
|
|
|
|
try
|
|
{
|
|
using var message = new MailMessage
|
|
{
|
|
From = new MailAddress(settings.EmailFromAddress, settings.EmailFromDisplayName),
|
|
Subject = subject,
|
|
Body = body,
|
|
BodyEncoding = Encoding.UTF8,
|
|
SubjectEncoding = Encoding.UTF8,
|
|
IsBodyHtml = true
|
|
};
|
|
|
|
foreach (var recipient in recipients)
|
|
{
|
|
message.To.Add(recipient);
|
|
}
|
|
|
|
using var client = new SmtpClient(settings.EmailSmtpHost, settings.EmailSmtpPort)
|
|
{
|
|
EnableSsl = settings.EmailUseSsl,
|
|
DeliveryMethod = SmtpDeliveryMethod.Network
|
|
};
|
|
|
|
if (!string.IsNullOrWhiteSpace(settings.EmailUsername))
|
|
{
|
|
client.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword);
|
|
}
|
|
|
|
using var registration = cancellationToken.Register(client.SendAsyncCancel);
|
|
await client.SendMailAsync(message);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!swallowErrors)
|
|
{
|
|
throw new InvalidOperationException($"SMTP test email send failed: {ex.Message}", ex);
|
|
}
|
|
|
|
_logger.LogWarning(ex, "Email notification send failed");
|
|
}
|
|
}
|
|
|
|
private static List<string> ParseRecipients(string rawAddresses) =>
|
|
rawAddresses
|
|
.Split([',', ';', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.Where(static item => !string.IsNullOrWhiteSpace(item))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
private static Dictionary<string, string> CreateTokenMap(IReadOnlyDictionary<string, string?> rawTokens)
|
|
{
|
|
var tokens = rawTokens.ToDictionary(
|
|
static pair => pair.Key,
|
|
static pair => pair.Value?.Trim() ?? string.Empty,
|
|
StringComparer.OrdinalIgnoreCase);
|
|
tokens["appName"] = AppName;
|
|
return tokens;
|
|
}
|
|
|
|
private static string RenderSubject(string template, IReadOnlyDictionary<string, string> tokens)
|
|
{
|
|
var rendered = RenderTemplate(template, tokens, htmlEncodeValues: false);
|
|
rendered = rendered.Replace("\r", " ").Replace("\n", " ").Trim();
|
|
return string.IsNullOrWhiteSpace(rendered) ? $"[{AppName}] Notification" : rendered;
|
|
}
|
|
|
|
private static string RenderHtml(string template, IReadOnlyDictionary<string, string> tokens)
|
|
{
|
|
var rendered = RenderTemplate(template, tokens, htmlEncodeValues: true).Trim();
|
|
return string.IsNullOrWhiteSpace(rendered) ? "<div></div>" : rendered;
|
|
}
|
|
|
|
private static string RenderTemplate(string template, IReadOnlyDictionary<string, string> tokens, bool htmlEncodeValues)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(template))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return TemplateTokenRegex.Replace(
|
|
template,
|
|
match =>
|
|
{
|
|
var tokenName = match.Groups["name"].Value;
|
|
if (!tokens.TryGetValue(tokenName, out var value))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return htmlEncodeValues ? WebUtility.HtmlEncode(value) : value;
|
|
});
|
|
}
|
|
}
|