Files
douyin/service/EmailNotificationSettingsService.cs
T

168 lines
7.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 端口必须在 165535 之间");
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;
}
}
}