feat: add fnOS packaging, storage workflows and release pipeline
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user