diff --git a/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs b/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs
index 567cd04..309d33b 100644
--- a/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs
+++ b/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using System.Net.Mail;
using TelegramPanel.Core.Interfaces;
using TelegramPanel.Core.Models;
using TelegramPanel.Data.Entities;
@@ -326,6 +327,190 @@ public class AccountTelegramToolsService
}
}
+ ///
+ /// 获取两步验证找回邮箱状态(是否已绑定、是否存在待确认的邮箱)。
+ ///
+ public async Task<(bool Success, string? Error, bool HasTwoFactorPassword, bool HasRecoveryEmail, string? UnconfirmedEmailPattern)>
+ GetTwoFactorRecoveryEmailStatusAsync(int accountId, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var client = await GetOrCreateConnectedClientAsync(accountId, cancellationToken);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var pwd = await client.Account_GetPassword();
+ var hasPassword = pwd.current_algo != null;
+ var hasRecoveryEmail = pwd.flags.HasFlag(TL.Account_Password.Flags.has_recovery);
+ var unconfirmed = pwd.flags.HasFlag(TL.Account_Password.Flags.has_email_unconfirmed_pattern)
+ ? (pwd.email_unconfirmed_pattern ?? "").Trim()
+ : null;
+
+ if (string.IsNullOrWhiteSpace(unconfirmed))
+ unconfirmed = null;
+
+ return (true, null, hasPassword, hasRecoveryEmail, unconfirmed);
+ }
+ catch (Exception ex)
+ {
+ var (summary, details) = MapTelegramException(ex);
+ var msg = string.IsNullOrWhiteSpace(details) ? summary : $"{summary}:{details}";
+ return (false, msg, false, false, null);
+ }
+ }
+
+ ///
+ /// 绑定/换绑两步验证找回邮箱(会发送验证码到邮箱,需调用 ConfirmTwoFactorRecoveryEmailAsync 确认)。
+ ///
+ public async Task<(bool Success, string? Error, string? EmailPattern)> SetTwoFactorRecoveryEmailAsync(
+ int accountId,
+ string? currentPassword,
+ string email,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ email = (email ?? string.Empty).Trim();
+ if (string.IsNullOrWhiteSpace(email))
+ return (false, "邮箱不能为空", null);
+
+ try
+ {
+ _ = new MailAddress(email);
+ }
+ catch
+ {
+ return (false, "邮箱格式不正确", null);
+ }
+
+ currentPassword = (currentPassword ?? string.Empty).Trim();
+
+ var client = await GetOrCreateConnectedClientAsync(accountId, cancellationToken);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var pwd = await client.Account_GetPassword();
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (pwd.current_algo == null)
+ return (false, "该账号未开启两步验证,无法绑定找回邮箱,请先设置二级密码", null);
+
+ if (string.IsNullOrWhiteSpace(currentPassword))
+ return (false, "请填写原二级密码", null);
+
+ var oldCheck = await WTelegram.Client.InputCheckPassword(pwd, currentPassword);
+
+ var settings = new TL.Account_PasswordInputSettings
+ {
+ flags = TL.Account_PasswordInputSettings.Flags.has_email,
+ email = email
+ };
+
+ await client.Account_UpdatePasswordSettings(oldCheck, settings);
+
+ // 更新后可通过 getPassword 获取“待确认邮箱”掩码信息
+ var after = await client.Account_GetPassword();
+ var pattern = after.flags.HasFlag(TL.Account_Password.Flags.has_email_unconfirmed_pattern)
+ ? (after.email_unconfirmed_pattern ?? "").Trim()
+ : null;
+
+ if (string.IsNullOrWhiteSpace(pattern))
+ pattern = null;
+
+ return (true, null, pattern);
+ }
+ catch (Exception ex)
+ {
+ var (summary, details) = MapTelegramException(ex);
+ var msg = string.IsNullOrWhiteSpace(details) ? summary : $"{summary}:{details}";
+ return (false, msg, null);
+ }
+ }
+
+ ///
+ /// 确认两步验证找回邮箱验证码。
+ ///
+ public async Task<(bool Success, string? Error)> ConfirmTwoFactorRecoveryEmailAsync(
+ int accountId,
+ string code,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ code = (code ?? string.Empty).Trim();
+ if (string.IsNullOrWhiteSpace(code))
+ return (false, "验证码不能为空");
+
+ var client = await GetOrCreateConnectedClientAsync(accountId, cancellationToken);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ _ = await client.Account_ConfirmPasswordEmail(code);
+ return (true, null);
+ }
+ catch (Exception ex)
+ {
+ var (summary, details) = MapTelegramException(ex);
+ var msg = string.IsNullOrWhiteSpace(details) ? summary : $"{summary}:{details}";
+ return (false, msg);
+ }
+ }
+
+ ///
+ /// 重发两步验证找回邮箱验证码(需要先设置邮箱)。
+ ///
+ public async Task<(bool Success, string? Error, string? EmailPattern, int? CodeLength)> ResendTwoFactorRecoveryEmailAsync(
+ int accountId,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var client = await GetOrCreateConnectedClientAsync(accountId, cancellationToken);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var ok = await client.Account_ResendPasswordEmail();
+ if (!ok)
+ return (false, "重发失败", null, null);
+
+ var pwd = await client.Account_GetPassword();
+ var pattern = pwd.flags.HasFlag(TL.Account_Password.Flags.has_email_unconfirmed_pattern)
+ ? (pwd.email_unconfirmed_pattern ?? "").Trim()
+ : null;
+
+ if (string.IsNullOrWhiteSpace(pattern))
+ pattern = null;
+
+ // 该 API 不返回验证码长度,仅返回邮箱掩码信息
+ return (true, null, pattern, null);
+ }
+ catch (Exception ex)
+ {
+ var (summary, details) = MapTelegramException(ex);
+ var msg = string.IsNullOrWhiteSpace(details) ? summary : $"{summary}:{details}";
+ return (false, msg, null, null);
+ }
+ }
+
+ ///
+ /// 取消待确认的找回邮箱验证码。
+ ///
+ public async Task<(bool Success, string? Error)> CancelTwoFactorRecoveryEmailAsync(
+ int accountId,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var client = await GetOrCreateConnectedClientAsync(accountId, cancellationToken);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ _ = await client.Account_CancelPasswordEmail();
+ return (true, null);
+ }
+ catch (Exception ex)
+ {
+ var (summary, details) = MapTelegramException(ex);
+ var msg = string.IsNullOrWhiteSpace(details) ? summary : $"{summary}:{details}";
+ return (false, msg);
+ }
+ }
+
public async Task KickAuthorizationAsync(int accountId, long authorizationHash)
{
var client = await GetOrCreateConnectedClientAsync(accountId);
diff --git a/src/TelegramPanel.Web/Components/Dialogs/ChangeTwoFactorRecoveryEmailDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/ChangeTwoFactorRecoveryEmailDialog.razor
new file mode 100644
index 0000000..067f7ab
--- /dev/null
+++ b/src/TelegramPanel.Web/Components/Dialogs/ChangeTwoFactorRecoveryEmailDialog.razor
@@ -0,0 +1,328 @@
+@namespace TelegramPanel.Web.Components.Dialogs
+@inject AccountManagementService AccountManagement
+@inject TelegramPanel.Core.Services.Telegram.AccountTelegramToolsService AccountTelegramTools
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+@using TelegramPanel.Data.Entities
+
+
+
+
+
+ 这里绑定的是 Telegram 两步验证(2FA)的“找回邮箱”。修改后会向新邮箱发送验证码,需要输入验证码确认。
+
+
+ @if (!loaded)
+ {
+
+ }
+ else if (account == null)
+ {
+ 账号不存在
+ }
+ else
+ {
+ 账号:@account.Phone
+
+
+
+ @(hasTwoFactorPassword ? "已开启2FA" : "未开启2FA")
+
+
+ @(hasRecoveryEmail ? "已绑定找回邮箱" : "未绑定找回邮箱")
+
+ @if (!string.IsNullOrWhiteSpace(unconfirmedEmailPattern))
+ {
+ 待确认:@unconfirmedEmailPattern
+ }
+
+
+ @if (!hasTwoFactorPassword)
+ {
+
+ 该账号未开启两步验证,无法绑定找回邮箱。请先设置二级密码。
+
+ }
+ else if (step == Step.SetEmail)
+ {
+
+
+
+ }
+ else
+ {
+
+
+
+
+ 重发验证码
+
+
+ 取消验证
+
+
+ }
+ }
+
+
+
+ 关闭
+
+ @if (loaded && account != null && !hasTwoFactorPassword)
+ {
+
+ 去设置二级密码
+
+ }
+ else if (loaded && account != null && hasTwoFactorPassword)
+ {
+ @if (step == Step.SetEmail)
+ {
+
+ @if (running)
+ {
+
+ 发送中...
+ }
+ else
+ {
+ 发送验证码
+ }
+
+ }
+ else
+ {
+
+ @if (running)
+ {
+
+ 确认中...
+ }
+ else
+ {
+ 确认绑定
+ }
+
+ }
+ }
+
+
+
+@code
+{
+ [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
+ [Parameter] public int AccountId { get; set; }
+
+ private Account? account;
+ private bool loaded;
+ private bool running;
+
+ private bool hasTwoFactorPassword;
+ private bool hasRecoveryEmail;
+ private string? unconfirmedEmailPattern;
+
+ private Step step = Step.SetEmail;
+
+ private string currentPassword = "";
+ private string email = "";
+ private string code = "";
+
+ protected override async Task OnInitializedAsync()
+ {
+ await ReloadStatus();
+ }
+
+ private async Task ReloadStatus()
+ {
+ loaded = false;
+ try
+ {
+ var all = await AccountManagement.GetAllAccountsAsync();
+ account = all.FirstOrDefault(a => a.Id == AccountId);
+
+ if (account != null)
+ {
+ var (ok, err, hasPwd, hasEmail, unconfirmed) = await AccountTelegramTools.GetTwoFactorRecoveryEmailStatusAsync(AccountId);
+ if (!ok)
+ {
+ Snackbar.Add($"获取2FA邮箱状态失败:{err}", Severity.Warning);
+ }
+ hasTwoFactorPassword = hasPwd;
+ hasRecoveryEmail = hasEmail;
+ unconfirmedEmailPattern = unconfirmed;
+
+ step = string.IsNullOrWhiteSpace(unconfirmedEmailPattern) ? Step.SetEmail : Step.Confirm;
+ }
+ }
+ finally
+ {
+ loaded = true;
+ }
+ }
+
+ private async Task SendCode()
+ {
+ if (running)
+ return;
+
+ if (account == null)
+ return;
+
+ if (string.IsNullOrWhiteSpace(currentPassword))
+ {
+ Snackbar.Add("请填写原二级密码", Severity.Warning);
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(email))
+ {
+ Snackbar.Add("请填写新找回邮箱", Severity.Warning);
+ return;
+ }
+
+ bool? confirm = await DialogService.ShowMessageBox(
+ "确认发送验证码",
+ "将向新邮箱发送验证码,用于绑定/换绑找回邮箱。是否继续?",
+ yesText: "继续", cancelText: "取消");
+
+ if (confirm != true)
+ return;
+
+ running = true;
+ try
+ {
+ var (ok, err, pattern) = await AccountTelegramTools.SetTwoFactorRecoveryEmailAsync(
+ accountId: AccountId,
+ currentPassword: currentPassword,
+ email: email,
+ cancellationToken: CancellationToken.None);
+
+ if (!ok)
+ {
+ Snackbar.Add($"发送失败:{err}", Severity.Error);
+ return;
+ }
+
+ unconfirmedEmailPattern = pattern;
+ step = Step.Confirm;
+ code = "";
+ Snackbar.Add("验证码已发送,请到邮箱中查看并输入验证码确认。", Severity.Success);
+ }
+ finally
+ {
+ running = false;
+ }
+ }
+
+ private async Task Confirm()
+ {
+ if (running)
+ return;
+
+ if (string.IsNullOrWhiteSpace(code))
+ {
+ Snackbar.Add("请填写邮箱验证码", Severity.Warning);
+ return;
+ }
+
+ running = true;
+ try
+ {
+ var (ok, err) = await AccountTelegramTools.ConfirmTwoFactorRecoveryEmailAsync(AccountId, code, CancellationToken.None);
+ if (!ok)
+ {
+ Snackbar.Add($"确认失败:{err}", Severity.Error);
+ return;
+ }
+
+ Snackbar.Add("找回邮箱已确认绑定。", Severity.Success);
+ await ReloadStatus();
+ }
+ finally
+ {
+ running = false;
+ }
+ }
+
+ private async Task Resend()
+ {
+ if (running)
+ return;
+
+ running = true;
+ try
+ {
+ var (ok, err, pattern, _) = await AccountTelegramTools.ResendTwoFactorRecoveryEmailAsync(AccountId, CancellationToken.None);
+ if (!ok)
+ {
+ Snackbar.Add($"重发失败:{err}", Severity.Error);
+ return;
+ }
+
+ unconfirmedEmailPattern = pattern ?? unconfirmedEmailPattern;
+ Snackbar.Add("验证码已重发。", Severity.Success);
+ }
+ finally
+ {
+ running = false;
+ }
+ }
+
+ private async Task CancelVerification()
+ {
+ if (running)
+ return;
+
+ bool? confirm = await DialogService.ShowMessageBox(
+ "取消验证",
+ "将取消当前待确认的邮箱验证码。是否继续?",
+ yesText: "继续", cancelText: "取消");
+
+ if (confirm != true)
+ return;
+
+ running = true;
+ try
+ {
+ var (ok, err) = await AccountTelegramTools.CancelTwoFactorRecoveryEmailAsync(AccountId, CancellationToken.None);
+ if (!ok)
+ {
+ Snackbar.Add($"取消失败:{err}", Severity.Error);
+ return;
+ }
+
+ Snackbar.Add("已取消验证。", Severity.Success);
+ await ReloadStatus();
+ }
+ finally
+ {
+ running = false;
+ }
+ }
+
+ private void Close() => MudDialog.Close();
+
+ private async Task OpenPasswordDialog()
+ {
+ if (account == null)
+ return;
+
+ var parameters = new DialogParameters
+ {
+ ["AccountIds"] = new List { account.Id }
+ };
+ var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
+ DialogService.Show("设置二级密码", parameters, options);
+ MudDialog.Close();
+ await Task.CompletedTask;
+ }
+
+ private enum Step
+ {
+ SetEmail,
+ Confirm
+ }
+}
diff --git a/src/TelegramPanel.Web/Components/Pages/Accounts.razor b/src/TelegramPanel.Web/Components/Pages/Accounts.razor
index 2b080de..278e533 100644
--- a/src/TelegramPanel.Web/Components/Pages/Accounts.razor
+++ b/src/TelegramPanel.Web/Components/Pages/Accounts.razor
@@ -125,6 +125,8 @@
Disabled="@loading" OnClick="@(() => OpenDevices(context))" Title="在线设备" />
+
("绑定/换绑找回邮箱", parameters, options);
+ await Task.CompletedTask;
+ }
+
private async Task OpenBatchChangeTwoFactorPassword()
{
if (loading)