feat(mail): 添加收件箱功能,自动获得验证码

This commit is contained in:
cnlimiter
2026-03-20 12:54:39 +08:00
parent 87ff48cdaf
commit b1a8d02353
16 changed files with 186 additions and 44 deletions

View File

@@ -84,7 +84,7 @@ a = Analysis(
'src.core.utils', 'src.core.utils',
'src.services.base', 'src.services.base',
'src.services.tempmail', 'src.services.tempmail',
'src.services.custom_domain', 'src.services.moe_mail',
'src.services.outlook', 'src.services.outlook',
'src.services.outlook.account', 'src.services.outlook.account',
'src.services.outlook.base', 'src.services.outlook.base',

View File

@@ -33,7 +33,7 @@ class EmailServiceType(str, Enum):
"""邮箱服务类型""" """邮箱服务类型"""
TEMPMAIL = "tempmail" TEMPMAIL = "tempmail"
OUTLOOK = "outlook" OUTLOOK = "outlook"
CUSTOM_DOMAIN = "custom_domain" MOE_MAIL = "moe_mail"
TEMP_MAIL = "temp_mail" TEMP_MAIL = "temp_mail"
DUCK_MAIL = "duck_mail" DUCK_MAIL = "duck_mail"
FREEMAIL = "freemail" FREEMAIL = "freemail"
@@ -109,7 +109,7 @@ EMAIL_SERVICE_DEFAULTS = {
"smtp_port": 587, "smtp_port": 587,
"timeout": 30, "timeout": 30,
}, },
"custom_domain": { "moe_mail": {
"base_url": "", # 需要用户配置 "base_url": "", # 需要用户配置
"api_key_header": "X-API-Key", "api_key_header": "X-API-Key",
"timeout": 30, "timeout": 30,

View File

@@ -22,7 +22,7 @@ class SettingCategory(str, Enum):
REGISTRATION = "registration" REGISTRATION = "registration"
EMAIL = "email" EMAIL = "email"
TEMPMAIL = "tempmail" TEMPMAIL = "tempmail"
CUSTOM_DOMAIN = "custom_domain" CUSTOM_DOMAIN = "moe_mail"
SECURITY = "security" SECURITY = "security"
CPA = "cpa" CPA = "cpa"
@@ -252,7 +252,7 @@ SETTING_DEFINITIONS: Dict[str, SettingDefinition] = {
# 邮箱服务配置 # 邮箱服务配置
"email_service_priority": SettingDefinition( "email_service_priority": SettingDefinition(
db_key="email.service_priority", db_key="email.service_priority",
default_value={"tempmail": 0, "outlook": 1, "custom_domain": 2}, default_value={"tempmail": 0, "outlook": 1, "moe_mail": 2},
category=SettingCategory.EMAIL, category=SettingCategory.EMAIL,
description="邮箱服务优先级" description="邮箱服务优先级"
), ),
@@ -665,7 +665,7 @@ class Settings(BaseModel):
registration_sleep_max: int = 30 registration_sleep_max: int = 30
# 邮箱服务配置 # 邮箱服务配置
email_service_priority: Dict[str, int] = {"tempmail": 0, "outlook": 1, "custom_domain": 2} email_service_priority: Dict[str, int] = {"tempmail": 0, "outlook": 1, "moe_mail": 2}
# Tempmail.lol 配置 # Tempmail.lol 配置
tempmail_base_url: str = "https://api.tempmail.lol/v2" tempmail_base_url: str = "https://api.tempmail.lol/v2"

View File

@@ -42,7 +42,7 @@ class Account(Base):
client_id = Column(String(255)) # OAuth Client ID client_id = Column(String(255)) # OAuth Client ID
account_id = Column(String(255)) account_id = Column(String(255))
workspace_id = Column(String(255)) workspace_id = Column(String(255))
email_service = Column(String(50), nullable=False) # 'tempmail', 'outlook', 'custom_domain' email_service = Column(String(50), nullable=False) # 'tempmail', 'outlook', 'moe_mail'
email_service_id = Column(String(255)) # 邮箱服务中的ID email_service_id = Column(String(255)) # 邮箱服务中的ID
proxy_used = Column(String(255)) proxy_used = Column(String(255))
registered_at = Column(DateTime, default=datetime.utcnow) registered_at = Column(DateTime, default=datetime.utcnow)
@@ -89,7 +89,7 @@ class EmailService(Base):
__tablename__ = 'email_services' __tablename__ = 'email_services'
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
service_type = Column(String(50), nullable=False) # 'outlook', 'custom_domain' service_type = Column(String(50), nullable=False) # 'outlook', 'moe_mail'
name = Column(String(100), nullable=False) name = Column(String(100), nullable=False)
config = Column(JSONEncodedDict, nullable=False) # 服务配置(加密存储) config = Column(JSONEncodedDict, nullable=False) # 服务配置(加密存储)
enabled = Column(Boolean, default=True) enabled = Column(Boolean, default=True)

View File

@@ -117,6 +117,14 @@ class DatabaseSessionManager:
Base.metadata.create_all(bind=self.engine) Base.metadata.create_all(bind=self.engine)
with self.engine.connect() as conn: with self.engine.connect() as conn:
# 数据迁移:将旧的 custom_domain 记录统一为 moe_mail
try:
conn.execute(text("UPDATE email_services SET service_type='moe_mail' WHERE service_type='custom_domain'"))
conn.execute(text("UPDATE accounts SET email_service='moe_mail' WHERE email_service='custom_domain'"))
conn.commit()
except Exception as e:
logger.warning(f"迁移 custom_domain -> moe_mail 时出错: {e}")
for table_name, column_name, column_type in migrations: for table_name, column_name, column_type in migrations:
try: try:
# 检查列是否存在 # 检查列是否存在

View File

@@ -20,7 +20,7 @@ from .freemail import FreemailService
# 注册服务 # 注册服务
EmailServiceFactory.register(EmailServiceType.TEMPMAIL, TempmailService) EmailServiceFactory.register(EmailServiceType.TEMPMAIL, TempmailService)
EmailServiceFactory.register(EmailServiceType.OUTLOOK, OutlookService) EmailServiceFactory.register(EmailServiceType.OUTLOOK, OutlookService)
EmailServiceFactory.register(EmailServiceType.CUSTOM_DOMAIN, MeoMailEmailService) EmailServiceFactory.register(EmailServiceType.MOE_MAIL, MeoMailEmailService)
EmailServiceFactory.register(EmailServiceType.TEMP_MAIL, TempMailService) EmailServiceFactory.register(EmailServiceType.TEMP_MAIL, TempMailService)
EmailServiceFactory.register(EmailServiceType.DUCK_MAIL, DuckMailService) EmailServiceFactory.register(EmailServiceType.DUCK_MAIL, DuckMailService)
EmailServiceFactory.register(EmailServiceType.FREEMAIL, FreemailService) EmailServiceFactory.register(EmailServiceType.FREEMAIL, FreemailService)

View File

@@ -40,7 +40,7 @@ class MeoMailEmailService(BaseEmailService):
- default_expiry: 默认过期时间(毫秒) - default_expiry: 默认过期时间(毫秒)
name: 服务名称 name: 服务名称
""" """
super().__init__(EmailServiceType.CUSTOM_DOMAIN, name) super().__init__(EmailServiceType.MOE_MAIL, name)
# 必需配置检查 # 必需配置检查
required_keys = ["base_url", "api_key"] required_keys = ["base_url", "api_key"]

View File

@@ -950,3 +950,105 @@ async def upload_account_to_tm(account_id: int, request: Optional[UploadTMReques
success, message = upload_to_team_manager(account, api_url, api_key) success, message = upload_to_team_manager(account, api_url, api_key)
return {"success": success, "message": message} return {"success": success, "message": message}
# ============== Inbox Code ==============
def _build_inbox_config(db, service_type, email: str) -> dict:
"""根据账号邮箱服务类型从数据库构建服务配置(不传 proxy_url"""
from ...database.models import EmailService as EmailServiceModel
from ...services import EmailServiceType as EST
if service_type == EST.TEMPMAIL:
settings = get_settings()
return {
"base_url": settings.tempmail_base_url,
"timeout": settings.tempmail_timeout,
"max_retries": settings.tempmail_max_retries,
}
if service_type == EST.MOE_MAIL:
# 按域名后缀匹配,找不到则取 priority 最小的
domain = email.split("@")[1] if "@" in email else ""
services = db.query(EmailServiceModel).filter(
EmailServiceModel.service_type == "moe_mail",
EmailServiceModel.enabled == True
).order_by(EmailServiceModel.priority.asc()).all()
svc = None
for s in services:
cfg = s.config or {}
if cfg.get("default_domain") == domain or cfg.get("domain") == domain:
svc = s
break
if not svc and services:
svc = services[0]
if not svc:
return None
cfg = svc.config.copy()
if "api_url" in cfg and "base_url" not in cfg:
cfg["base_url"] = cfg.pop("api_url")
return cfg
# 其余服务类型:直接按 service_type 查数据库
type_map = {
EST.TEMP_MAIL: "temp_mail",
EST.DUCK_MAIL: "duck_mail",
EST.FREEMAIL: "freemail",
EST.OUTLOOK: "outlook",
}
db_type = type_map.get(service_type)
if not db_type:
return None
query = db.query(EmailServiceModel).filter(
EmailServiceModel.service_type == db_type,
EmailServiceModel.enabled == True
)
if service_type == EST.OUTLOOK:
# 按 config.email 匹配账号 email
services = query.all()
svc = next((s for s in services if (s.config or {}).get("email") == email), None)
else:
svc = query.order_by(EmailServiceModel.priority.asc()).first()
if not svc:
return None
cfg = svc.config.copy() if svc.config else {}
if "api_url" in cfg and "base_url" not in cfg:
cfg["base_url"] = cfg.pop("api_url")
return cfg
@router.post("/{account_id}/inbox-code")
async def get_account_inbox_code(account_id: int):
"""查询账号邮箱收件箱最新验证码"""
from ...services import EmailServiceFactory, EmailServiceType
with get_db() as db:
account = crud.get_account_by_id(db, account_id)
if not account:
raise HTTPException(status_code=404, detail="账号不存在")
try:
service_type = EmailServiceType(account.email_service)
except ValueError:
return {"success": False, "error": "不支持的邮箱服务类型"}
config = _build_inbox_config(db, service_type, account.email)
if config is None:
return {"success": False, "error": "未找到可用的邮箱服务配置"}
try:
svc = EmailServiceFactory.create(service_type, config)
code = svc.get_verification_code(
account.email,
email_id=account.email_service_id,
timeout=12
)
except Exception as e:
return {"success": False, "error": str(e)}
if not code:
return {"success": False, "error": "未收到验证码邮件"}
return {"success": True, "code": code, "email": account.email}

View File

@@ -153,7 +153,7 @@ async def get_email_services_stats():
for service_type, count in type_stats: for service_type, count in type_stats:
if service_type == 'outlook': if service_type == 'outlook':
stats['outlook_count'] = count stats['outlook_count'] = count
elif service_type == 'custom_domain': elif service_type == 'moe_mail':
stats['custom_count'] = count stats['custom_count'] = count
elif service_type == 'temp_mail': elif service_type == 'temp_mail':
stats['temp_mail_count'] = count stats['temp_mail_count'] = count
@@ -191,8 +191,8 @@ async def get_service_types():
] ]
}, },
{ {
"value": "custom_domain", "value": "moe_mail",
"label": "自定义域名", "label": "MoeMail",
"description": "自定义域名邮箱服务", "description": "自定义域名邮箱服务",
"config_fields": [ "config_fields": [
{"name": "base_url", "label": "API 地址", "required": True}, {"name": "base_url", "label": "API 地址", "required": True},

View File

@@ -205,7 +205,7 @@ def _normalize_email_service_config(
if 'api_url' in normalized and 'base_url' not in normalized: if 'api_url' in normalized and 'base_url' not in normalized:
normalized['base_url'] = normalized.pop('api_url') normalized['base_url'] = normalized.pop('api_url')
if service_type == EmailServiceType.CUSTOM_DOMAIN: if service_type == EmailServiceType.MOE_MAIL:
if 'domain' in normalized and 'default_domain' not in normalized: if 'domain' in normalized and 'default_domain' not in normalized:
normalized['default_domain'] = normalized.pop('domain') normalized['default_domain'] = normalized.pop('domain')
elif service_type in (EmailServiceType.TEMP_MAIL, EmailServiceType.FREEMAIL): elif service_type in (EmailServiceType.TEMP_MAIL, EmailServiceType.FREEMAIL):
@@ -291,11 +291,11 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy:
"max_retries": settings.tempmail_max_retries, "max_retries": settings.tempmail_max_retries,
"proxy_url": actual_proxy_url, "proxy_url": actual_proxy_url,
} }
elif service_type == EmailServiceType.CUSTOM_DOMAIN: elif service_type == EmailServiceType.MOE_MAIL:
# 检查数据库中是否有可用的自定义域名服务 # 检查数据库中是否有可用的自定义域名服务
from ...database.models import EmailService as EmailServiceModel from ...database.models import EmailService as EmailServiceModel
db_service = db.query(EmailServiceModel).filter( db_service = db.query(EmailServiceModel).filter(
EmailServiceModel.service_type == "custom_domain", EmailServiceModel.service_type == "moe_mail",
EmailServiceModel.enabled == True EmailServiceModel.enabled == True
).order_by(EmailServiceModel.priority.asc()).first() ).order_by(EmailServiceModel.priority.asc()).first()
@@ -796,7 +796,7 @@ async def start_registration(
""" """
启动注册任务 启动注册任务
- email_service_type: 邮箱服务类型 (tempmail, outlook, custom_domain) - email_service_type: 邮箱服务类型 (tempmail, outlook, moe_mail)
- proxy: 代理地址 - proxy: 代理地址
- email_service_config: 邮箱服务配置outlook 需要提供账户信息) - email_service_config: 邮箱服务配置outlook 需要提供账户信息)
""" """
@@ -1069,7 +1069,7 @@ async def get_available_email_services():
返回所有已启用的邮箱服务,包括: 返回所有已启用的邮箱服务,包括:
- tempmail: 临时邮箱(无需配置) - tempmail: 临时邮箱(无需配置)
- outlook: 已导入的 Outlook 账户 - outlook: 已导入的 Outlook 账户
- custom_domain: 已配置的自定义域名服务 - moe_mail: 已配置的自定义域名服务
""" """
from ...database.models import EmailService as EmailServiceModel from ...database.models import EmailService as EmailServiceModel
from ...config.settings import get_settings from ...config.settings import get_settings
@@ -1091,7 +1091,7 @@ async def get_available_email_services():
"count": 0, "count": 0,
"services": [] "services": []
}, },
"custom_domain": { "moe_mail": {
"available": False, "available": False,
"count": 0, "count": 0,
"services": [] "services": []
@@ -1135,32 +1135,32 @@ async def get_available_email_services():
# 获取自定义域名服务 # 获取自定义域名服务
custom_services = db.query(EmailServiceModel).filter( custom_services = db.query(EmailServiceModel).filter(
EmailServiceModel.service_type == "custom_domain", EmailServiceModel.service_type == "moe_mail",
EmailServiceModel.enabled == True EmailServiceModel.enabled == True
).order_by(EmailServiceModel.priority.asc()).all() ).order_by(EmailServiceModel.priority.asc()).all()
for service in custom_services: for service in custom_services:
config = service.config or {} config = service.config or {}
result["custom_domain"]["services"].append({ result["moe_mail"]["services"].append({
"id": service.id, "id": service.id,
"name": service.name, "name": service.name,
"type": "custom_domain", "type": "moe_mail",
"default_domain": config.get("default_domain"), "default_domain": config.get("default_domain"),
"priority": service.priority "priority": service.priority
}) })
result["custom_domain"]["count"] = len(custom_services) result["moe_mail"]["count"] = len(custom_services)
result["custom_domain"]["available"] = len(custom_services) > 0 result["moe_mail"]["available"] = len(custom_services) > 0
# 如果数据库中没有自定义域名服务,检查 settings # 如果数据库中没有自定义域名服务,检查 settings
if not result["custom_domain"]["available"]: if not result["moe_mail"]["available"]:
if settings.custom_domain_base_url and settings.custom_domain_api_key: if settings.custom_domain_base_url and settings.custom_domain_api_key:
result["custom_domain"]["available"] = True result["moe_mail"]["available"] = True
result["custom_domain"]["count"] = 1 result["moe_mail"]["count"] = 1
result["custom_domain"]["services"].append({ result["moe_mail"]["services"].append({
"id": None, "id": None,
"name": "默认自定义域名服务", "name": "默认自定义域名服务",
"type": "custom_domain", "type": "moe_mail",
"from_settings": True "from_settings": True
}) })

View File

@@ -329,6 +329,7 @@ function renderAccounts(accounts) {
<a href="#" class="dropdown-item" onclick="event.preventDefault();closeMoreMenu(this);refreshToken(${account.id})">刷新</a> <a href="#" class="dropdown-item" onclick="event.preventDefault();closeMoreMenu(this);refreshToken(${account.id})">刷新</a>
<a href="#" class="dropdown-item" onclick="event.preventDefault();closeMoreMenu(this);uploadAccount(${account.id})">上传</a> <a href="#" class="dropdown-item" onclick="event.preventDefault();closeMoreMenu(this);uploadAccount(${account.id})">上传</a>
<a href="#" class="dropdown-item" onclick="event.preventDefault();closeMoreMenu(this);markSubscription(${account.id})">标记</a> <a href="#" class="dropdown-item" onclick="event.preventDefault();closeMoreMenu(this);markSubscription(${account.id})">标记</a>
<a href="#" class="dropdown-item" onclick="event.preventDefault();closeMoreMenu(this);checkInboxCode(${account.id})">收件箱</a>
</div> </div>
</div> </div>
<button class="btn btn-danger btn-sm" onclick="deleteAccount(${account.id}, '${escapeHtml(account.email)}')">删除</button> <button class="btn btn-danger btn-sm" onclick="deleteAccount(${account.id}, '${escapeHtml(account.email)}')">删除</button>
@@ -1217,3 +1218,34 @@ async function saveCookies(id) {
toast.error('保存 Cookies 失败: ' + e.message); toast.error('保存 Cookies 失败: ' + e.message);
} }
} }
// 查询收件箱验证码
async function checkInboxCode(id) {
toast.info('正在查询收件箱...');
try {
const result = await api.post(`/accounts/${id}/inbox-code`);
if (result.success) {
showInboxCodeResult(result.code, result.email);
} else {
toast.error('查询失败: ' + (result.error || '未收到验证码'));
}
} catch (error) {
toast.error('查询失败: ' + error.message);
}
}
function showInboxCodeResult(code, email) {
elements.modalBody.innerHTML = `
<div style="text-align:center; padding:24px 16px;">
<div style="font-size:13px;color:var(--text-muted);margin-bottom:12px;">
${escapeHtml(email)} 最新验证码
</div>
<div style="font-size:36px;font-weight:700;letter-spacing:8px;
color:var(--primary);font-family:monospace;margin-bottom:20px;">
${escapeHtml(code)}
</div>
<button class="btn btn-primary" onclick="copyToClipboard('${escapeHtml(code)}')">复制验证码</button>
</div>
`;
elements.detailModal.classList.add('active');
}

View File

@@ -21,7 +21,7 @@ let toastShown = false; // 标记是否已显示过 toast
let availableServices = { let availableServices = {
tempmail: { available: true, services: [] }, tempmail: { available: true, services: [] },
outlook: { available: false, services: [] }, outlook: { available: false, services: [] },
custom_domain: { available: false, services: [] }, moe_mail: { available: false, services: [] },
temp_mail: { available: false, services: [] }, temp_mail: { available: false, services: [] },
duck_mail: { available: false, services: [] }, duck_mail: { available: false, services: [] },
freemail: { available: false, services: [] } freemail: { available: false, services: [] }
@@ -293,15 +293,15 @@ function updateEmailServiceOptions() {
} }
// 自定义域名 // 自定义域名
if (availableServices.custom_domain.available) { if (availableServices.moe_mail.available) {
const optgroup = document.createElement('optgroup'); const optgroup = document.createElement('optgroup');
optgroup.label = `🔗 自定义域名 (${availableServices.custom_domain.count} 个服务)`; optgroup.label = `🔗 自定义域名 (${availableServices.moe_mail.count} 个服务)`;
availableServices.custom_domain.services.forEach(service => { availableServices.moe_mail.services.forEach(service => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = `custom_domain:${service.id || 'default'}`; option.value = `moe_mail:${service.id || 'default'}`;
option.textContent = service.name + (service.default_domain ? ` (@${service.default_domain})` : ''); option.textContent = service.name + (service.default_domain ? ` (@${service.default_domain})` : '');
option.dataset.type = 'custom_domain'; option.dataset.type = 'moe_mail';
if (service.id) { if (service.id) {
option.dataset.serviceId = service.id; option.dataset.serviceId = service.id;
} }
@@ -402,8 +402,8 @@ function handleServiceChange(e) {
if (service) { if (service) {
addLog('info', `[系统] 已选择 Outlook 账户: ${service.name}`); addLog('info', `[系统] 已选择 Outlook 账户: ${service.name}`);
} }
} else if (type === 'custom_domain') { } else if (type === 'moe_mail') {
const service = availableServices.custom_domain.services.find(s => s.id == id); const service = availableServices.moe_mail.services.find(s => s.id == id);
if (service) { if (service) {
addLog('info', `[系统] 已选择自定义域名服务: ${service.name}`); addLog('info', `[系统] 已选择自定义域名服务: ${service.name}`);
} }

View File

@@ -4,7 +4,7 @@
// 状态 // 状态
let outlookServices = []; let outlookServices = [];
let customServices = []; // 合并 custom_domain + temp_mail + duck_mail let customServices = []; // 合并 moe_mail + temp_mail + duck_mail
let selectedOutlook = new Set(); let selectedOutlook = new Set();
let selectedCustom = new Set(); let selectedCustom = new Set();
@@ -293,11 +293,11 @@ function getCustomServiceAddress(service) {
return `${escapeHtml(baseUrl)}<div style="color: var(--text-muted); margin-top: 4px;">默认域名:@${escapeHtml(domain)}</div>`; return `${escapeHtml(baseUrl)}<div style="color: var(--text-muted); margin-top: 4px;">默认域名:@${escapeHtml(domain)}</div>`;
} }
// 加载自定义邮箱服务(custom_domain + temp_mail + duck_mail + freemail 合并) // 加载自定义邮箱服务(moe_mail + temp_mail + duck_mail + freemail 合并)
async function loadCustomServices() { async function loadCustomServices() {
try { try {
const [r1, r2, r3, r4] = await Promise.all([ const [r1, r2, r3, r4] = await Promise.all([
api.get('/email-services?service_type=custom_domain'), api.get('/email-services?service_type=moe_mail'),
api.get('/email-services?service_type=temp_mail'), api.get('/email-services?service_type=temp_mail'),
api.get('/email-services?service_type=duck_mail'), api.get('/email-services?service_type=duck_mail'),
api.get('/email-services?service_type=freemail') api.get('/email-services?service_type=freemail')
@@ -422,7 +422,7 @@ async function handleAddCustom(e) {
let serviceType, config; let serviceType, config;
if (subType === 'moemail') { if (subType === 'moemail') {
serviceType = 'custom_domain'; serviceType = 'moe_mail';
config = { config = {
base_url: formData.get('api_url'), base_url: formData.get('api_url'),
api_key: formData.get('api_key'), api_key: formData.get('api_key'),

View File

@@ -351,7 +351,7 @@ const statusMap = {
service: { service: {
tempmail: 'Tempmail.lol', tempmail: 'Tempmail.lol',
outlook: 'Outlook', outlook: 'Outlook',
custom_domain: '自定义域名', moe_mail: 'MoeMail',
temp_mail: 'Temp-Mail自部署', temp_mail: 'Temp-Mail自部署',
duck_mail: 'DuckMail', duck_mail: 'DuckMail',
freemail: 'Freemail' freemail: 'Freemail'

View File

@@ -109,7 +109,7 @@
<option value="">全部邮箱服务</option> <option value="">全部邮箱服务</option>
<option value="tempmail">Tempmail</option> <option value="tempmail">Tempmail</option>
<option value="outlook">Outlook</option> <option value="outlook">Outlook</option>
<option value="custom_domain">自定义域名</option> <option value="moe_mail">MoeMail</option>
</select> </select>
<input type="text" id="search-input" class="form-input" placeholder="🔍 搜索邮箱..." style="min-width: 200px;"> <input type="text" id="search-input" class="form-input" placeholder="🔍 搜索邮箱..." style="min-width: 200px;">

View File

@@ -178,7 +178,7 @@
<select id="email-service" name="email_service" required> <select id="email-service" name="email_service" required>
<option value="tempmail:default">Tempmail.lol (临时邮箱)</option> <option value="tempmail:default">Tempmail.lol (临时邮箱)</option>
<option value="outlook">Outlook</option> <option value="outlook">Outlook</option>
<option value="custom_domain">自定义域名</option> <option value="moe_mail">MoeMail</option>
<option value="temp_mail">Temp-Mail 自部署</option> <option value="temp_mail">Temp-Mail 自部署</option>
<option value="duck_mail">DuckMail</option> <option value="duck_mail">DuckMail</option>
<option value="outlook_batch:all">Outlook 批量注册</option> <option value="outlook_batch:all">Outlook 批量注册</option>