168 lines
7.3 KiB
C#
168 lines
7.3 KiB
C#
using dy.net.model.dto;
|
||
using dy.net.model.entity;
|
||
using MailKit.Security;
|
||
using Microsoft.AspNetCore.DataProtection;
|
||
using MimeKit;
|
||
using SqlSugar;
|
||
|
||
namespace dy.net.service
|
||
{
|
||
public sealed class EmailNotificationSettingsService
|
||
{
|
||
private const string SettingsId = "default";
|
||
private readonly ISqlSugarClient _db;
|
||
private readonly IDataProtector _protector;
|
||
private readonly IEmailNotificationSender _sender;
|
||
|
||
public EmailNotificationSettingsService(
|
||
ISqlSugarClient db,
|
||
IDataProtectionProvider provider,
|
||
IEmailNotificationSender sender)
|
||
{
|
||
_db = db;
|
||
_protector = provider.CreateProtector("dysync.email.password.v1");
|
||
_sender = sender;
|
||
}
|
||
|
||
public async Task<EmailNotificationSettings> GetAsync() =>
|
||
await _db.Queryable<EmailNotificationSettings>().InSingleAsync(SettingsId)
|
||
?? new EmailNotificationSettings();
|
||
|
||
public async Task<string> GetPasswordAsync(EmailNotificationSettings settings = null)
|
||
{
|
||
settings ??= await GetAsync();
|
||
if (string.IsNullOrWhiteSpace(settings.ProtectedPassword)) return string.Empty;
|
||
try { return _protector.Unprotect(settings.ProtectedPassword); }
|
||
catch (Exception ex)
|
||
{
|
||
Serilog.Log.Error(ex, "SMTP 密码解密失败,请重新输入密码");
|
||
return string.Empty;
|
||
}
|
||
}
|
||
|
||
public async Task<EmailNotificationSettings> BuildCandidateAsync(EmailNotificationSettingsDto dto)
|
||
{
|
||
dto ??= new EmailNotificationSettingsDto();
|
||
var existing = await GetAsync();
|
||
var password = string.IsNullOrWhiteSpace(dto.Password)
|
||
? await GetPasswordAsync(existing)
|
||
: dto.Password;
|
||
var candidate = new EmailNotificationSettings
|
||
{
|
||
Id = SettingsId,
|
||
Enabled = dto.Enabled,
|
||
Host = dto.Host?.Trim(),
|
||
Port = dto.Port <= 0 ? 465 : dto.Port,
|
||
SecurityMode = dto.SecurityMode,
|
||
UserName = dto.UserName?.Trim(),
|
||
ProtectedPassword = string.IsNullOrWhiteSpace(password) ? null : _protector.Protect(password),
|
||
FromAddress = dto.FromAddress?.Trim(),
|
||
FromName = Limit(dto.FromName, 200),
|
||
Recipients = NormalizeRecipients(dto.Recipients),
|
||
LastTestedAt = existing.LastTestedAt,
|
||
LastTestMessage = existing.LastTestMessage
|
||
};
|
||
Validate(candidate, password, requireComplete: candidate.Enabled);
|
||
return candidate;
|
||
}
|
||
|
||
public async Task<EmailNotificationSettingsDto> SaveAsync(EmailNotificationSettingsDto dto)
|
||
{
|
||
var candidate = await BuildCandidateAsync(dto);
|
||
await _db.Storageable(candidate).ExecuteCommandAsync();
|
||
return ToDto(candidate);
|
||
}
|
||
|
||
public async Task<string> TestAsync(EmailNotificationSettingsDto dto, CancellationToken cancellationToken)
|
||
{
|
||
var candidate = await BuildCandidateAsync(dto);
|
||
var password = await GetPasswordAsync(candidate);
|
||
Validate(candidate, password, requireComplete: true);
|
||
var now = DateTime.Now;
|
||
await _sender.SendAsync(
|
||
candidate,
|
||
password,
|
||
"dysync.net 邮箱通知测试成功",
|
||
$"<h2>邮箱通知配置可用</h2><p>测试时间:{now:yyyy-MM-dd HH:mm:ss}</p><p>开播通知将只在博主首次进入直播状态时发送。</p>",
|
||
cancellationToken);
|
||
candidate.LastTestedAt = DateTime.Now;
|
||
candidate.LastTestMessage = "测试邮件发送成功";
|
||
await _db.Storageable(candidate).ExecuteCommandAsync();
|
||
return candidate.LastTestMessage;
|
||
}
|
||
|
||
public async Task<string> GetReadinessErrorAsync()
|
||
{
|
||
var settings = await GetAsync();
|
||
if (!settings.Enabled) return "请先在系统设置中启用邮箱通知";
|
||
var password = await GetPasswordAsync(settings);
|
||
try
|
||
{
|
||
Validate(settings, password, requireComplete: true);
|
||
return null;
|
||
}
|
||
catch (InvalidOperationException ex)
|
||
{
|
||
return "邮箱通知配置不完整:" + ex.Message;
|
||
}
|
||
}
|
||
|
||
public EmailNotificationSettingsDto ToDto(EmailNotificationSettings settings) => new()
|
||
{
|
||
Enabled = settings.Enabled,
|
||
Host = settings.Host,
|
||
Port = settings.Port,
|
||
SecurityMode = settings.SecurityMode,
|
||
UserName = settings.UserName,
|
||
HasPassword = !string.IsNullOrWhiteSpace(settings.ProtectedPassword),
|
||
FromAddress = settings.FromAddress,
|
||
FromName = settings.FromName,
|
||
Recipients = settings.Recipients,
|
||
LastTestedAt = settings.LastTestedAt,
|
||
LastTestMessage = settings.LastTestMessage
|
||
};
|
||
|
||
public static SecureSocketOptions ToSocketOptions(EmailSecurityMode mode) => mode switch
|
||
{
|
||
EmailSecurityMode.None => SecureSocketOptions.None,
|
||
EmailSecurityMode.StartTls => SecureSocketOptions.StartTls,
|
||
_ => SecureSocketOptions.SslOnConnect
|
||
};
|
||
|
||
private static void Validate(EmailNotificationSettings settings, string password, bool requireComplete)
|
||
{
|
||
if (settings.Port is < 1 or > 65535) throw new InvalidOperationException("SMTP 端口必须在 1–65535 之间");
|
||
if (!requireComplete) return;
|
||
if (string.IsNullOrWhiteSpace(settings.Host)) throw new InvalidOperationException("请填写 SMTP 服务器");
|
||
if (string.IsNullOrWhiteSpace(settings.FromAddress) || !MailboxAddress.TryParse(settings.FromAddress, out _))
|
||
throw new InvalidOperationException("发件邮箱格式不正确");
|
||
if (ParseRecipients(settings.Recipients).Count == 0) throw new InvalidOperationException("请至少填写一个收件邮箱");
|
||
if (!string.IsNullOrWhiteSpace(settings.UserName) && string.IsNullOrWhiteSpace(password))
|
||
throw new InvalidOperationException("请填写 SMTP 密码或授权码");
|
||
}
|
||
|
||
public static List<MailboxAddress> ParseRecipients(string recipients)
|
||
{
|
||
var result = new List<MailboxAddress>();
|
||
foreach (var value in (recipients ?? string.Empty)
|
||
.Split(new[] { ',', ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||
.Select(x => x.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).Take(20))
|
||
{
|
||
if (!MailboxAddress.TryParse(value, out var address))
|
||
throw new InvalidOperationException($"收件邮箱格式不正确:{value}");
|
||
result.Add(address);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private static string NormalizeRecipients(string recipients) =>
|
||
string.Join(";", ParseRecipients(recipients).Select(x => x.Address));
|
||
|
||
private static string Limit(string value, int maxLength)
|
||
{
|
||
value = value?.Trim();
|
||
return value != null && value.Length > maxLength ? value[..maxLength] : value;
|
||
}
|
||
}
|
||
}
|