Files
Telegram-Panel/src/TelegramPanel.Web/Components/Dialogs/BatchChangeTwoFactorRecoveryEmailDialog.razor
T

694 lines
30 KiB
Plaintext
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.
@namespace TelegramPanel.Web.Components.Dialogs
@inject IConfiguration Configuration
@inject AccountManagementService AccountManagement
@inject TelegramPanel.Core.Services.Telegram.AccountTelegramToolsService AccountTelegramTools
@inject TelegramPanel.Web.Services.CloudMailClient CloudMail
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@using System.Text.RegularExpressions
@using TelegramPanel.Core.Utils
@using TelegramPanel.Data.Entities
<MudDialog>
<DialogContent>
<MudStack Spacing="2">
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
将使用 Cloud Mail 收取 Telegram 邮件验证码,批量完成“找回邮箱”绑定/换绑;可选同时变更“登录邮箱”(用于接收登录确认邮件)。
</MudAlert>
<MudTextField @bind-Value="cloudMailBaseUrl" Label="Cloud Mail URL" Variant="Variant.Outlined" />
<MudTextField @bind-Value="cloudMailToken" Label="Authorization Token" Variant="Variant.Outlined" />
<MudTextField @bind-Value="domain" Label="邮箱域名" Variant="Variant.Outlined"
HelperText="将生成 手机号数字@域名,例如:8413111454444@xx.com" />
<MudSwitch @bind-Value="changeLoginEmail" Label="同时修改登录邮箱(用于接收登录确认邮件)" Color="Color.Primary" />
@if (changeLoginEmail)
{
<MudSwitch @bind-Value="trySetLoginEmailWhenMissing" Label="无登录邮箱时也尝试设置(可能失败)" Color="Color.Primary" />
}
<MudSwitch @bind-Value="useStoredPasswords" Label="优先使用数据库中保存的原二级密码" Color="Color.Primary" />
@if (!useStoredPasswords)
{
<MudTextField @bind-Value="currentPassword" Label="原二级密码(统一)" Variant="Variant.Outlined"
InputType="InputType.Password" Required="true" />
}
else
{
<MudTextField @bind-Value="currentPassword" Label="原二级密码(统一,作为兜底)" Variant="Variant.Outlined"
HelperText="当某账号未保存二级密码时,使用这里作为兜底;留空则该账号会失败"
InputType="InputType.Password" />
}
<MudSwitch @bind-Value="autoConfirm" Label="自动从邮箱取码并确认绑定" Color="Color.Primary" />
@if (autoConfirm)
{
<MudNumericField @bind-Value="pollIntervalSeconds" Label="收码轮询间隔(秒)" Variant="Variant.Outlined" Min="2" Max="30" />
<MudNumericField @bind-Value="pollTimeoutSeconds" Label="收码超时(秒)" Variant="Variant.Outlined" Min="10" Max="300" />
<MudTextField @bind-Value="sendEmailFilter" Label="发件人过滤(sendEmail,支持%)" Variant="Variant.Outlined"
HelperText="留空=不过滤;Cloud Mail 支持模糊匹配,例如 %telegram%" />
<MudTextField @bind-Value="subjectFilter" Label="主题过滤(subject,支持%)" Variant="Variant.Outlined"
HelperText="留空=不过滤;Cloud Mail 支持模糊匹配,例如 %Telegram%" />
}
@if (accounts.Count > 0)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
已选账号:@accounts.Count;进度:@processed/@accounts.Count;成功:@success;失败:@failed;跳过:@skipped
</MudAlert>
}
@if (results.Count > 0)
{
<MudTable Items="results" Dense="true" Hover="true">
<HeaderContent>
<MudTh>手机号</MudTh>
<MudTh>邮箱</MudTh>
<MudTh>结果</MudTh>
<MudTh>信息</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="手机号">@context.Phone</MudTd>
<MudTd DataLabel="邮箱">@context.Email</MudTd>
<MudTd DataLabel="结果">@context.Result</MudTd>
<MudTd DataLabel="信息">@(context.Message ?? "-")</MudTd>
</RowTemplate>
</MudTable>
}
</MudStack>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Text" Color="Color.Default" Disabled="@running" OnClick="Cancel">关闭</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@running" OnClick="Start">开始批量换绑</MudButton>
</DialogActions>
</MudDialog>
@code
{
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
[Parameter] public List<int>? AccountIds { get; set; }
private readonly List<Account> accounts = new();
private readonly List<ResultRow> results = new();
private bool running;
private int processed;
private int success;
private int failed;
private int skipped;
private string cloudMailBaseUrl = "";
private string cloudMailToken = "";
private string domain = "";
private bool changeLoginEmail = true;
private bool trySetLoginEmailWhenMissing;
private bool useStoredPasswords = true;
private string currentPassword = "";
private bool autoConfirm = true;
private int pollIntervalSeconds = 3;
private int pollTimeoutSeconds = 60;
private string sendEmailFilter = "";
private string subjectFilter = "";
protected override async Task OnInitializedAsync()
{
cloudMailBaseUrl = Configuration["CloudMail:BaseUrl"] ?? "";
cloudMailToken = Configuration["CloudMail:Token"] ?? "";
domain = (Configuration["CloudMail:Domain"] ?? "").Trim().TrimStart('@');
var ids = AccountIds?.Distinct().Where(x => x > 0).ToList() ?? new List<int>();
if (ids.Count == 0)
return;
var all = await AccountManagement.GetAllAccountsAsync();
accounts.AddRange(all.Where(a => ids.Contains(a.Id)).OrderBy(a => a.Id));
}
private async Task Start()
{
if (running)
return;
if (accounts.Count == 0)
{
Snackbar.Add("未选择账号", Severity.Warning);
return;
}
cloudMailBaseUrl = (cloudMailBaseUrl ?? string.Empty).Trim();
cloudMailToken = (cloudMailToken ?? string.Empty).Trim();
domain = (domain ?? string.Empty).Trim().TrimStart('@');
if (string.IsNullOrWhiteSpace(cloudMailBaseUrl) || string.IsNullOrWhiteSpace(cloudMailToken))
{
Snackbar.Add("请先在系统设置里配置 Cloud Mail URL/Token(或在此对话框中填写)", Severity.Warning);
return;
}
if (string.IsNullOrWhiteSpace(domain))
{
Snackbar.Add("请填写邮箱域名", Severity.Warning);
return;
}
bool? confirm = await DialogService.ShowMessageBox(
"确认批量换绑",
$"将对 {accounts.Count} 个账号批量换绑找回邮箱{(changeLoginEmail ? "(并同时修改登录邮箱)" : "")},邮箱格式为:{{手机号数字}}@{domain}。是否继续?",
yesText: "继续", cancelText: "取消");
if (confirm != true)
return;
running = true;
processed = 0;
success = 0;
failed = 0;
skipped = 0;
results.Clear();
try
{
foreach (var a in accounts)
{
var phoneDigits = PhoneNumberFormatter.NormalizeToDigits(a.Phone);
var email = string.IsNullOrWhiteSpace(phoneDigits) ? "" : $"{phoneDigits}@{domain}";
var (kind, msg) = await ProcessOneAsync(a, email);
processed++;
if (kind == ResultKind.Success) success++;
else if (kind == ResultKind.Failed) failed++;
else skipped++;
results.Add(new ResultRow(a.DisplayPhone, email, KindToText(kind), msg));
StateHasChanged();
}
Snackbar.Add($"批量换绑完成:成功 {success}/{accounts.Count},失败 {failed}/{accounts.Count},跳过 {skipped}/{accounts.Count}", failed == 0 ? Severity.Success : Severity.Warning);
}
catch (Exception ex)
{
Snackbar.Add($"执行失败:{ex.Message}", Severity.Error);
}
finally
{
running = false;
}
}
private async Task<(ResultKind Kind, string? Message)> ProcessOneAsync(Account account, string email)
{
if (string.IsNullOrWhiteSpace(email))
return (ResultKind.Failed, "手机号无效,无法生成邮箱");
var messages = new List<string>();
var okRecovery = true;
var okLogin = true;
var loginKind = ResultKind.Success;
// 1) 找回邮箱(2FA)
var effectivePassword = useStoredPasswords
? (!string.IsNullOrWhiteSpace(account.TwoFactorPassword) ? account.TwoFactorPassword : currentPassword)
: currentPassword;
effectivePassword = (effectivePassword ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(effectivePassword))
{
okRecovery = false;
messages.Add("找回邮箱失败:未提供原二级密码(数据库未保存且未填写统一密码)");
}
else
{
var startUtc = DateTimeOffset.UtcNow;
var (sent, err, pattern) = await AccountTelegramTools.SetTwoFactorRecoveryEmailAsync(
accountId: account.Id,
currentPassword: effectivePassword,
email: email,
cancellationToken: CancellationToken.None);
if (!sent)
{
okRecovery = false;
if (autoConfirm && (err ?? "").Contains("EMAIL_UNCONFIRMED", StringComparison.OrdinalIgnoreCase))
{
var pendingCode = await TryWaitTelegramCodeAsync(email, startUtc, CancellationToken.None, TelegramMailCodePurpose.RecoveryEmail, allowOlder: true);
if (string.IsNullOrWhiteSpace(pendingCode))
{
var diag = await TryBuildReceiveDiagAsync(email, CancellationToken.None);
messages.Add($"找回邮箱失败:邮箱未确认但收码超时({pollTimeoutSeconds}s):{err}{(string.IsNullOrWhiteSpace(diag) ? "" : ";" + diag)}");
}
else
{
var (pendingConfirmed, pendingConfirmErr) = await AccountTelegramTools.ConfirmTwoFactorRecoveryEmailAsync(
accountId: account.Id,
code: pendingCode,
cancellationToken: CancellationToken.None);
if (pendingConfirmed)
{
okRecovery = true;
messages.Add("找回邮箱已确认绑定(之前存在待确认邮箱)");
}
else
{
if ((pendingConfirmErr ?? "").Contains("CODE_INVALID", StringComparison.OrdinalIgnoreCase))
{
_ = await AccountTelegramTools.ResendTwoFactorRecoveryEmailAsync(account.Id, CancellationToken.None);
var retryCode = await TryWaitTelegramCodeAsync(email, DateTimeOffset.UtcNow, CancellationToken.None, TelegramMailCodePurpose.RecoveryEmail, allowOlder: true);
if (!string.IsNullOrWhiteSpace(retryCode))
{
var (retryOk, retryErr) = await AccountTelegramTools.ConfirmTwoFactorRecoveryEmailAsync(
accountId: account.Id,
code: retryCode,
cancellationToken: CancellationToken.None);
if (retryOk)
{
okRecovery = true;
messages.Add("找回邮箱已确认绑定(之前存在待确认邮箱,重发后重试成功)");
}
else
{
messages.Add($"找回邮箱确认失败:{retryErr}");
}
}
}
if (!okRecovery)
messages.Add($"找回邮箱确认失败:{pendingConfirmErr}");
}
}
}
else
{
messages.Add($"找回邮箱失败:{err}");
}
}
else if (string.IsNullOrWhiteSpace(pattern))
{
messages.Add("找回邮箱无需确认(可能已是目标邮箱)");
}
else if (!autoConfirm)
{
messages.Add("找回邮箱已发送验证码(未自动确认)");
}
else
{
var code = await TryWaitTelegramCodeAsync(email, startUtc, CancellationToken.None);
if (string.IsNullOrWhiteSpace(code))
{
okRecovery = false;
var diag = await TryBuildReceiveDiagAsync(email, CancellationToken.None);
messages.Add($"找回邮箱收码超时({pollTimeoutSeconds}s){(string.IsNullOrWhiteSpace(diag) ? "" : ":" + diag)}");
}
else
{
var (confirmed, confirmErr) = await AccountTelegramTools.ConfirmTwoFactorRecoveryEmailAsync(
accountId: account.Id,
code: code,
cancellationToken: CancellationToken.None);
if (confirmed)
{
messages.Add("找回邮箱已确认绑定");
}
else
{
okRecovery = false;
if ((confirmErr ?? "").Contains("CODE_INVALID", StringComparison.OrdinalIgnoreCase))
{
_ = await AccountTelegramTools.ResendTwoFactorRecoveryEmailAsync(account.Id, CancellationToken.None);
var retryCode = await TryWaitTelegramCodeAsync(email, DateTimeOffset.UtcNow, CancellationToken.None);
if (!string.IsNullOrWhiteSpace(retryCode))
{
var (retryOk, retryErr) = await AccountTelegramTools.ConfirmTwoFactorRecoveryEmailAsync(
accountId: account.Id,
code: retryCode,
cancellationToken: CancellationToken.None);
if (retryOk)
{
okRecovery = true;
messages.Add("找回邮箱已确认绑定(重发后重试成功)");
}
else
{
messages.Add($"找回邮箱确认失败:{retryErr}");
}
}
}
if (!okRecovery)
messages.Add($"找回邮箱确认失败:{confirmErr}");
}
}
}
}
// 2) 登录邮箱(用于登录确认邮件)
if (changeLoginEmail)
{
var (statusOk, statusErr, hasLoginEmail, _) = await AccountTelegramTools.GetLoginEmailStatusAsync(account.Id, CancellationToken.None);
if (!statusOk)
{
okLogin = false;
loginKind = ResultKind.Failed;
messages.Add($"登录邮箱失败:获取状态失败:{statusErr}");
}
else if (!hasLoginEmail && !trySetLoginEmailWhenMissing)
{
loginKind = ResultKind.Skipped;
messages.Add("登录邮箱已跳过:该账号未设置登录邮箱(Telegram 未要求配置时,通常无法在已登录状态下直接新增)");
}
else
{
var startUtc = DateTimeOffset.UtcNow;
var (sent, err, pattern) = await AccountTelegramTools.SetLoginEmailAsync(account.Id, email, CancellationToken.None);
if (!sent)
{
var msg = err ?? "未知错误";
if (msg.Contains("EMAIL_NOT_SETUP", StringComparison.OrdinalIgnoreCase))
{
loginKind = ResultKind.Skipped;
messages.Add($"登录邮箱不支持:{msg}");
}
else
{
okLogin = false;
loginKind = ResultKind.Failed;
messages.Add($"登录邮箱失败:{msg}");
}
}
else if (!autoConfirm)
{
messages.Add($"登录邮箱已发送验证码(未自动确认){(string.IsNullOrWhiteSpace(pattern) ? "" : $",掩码:{pattern}")}");
}
else
{
var code = await TryWaitTelegramCodeAsync(email, startUtc, CancellationToken.None, TelegramMailCodePurpose.LoginEmail);
if (string.IsNullOrWhiteSpace(code))
{
okLogin = false;
loginKind = ResultKind.Failed;
var diag = await TryBuildReceiveDiagAsync(email, CancellationToken.None);
messages.Add($"登录邮箱收码超时({pollTimeoutSeconds}s){(string.IsNullOrWhiteSpace(diag) ? "" : ":" + diag)}");
}
else
{
var (confirmed, confirmErr) = await AccountTelegramTools.ConfirmLoginEmailAsync(account.Id, code, CancellationToken.None);
if (confirmed)
{
messages.Add("登录邮箱已确认");
}
else
{
okLogin = false;
loginKind = ResultKind.Failed;
if ((confirmErr ?? "").Contains("CODE_INVALID", StringComparison.OrdinalIgnoreCase)
|| (confirmErr ?? "").Contains("EMAIL_TOKEN_INVALID", StringComparison.OrdinalIgnoreCase))
{
_ = await AccountTelegramTools.SetLoginEmailAsync(account.Id, email, CancellationToken.None);
var retryCode = await TryWaitTelegramCodeAsync(email, DateTimeOffset.UtcNow, CancellationToken.None, TelegramMailCodePurpose.LoginEmail);
if (!string.IsNullOrWhiteSpace(retryCode))
{
var (retryOk, retryErr) = await AccountTelegramTools.ConfirmLoginEmailAsync(account.Id, retryCode, CancellationToken.None);
if (retryOk)
{
okLogin = true;
loginKind = ResultKind.Success;
messages.Add("登录邮箱已确认(重发后重试成功)");
}
else
{
messages.Add($"登录邮箱确认失败:{retryErr}");
}
}
}
if (!okLogin)
messages.Add($"登录邮箱确认失败:{confirmErr}");
}
}
}
}
}
var kind = changeLoginEmail ? loginKind : (okRecovery ? ResultKind.Success : ResultKind.Failed);
var msgAll = messages.Count == 0 ? null : string.Join(";", messages);
return (kind, msgAll);
}
private async Task<string?> TryWaitTelegramCodeAsync(
string toEmail,
DateTimeOffset startUtc,
CancellationToken cancellationToken,
TelegramMailCodePurpose purpose = TelegramMailCodePurpose.Any,
bool allowOlder = false)
{
var timeout = TimeSpan.FromSeconds(Math.Clamp(pollTimeoutSeconds, 10, 300));
var interval = TimeSpan.FromSeconds(Math.Clamp(pollIntervalSeconds, 2, 30));
var deadline = DateTimeOffset.UtcNow + timeout;
var allowOldBeforeUtc = allowOlder ? DateTimeOffset.UtcNow.AddHours(-24) : startUtc.AddMinutes(-10);
var useFilters = !string.IsNullOrWhiteSpace(sendEmailFilter) || !string.IsNullOrWhiteSpace(subjectFilter);
while (DateTimeOffset.UtcNow < deadline)
{
cancellationToken.ThrowIfCancellationRequested();
List<TelegramPanel.Web.Services.CloudMailEmail> emails = new();
try
{
TelegramPanel.Web.Services.CloudMailEmailListRequest BuildReq(string? recipient) => new TelegramPanel.Web.Services.CloudMailEmailListRequest
{
ToEmail = string.IsNullOrWhiteSpace(recipient) ? null : recipient,
SendEmail = useFilters && !string.IsNullOrWhiteSpace(sendEmailFilter) ? sendEmailFilter.Trim() : null,
Subject = useFilters && !string.IsNullOrWhiteSpace(subjectFilter) ? subjectFilter.Trim() : null,
TimeSort = "desc",
Type = null,
IsDel = null,
Num = 1,
Size = 50
};
// 1) 服务端按收件人精确过滤
emails = await CloudMail.GetEmailListAsync(cloudMailBaseUrl, cloudMailToken, BuildReq(toEmail), cancellationToken);
// 2) 兼容:服务端 toEmail 过滤可能只对模糊生效
if (emails.Count == 0)
emails = await CloudMail.GetEmailListAsync(cloudMailBaseUrl, cloudMailToken, BuildReq("%" + toEmail + "%"), cancellationToken);
// 3) 过滤条件过严时:去掉过滤再试
if (useFilters && emails.Count == 0)
{
emails = await CloudMail.GetEmailListAsync(cloudMailBaseUrl, cloudMailToken, BuildReq(toEmail) with { SendEmail = null, Subject = null }, cancellationToken);
if (emails.Count == 0)
emails = await CloudMail.GetEmailListAsync(cloudMailBaseUrl, cloudMailToken, BuildReq("%" + toEmail + "%") with { SendEmail = null, Subject = null }, cancellationToken);
}
// 4) 最后兜底:拉取最近邮件并在本地匹配收件人
if (emails.Count == 0)
emails = await CloudMail.GetEmailListAsync(cloudMailBaseUrl, cloudMailToken, BuildReq(null) with { SendEmail = null, Subject = null }, cancellationToken);
}
catch
{
await Task.Delay(interval, cancellationToken);
continue;
}
var candidates = new List<(DateTimeOffset CreatedUtc, TelegramPanel.Web.Services.CloudMailEmail Mail)>();
foreach (var mail in FilterCandidates(emails, toEmail))
{
var createdUtc = TryParseUtc(mail.CreateTime);
if (createdUtc.HasValue && createdUtc.Value < allowOldBeforeUtc)
continue;
// 邮件列表可能包含“找回邮箱验证码 + 登录邮箱验证码”,必须按用途过滤,否则容易拿到错误验证码导致 CODE_INVALID。
var merged = BuildMergedText(mail);
if (!IsMatchPurpose(merged, purpose))
continue;
candidates.Add((createdUtc ?? DateTimeOffset.MinValue, mail));
}
foreach (var c in candidates.OrderByDescending(x => x.CreatedUtc))
{
var code = TryExtractCode(c.Mail);
if (!string.IsNullOrWhiteSpace(code))
return code;
}
await Task.Delay(interval, cancellationToken);
}
return null;
}
private enum TelegramMailCodePurpose
{
Any = 0,
RecoveryEmail = 1,
LoginEmail = 2
}
private static bool IsMatchPurpose(string merged, TelegramMailCodePurpose purpose)
{
merged = (merged ?? string.Empty).Trim();
if (merged.Length == 0)
return purpose == TelegramMailCodePurpose.Any;
if (purpose == TelegramMailCodePurpose.Any)
return true;
var lower = merged.ToLowerInvariant();
// 示例:
// - 找回邮箱:"... verify your new password recovery email ..."
// - 登录邮箱:"... verify your email for Login."
if (purpose == TelegramMailCodePurpose.RecoveryEmail)
{
return (lower.Contains("recovery") && lower.Contains("email") && lower.Contains("verify"))
|| lower.Contains("password recovery email");
}
if (purpose == TelegramMailCodePurpose.LoginEmail)
{
return (lower.Contains("verify") && lower.Contains("email") && lower.Contains("login"))
&& !lower.Contains("recovery");
}
return true;
}
private static string BuildMergedText(TelegramPanel.Web.Services.CloudMailEmail mail)
{
var subject = mail.Subject ?? "";
var text = mail.Text ?? "";
var html = mail.Content ?? "";
return $"{subject}\n{text}\n{StripHtml(html)}";
}
private static IEnumerable<TelegramPanel.Web.Services.CloudMailEmail> FilterCandidates(
IEnumerable<TelegramPanel.Web.Services.CloudMailEmail> emails,
string toEmail)
{
var target = (toEmail ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(target))
return emails ?? Array.Empty<TelegramPanel.Web.Services.CloudMailEmail>();
return (emails ?? Array.Empty<TelegramPanel.Web.Services.CloudMailEmail>())
.Where(m =>
{
var e = (m.ToEmail ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(e)) return false;
return string.Equals(e, target, StringComparison.OrdinalIgnoreCase)
|| e.EndsWith(target, StringComparison.OrdinalIgnoreCase)
|| e.Contains(target, StringComparison.OrdinalIgnoreCase);
});
}
private async Task<string?> TryBuildReceiveDiagAsync(string toEmail, CancellationToken cancellationToken)
{
try
{
var req = new TelegramPanel.Web.Services.CloudMailEmailListRequest
{
ToEmail = null,
SendEmail = null,
Subject = null,
TimeSort = "desc",
Type = null,
IsDel = null,
Num = 1,
Size = 10
};
var all = await CloudMail.GetEmailListAsync(cloudMailBaseUrl, cloudMailToken, req, cancellationToken);
var matched = FilterCandidates(all, toEmail).ToList();
if (matched.Count > 0)
{
var top = matched[0];
return $"Cloud Mail 已拉到 {matched.Count} 封匹配邮件(最近主题:{(top.Subject ?? "").Trim()},时间:{(top.CreateTime ?? "").Trim()}),但未能解析出验证码";
}
var any = all.FirstOrDefault();
if (any == null)
return "Cloud Mail emailList 返回空列表(可能是 Token/权限/收件箱同步问题)";
return $"Cloud Mail 拉到 {all.Count} 封最近邮件,但未匹配到收件人 {toEmail}(最近一封收件人:{(any.ToEmail ?? "").Trim()})";
}
catch (Exception ex)
{
return $"收码诊断失败:{ex.Message}";
}
}
private static DateTimeOffset? TryParseUtc(string? createTime)
{
createTime = (createTime ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(createTime))
return null;
if (DateTime.TryParse(createTime, out var dt))
{
if (dt.Kind == DateTimeKind.Unspecified)
dt = DateTime.SpecifyKind(dt, DateTimeKind.Utc);
else
dt = dt.ToUniversalTime();
return new DateTimeOffset(dt);
}
return null;
}
private static string? TryExtractCode(TelegramPanel.Web.Services.CloudMailEmail mail)
{
var subject = mail.Subject ?? "";
var text = mail.Text ?? "";
var html = mail.Content ?? "";
var merged = $"{subject}\n{text}\n{StripHtml(html)}";
// Telegram 常见邮件模板:
// - "Your Code - 164581"
// - "Your code is: 164581."
var m = Regex.Match(merged, "(?i)your\\s+code\\s*[-–—]\\s*(\\d{5,6})");
if (m.Success) return m.Groups[1].Value;
m = Regex.Match(merged, "(?i)your\\s+code\\s+is\\s*[::]?\\s*(\\d{5,6})");
if (m.Success) return m.Groups[1].Value;
var matches = Regex.Matches(merged, "\\b\\d{5,6}\\b");
if (matches.Count == 0)
return null;
return matches[^1].Value;
}
private static string StripHtml(string html)
{
if (string.IsNullOrWhiteSpace(html))
return string.Empty;
return Regex.Replace(html, "<.*?>", " ").Replace("&nbsp;", " ").Trim();
}
private void Cancel() => MudDialog.Close();
private enum ResultKind
{
Success,
Failed,
Skipped
}
private static string KindToText(ResultKind kind) => kind switch
{
ResultKind.Success => "成功",
ResultKind.Failed => "失败",
ResultKind.Skipped => "跳过",
_ => "-"
};
private sealed record ResultRow(string Phone, string Email, string Result, string? Message);
}