mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-09-05 15:36:52 +08:00
feat(services): 新增标准 IMAP 邮箱服务支持(Gmail/QQ/163等)
- 新增 EmailServiceType.IMAP_MAIL 枚举值和默认配置 - 新建 ImapMailService(imaplib 标准库,强制直连) - 注册路由新增 imap_mail 分支和 available-services 键 - 邮箱服务路由新增 imap_mail stats 统计和类型描述 - accounts 路由 _build_inbox_config 支持 imap_mail - 前端表单/列表/编辑完整支持 IMAP 子类型 - 无新增依赖
This commit is contained in:
@@ -37,6 +37,7 @@ class EmailServiceType(str, Enum):
|
|||||||
TEMP_MAIL = "temp_mail"
|
TEMP_MAIL = "temp_mail"
|
||||||
DUCK_MAIL = "duck_mail"
|
DUCK_MAIL = "duck_mail"
|
||||||
FREEMAIL = "freemail"
|
FREEMAIL = "freemail"
|
||||||
|
IMAP_MAIL = "imap_mail"
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -128,6 +129,15 @@ EMAIL_SERVICE_DEFAULTS = {
|
|||||||
"domain": "",
|
"domain": "",
|
||||||
"timeout": 30,
|
"timeout": 30,
|
||||||
"max_retries": 3,
|
"max_retries": 3,
|
||||||
|
},
|
||||||
|
"imap_mail": {
|
||||||
|
"host": "",
|
||||||
|
"port": 993,
|
||||||
|
"use_ssl": True,
|
||||||
|
"email": "",
|
||||||
|
"password": "",
|
||||||
|
"timeout": 30,
|
||||||
|
"max_retries": 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from .moe_mail import MeoMailEmailService
|
|||||||
from .temp_mail import TempMailService
|
from .temp_mail import TempMailService
|
||||||
from .duck_mail import DuckMailService
|
from .duck_mail import DuckMailService
|
||||||
from .freemail import FreemailService
|
from .freemail import FreemailService
|
||||||
|
from .imap_mail import ImapMailService
|
||||||
|
|
||||||
# 注册服务
|
# 注册服务
|
||||||
EmailServiceFactory.register(EmailServiceType.TEMPMAIL, TempmailService)
|
EmailServiceFactory.register(EmailServiceType.TEMPMAIL, TempmailService)
|
||||||
@@ -24,6 +25,7 @@ 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)
|
||||||
|
EmailServiceFactory.register(EmailServiceType.IMAP_MAIL, ImapMailService)
|
||||||
|
|
||||||
# 导出 Outlook 模块的额外内容
|
# 导出 Outlook 模块的额外内容
|
||||||
from .outlook.base import (
|
from .outlook.base import (
|
||||||
@@ -56,6 +58,7 @@ __all__ = [
|
|||||||
'TempMailService',
|
'TempMailService',
|
||||||
'DuckMailService',
|
'DuckMailService',
|
||||||
'FreemailService',
|
'FreemailService',
|
||||||
|
'ImapMailService',
|
||||||
# Outlook 模块
|
# Outlook 模块
|
||||||
'ProviderType',
|
'ProviderType',
|
||||||
'EmailMessage',
|
'EmailMessage',
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
"""
|
||||||
|
IMAP 邮箱服务
|
||||||
|
支持 Gmail / QQ / 163 / Yahoo / Outlook 等标准 IMAP 协议邮件服务商。
|
||||||
|
仅用于接收验证码,强制直连(imaplib 不支持代理)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import imaplib
|
||||||
|
import email
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from email.header import decode_header
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from .base import BaseEmailService, EmailServiceError
|
||||||
|
from ..config.constants import (
|
||||||
|
EmailServiceType,
|
||||||
|
OPENAI_EMAIL_SENDERS,
|
||||||
|
OTP_CODE_SEMANTIC_PATTERN,
|
||||||
|
OTP_CODE_PATTERN,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ImapMailService(BaseEmailService):
|
||||||
|
"""标准 IMAP 邮箱服务(仅接收验证码,强制直连)"""
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any] = None, name: str = None):
|
||||||
|
super().__init__(EmailServiceType.IMAP_MAIL, name)
|
||||||
|
|
||||||
|
cfg = config or {}
|
||||||
|
required_keys = ["host", "email", "password"]
|
||||||
|
missing_keys = [k for k in required_keys if not cfg.get(k)]
|
||||||
|
if missing_keys:
|
||||||
|
raise ValueError(f"缺少必需配置: {missing_keys}")
|
||||||
|
|
||||||
|
self.host: str = str(cfg["host"]).strip()
|
||||||
|
self.port: int = int(cfg.get("port", 993))
|
||||||
|
self.use_ssl: bool = bool(cfg.get("use_ssl", True))
|
||||||
|
self.email_addr: str = str(cfg["email"]).strip()
|
||||||
|
self.password: str = str(cfg["password"])
|
||||||
|
self.timeout: int = int(cfg.get("timeout", 30))
|
||||||
|
self.max_retries: int = int(cfg.get("max_retries", 3))
|
||||||
|
|
||||||
|
def _connect(self) -> imaplib.IMAP4:
|
||||||
|
"""建立 IMAP 连接并登录,返回 mail 对象"""
|
||||||
|
if self.use_ssl:
|
||||||
|
mail = imaplib.IMAP4_SSL(self.host, self.port)
|
||||||
|
else:
|
||||||
|
mail = imaplib.IMAP4(self.host, self.port)
|
||||||
|
mail.starttls()
|
||||||
|
mail.login(self.email_addr, self.password)
|
||||||
|
return mail
|
||||||
|
|
||||||
|
def _decode_str(self, value) -> str:
|
||||||
|
"""解码邮件头部字段"""
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
parts = decode_header(value)
|
||||||
|
decoded = []
|
||||||
|
for part, charset in parts:
|
||||||
|
if isinstance(part, bytes):
|
||||||
|
decoded.append(part.decode(charset or "utf-8", errors="replace"))
|
||||||
|
else:
|
||||||
|
decoded.append(str(part))
|
||||||
|
return " ".join(decoded)
|
||||||
|
|
||||||
|
def _get_text_body(self, msg) -> str:
|
||||||
|
"""提取邮件纯文本内容"""
|
||||||
|
body = ""
|
||||||
|
if msg.is_multipart():
|
||||||
|
for part in msg.walk():
|
||||||
|
if part.get_content_type() == "text/plain":
|
||||||
|
charset = part.get_content_charset() or "utf-8"
|
||||||
|
payload = part.get_payload(decode=True)
|
||||||
|
if payload:
|
||||||
|
body += payload.decode(charset, errors="replace")
|
||||||
|
else:
|
||||||
|
charset = msg.get_content_charset() or "utf-8"
|
||||||
|
payload = msg.get_payload(decode=True)
|
||||||
|
if payload:
|
||||||
|
body = payload.decode(charset, errors="replace")
|
||||||
|
return body
|
||||||
|
|
||||||
|
def _is_openai_sender(self, from_addr: str) -> bool:
|
||||||
|
"""判断发件人是否为 OpenAI"""
|
||||||
|
from_lower = from_addr.lower()
|
||||||
|
for sender in OPENAI_EMAIL_SENDERS:
|
||||||
|
if sender.startswith("@") or sender.startswith("."):
|
||||||
|
if sender in from_lower:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
if sender in from_lower:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _extract_otp(self, text: str) -> Optional[str]:
|
||||||
|
"""从文本中提取 6 位验证码,优先语义匹配,回退简单匹配"""
|
||||||
|
match = re.search(OTP_CODE_SEMANTIC_PATTERN, text, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
match = re.search(OTP_CODE_PATTERN, text)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_email(self, config: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||||
|
"""IMAP 模式不创建新邮箱,直接返回配置中的固定地址"""
|
||||||
|
self.update_status(True)
|
||||||
|
return {
|
||||||
|
"email": self.email_addr,
|
||||||
|
"service_id": self.email_addr,
|
||||||
|
"id": self.email_addr,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_verification_code(
|
||||||
|
self,
|
||||||
|
email: str,
|
||||||
|
email_id: str = None,
|
||||||
|
timeout: int = 60,
|
||||||
|
pattern: str = None,
|
||||||
|
otp_sent_at: Optional[float] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""轮询 IMAP 收件箱,获取 OpenAI 验证码"""
|
||||||
|
start_time = time.time()
|
||||||
|
seen_ids: set = set()
|
||||||
|
mail = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
mail = self._connect()
|
||||||
|
mail.select("INBOX")
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
try:
|
||||||
|
# 搜索所有未读邮件
|
||||||
|
status, data = mail.search(None, "UNSEEN")
|
||||||
|
if status != "OK" or not data or not data[0]:
|
||||||
|
time.sleep(3)
|
||||||
|
continue
|
||||||
|
|
||||||
|
msg_ids = data[0].split()
|
||||||
|
for msg_id in reversed(msg_ids): # 最新的优先
|
||||||
|
id_str = msg_id.decode()
|
||||||
|
if id_str in seen_ids:
|
||||||
|
continue
|
||||||
|
seen_ids.add(id_str)
|
||||||
|
|
||||||
|
# 获取邮件
|
||||||
|
status, msg_data = mail.fetch(msg_id, "(RFC822)")
|
||||||
|
if status != "OK" or not msg_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
raw = msg_data[0][1]
|
||||||
|
msg = email.message_from_bytes(raw)
|
||||||
|
|
||||||
|
# 检查发件人
|
||||||
|
from_addr = self._decode_str(msg.get("From", ""))
|
||||||
|
if not self._is_openai_sender(from_addr):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 提取验证码
|
||||||
|
body = self._get_text_body(msg)
|
||||||
|
code = self._extract_otp(body)
|
||||||
|
if code:
|
||||||
|
# 标记已读
|
||||||
|
mail.store(msg_id, "+FLAGS", "\\Seen")
|
||||||
|
self.update_status(True)
|
||||||
|
logger.info(f"IMAP 获取验证码成功: {code}")
|
||||||
|
return code
|
||||||
|
|
||||||
|
except imaplib.IMAP4.error as e:
|
||||||
|
logger.debug(f"IMAP 搜索邮件失败: {e}")
|
||||||
|
# 尝试重新连接
|
||||||
|
try:
|
||||||
|
mail.select("INBOX")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"IMAP 连接/轮询失败: {e}")
|
||||||
|
self.update_status(False, str(e))
|
||||||
|
finally:
|
||||||
|
if mail:
|
||||||
|
try:
|
||||||
|
mail.logout()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_health(self) -> bool:
|
||||||
|
"""尝试 IMAP 登录并选择收件箱"""
|
||||||
|
mail = None
|
||||||
|
try:
|
||||||
|
mail = self._connect()
|
||||||
|
status, _ = mail.select("INBOX")
|
||||||
|
return status == "OK"
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"IMAP 健康检查失败: {e}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
if mail:
|
||||||
|
try:
|
||||||
|
mail.logout()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def list_emails(self, **kwargs) -> list:
|
||||||
|
"""IMAP 单账号模式,返回固定地址"""
|
||||||
|
return [{"email": self.email_addr, "id": self.email_addr}]
|
||||||
|
|
||||||
|
def delete_email(self, email_id: str) -> bool:
|
||||||
|
"""IMAP 模式无需删除逻辑"""
|
||||||
|
return True
|
||||||
@@ -1002,6 +1002,7 @@ def _build_inbox_config(db, service_type, email: str) -> dict:
|
|||||||
EST.TEMP_MAIL: "temp_mail",
|
EST.TEMP_MAIL: "temp_mail",
|
||||||
EST.DUCK_MAIL: "duck_mail",
|
EST.DUCK_MAIL: "duck_mail",
|
||||||
EST.FREEMAIL: "freemail",
|
EST.FREEMAIL: "freemail",
|
||||||
|
EST.IMAP_MAIL: "imap_mail",
|
||||||
EST.OUTLOOK: "outlook",
|
EST.OUTLOOK: "outlook",
|
||||||
}
|
}
|
||||||
db_type = type_map.get(service_type)
|
db_type = type_map.get(service_type)
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ async def get_email_services_stats():
|
|||||||
'temp_mail_count': 0,
|
'temp_mail_count': 0,
|
||||||
'duck_mail_count': 0,
|
'duck_mail_count': 0,
|
||||||
'freemail_count': 0,
|
'freemail_count': 0,
|
||||||
|
'imap_mail_count': 0,
|
||||||
'tempmail_available': True, # 临时邮箱始终可用
|
'tempmail_available': True, # 临时邮箱始终可用
|
||||||
'enabled_count': enabled_count
|
'enabled_count': enabled_count
|
||||||
}
|
}
|
||||||
@@ -161,6 +162,8 @@ async def get_email_services_stats():
|
|||||||
stats['duck_mail_count'] = count
|
stats['duck_mail_count'] = count
|
||||||
elif service_type == 'freemail':
|
elif service_type == 'freemail':
|
||||||
stats['freemail_count'] = count
|
stats['freemail_count'] = count
|
||||||
|
elif service_type == 'imap_mail':
|
||||||
|
stats['imap_mail_count'] = count
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
@@ -231,6 +234,18 @@ async def get_service_types():
|
|||||||
{"name": "admin_token", "label": "Admin Token", "required": True, "secret": True},
|
{"name": "admin_token", "label": "Admin Token", "required": True, "secret": True},
|
||||||
{"name": "domain", "label": "邮箱域名", "required": False, "placeholder": "example.com"},
|
{"name": "domain", "label": "邮箱域名", "required": False, "placeholder": "example.com"},
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "imap_mail",
|
||||||
|
"label": "IMAP 邮箱",
|
||||||
|
"description": "标准 IMAP 协议邮箱(Gmail/QQ/163等),仅用于接收验证码,强制直连",
|
||||||
|
"config_fields": [
|
||||||
|
{"name": "host", "label": "IMAP 服务器", "required": True, "placeholder": "imap.gmail.com"},
|
||||||
|
{"name": "port", "label": "端口", "required": False, "default": 993},
|
||||||
|
{"name": "use_ssl", "label": "使用 SSL", "required": False, "default": True},
|
||||||
|
{"name": "email", "label": "邮箱地址", "required": True},
|
||||||
|
{"name": "password", "label": "密码/授权码", "required": True, "secret": True},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -372,6 +372,20 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy:
|
|||||||
logger.info(f"使用数据库 Freemail 服务: {db_service.name}")
|
logger.info(f"使用数据库 Freemail 服务: {db_service.name}")
|
||||||
else:
|
else:
|
||||||
raise ValueError("没有可用的 Freemail 邮箱服务,请先在邮箱服务页面添加服务")
|
raise ValueError("没有可用的 Freemail 邮箱服务,请先在邮箱服务页面添加服务")
|
||||||
|
elif service_type == EmailServiceType.IMAP_MAIL:
|
||||||
|
from ...database.models import EmailService as EmailServiceModel
|
||||||
|
|
||||||
|
db_service = db.query(EmailServiceModel).filter(
|
||||||
|
EmailServiceModel.service_type == "imap_mail",
|
||||||
|
EmailServiceModel.enabled == True
|
||||||
|
).order_by(EmailServiceModel.priority.asc()).first()
|
||||||
|
|
||||||
|
if db_service and db_service.config:
|
||||||
|
config = _normalize_email_service_config(service_type, db_service.config, actual_proxy_url)
|
||||||
|
crud.update_registration_task(db, task_uuid, email_service_id=db_service.id)
|
||||||
|
logger.info(f"使用数据库 IMAP 邮箱服务: {db_service.name}")
|
||||||
|
else:
|
||||||
|
raise ValueError("没有可用的 IMAP 邮箱服务,请先在邮箱服务中添加")
|
||||||
else:
|
else:
|
||||||
config = email_service_config or {}
|
config = email_service_config or {}
|
||||||
|
|
||||||
@@ -1110,6 +1124,11 @@ async def get_available_email_services():
|
|||||||
"available": False,
|
"available": False,
|
||||||
"count": 0,
|
"count": 0,
|
||||||
"services": []
|
"services": []
|
||||||
|
},
|
||||||
|
"imap_mail": {
|
||||||
|
"available": False,
|
||||||
|
"count": 0,
|
||||||
|
"services": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1219,6 +1238,25 @@ async def get_available_email_services():
|
|||||||
result["freemail"]["count"] = len(freemail_services)
|
result["freemail"]["count"] = len(freemail_services)
|
||||||
result["freemail"]["available"] = len(freemail_services) > 0
|
result["freemail"]["available"] = len(freemail_services) > 0
|
||||||
|
|
||||||
|
imap_mail_services = db.query(EmailServiceModel).filter(
|
||||||
|
EmailServiceModel.service_type == "imap_mail",
|
||||||
|
EmailServiceModel.enabled == True
|
||||||
|
).order_by(EmailServiceModel.priority.asc()).all()
|
||||||
|
|
||||||
|
for service in imap_mail_services:
|
||||||
|
config = service.config or {}
|
||||||
|
result["imap_mail"]["services"].append({
|
||||||
|
"id": service.id,
|
||||||
|
"name": service.name,
|
||||||
|
"type": "imap_mail",
|
||||||
|
"email": config.get("email"),
|
||||||
|
"host": config.get("host"),
|
||||||
|
"priority": service.priority
|
||||||
|
})
|
||||||
|
|
||||||
|
result["imap_mail"]["count"] = len(imap_mail_services)
|
||||||
|
result["imap_mail"]["available"] = len(imap_mail_services) > 0
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+53
-11
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
let outlookServices = [];
|
let outlookServices = [];
|
||||||
let customServices = []; // 合并 moe_mail + temp_mail + duck_mail
|
let customServices = []; // 合并 moe_mail + temp_mail + duck_mail + freemail + imap_mail
|
||||||
let selectedOutlook = new Set();
|
let selectedOutlook = new Set();
|
||||||
let selectedCustom = new Set();
|
let selectedCustom = new Set();
|
||||||
|
|
||||||
@@ -52,6 +52,7 @@ const elements = {
|
|||||||
addTempmailFields: document.getElementById('add-tempmail-fields'),
|
addTempmailFields: document.getElementById('add-tempmail-fields'),
|
||||||
addDuckmailFields: document.getElementById('add-duckmail-fields'),
|
addDuckmailFields: document.getElementById('add-duckmail-fields'),
|
||||||
addFreemailFields: document.getElementById('add-freemail-fields'),
|
addFreemailFields: document.getElementById('add-freemail-fields'),
|
||||||
|
addImapFields: document.getElementById('add-imap-fields'),
|
||||||
|
|
||||||
// 编辑自定义域名模态框
|
// 编辑自定义域名模态框
|
||||||
editCustomModal: document.getElementById('edit-custom-modal'),
|
editCustomModal: document.getElementById('edit-custom-modal'),
|
||||||
@@ -62,6 +63,7 @@ const elements = {
|
|||||||
editTempmailFields: document.getElementById('edit-tempmail-fields'),
|
editTempmailFields: document.getElementById('edit-tempmail-fields'),
|
||||||
editDuckmailFields: document.getElementById('edit-duckmail-fields'),
|
editDuckmailFields: document.getElementById('edit-duckmail-fields'),
|
||||||
editFreemailFields: document.getElementById('edit-freemail-fields'),
|
editFreemailFields: document.getElementById('edit-freemail-fields'),
|
||||||
|
editImapFields: document.getElementById('edit-imap-fields'),
|
||||||
editCustomTypeBadge: document.getElementById('edit-custom-type-badge'),
|
editCustomTypeBadge: document.getElementById('edit-custom-type-badge'),
|
||||||
editCustomSubTypeHidden: document.getElementById('edit-custom-sub-type-hidden'),
|
editCustomSubTypeHidden: document.getElementById('edit-custom-sub-type-hidden'),
|
||||||
|
|
||||||
@@ -76,7 +78,8 @@ const CUSTOM_SUBTYPE_LABELS = {
|
|||||||
moemail: '🔗 MoeMail(自定义域名 API)',
|
moemail: '🔗 MoeMail(自定义域名 API)',
|
||||||
tempmail: '📮 TempMail(自部署 Cloudflare Worker)',
|
tempmail: '📮 TempMail(自部署 Cloudflare Worker)',
|
||||||
duckmail: '🦆 DuckMail(DuckMail API)',
|
duckmail: '🦆 DuckMail(DuckMail API)',
|
||||||
freemail: 'Freemail(自部署 Cloudflare Worker)'
|
freemail: 'Freemail(自部署 Cloudflare Worker)',
|
||||||
|
imap: '📧 IMAP 邮箱(Gmail/QQ/163等)'
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
@@ -182,6 +185,7 @@ function switchAddSubType(subType) {
|
|||||||
elements.addTempmailFields.style.display = subType === 'tempmail' ? '' : 'none';
|
elements.addTempmailFields.style.display = subType === 'tempmail' ? '' : 'none';
|
||||||
elements.addDuckmailFields.style.display = subType === 'duckmail' ? '' : 'none';
|
elements.addDuckmailFields.style.display = subType === 'duckmail' ? '' : 'none';
|
||||||
elements.addFreemailFields.style.display = subType === 'freemail' ? '' : 'none';
|
elements.addFreemailFields.style.display = subType === 'freemail' ? '' : 'none';
|
||||||
|
elements.addImapFields.style.display = subType === 'imap' ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换编辑表单子类型显示
|
// 切换编辑表单子类型显示
|
||||||
@@ -191,6 +195,7 @@ function switchEditSubType(subType) {
|
|||||||
elements.editTempmailFields.style.display = subType === 'tempmail' ? '' : 'none';
|
elements.editTempmailFields.style.display = subType === 'tempmail' ? '' : 'none';
|
||||||
elements.editDuckmailFields.style.display = subType === 'duckmail' ? '' : 'none';
|
elements.editDuckmailFields.style.display = subType === 'duckmail' ? '' : 'none';
|
||||||
elements.editFreemailFields.style.display = subType === 'freemail' ? '' : 'none';
|
elements.editFreemailFields.style.display = subType === 'freemail' ? '' : 'none';
|
||||||
|
elements.editImapFields.style.display = subType === 'imap' ? '' : 'none';
|
||||||
elements.editCustomTypeBadge.textContent = CUSTOM_SUBTYPE_LABELS[subType] || CUSTOM_SUBTYPE_LABELS.moemail;
|
elements.editCustomTypeBadge.textContent = CUSTOM_SUBTYPE_LABELS[subType] || CUSTOM_SUBTYPE_LABELS.moemail;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +204,7 @@ async function loadStats() {
|
|||||||
try {
|
try {
|
||||||
const data = await api.get('/email-services/stats');
|
const data = await api.get('/email-services/stats');
|
||||||
elements.outlookCount.textContent = data.outlook_count || 0;
|
elements.outlookCount.textContent = data.outlook_count || 0;
|
||||||
elements.customCount.textContent = (data.custom_count || 0) + (data.temp_mail_count || 0) + (data.duck_mail_count || 0) + (data.freemail_count || 0);
|
elements.customCount.textContent = (data.custom_count || 0) + (data.temp_mail_count || 0) + (data.duck_mail_count || 0) + (data.freemail_count || 0) + (data.imap_mail_count || 0);
|
||||||
elements.tempmailStatus.textContent = data.tempmail_available ? '可用' : '不可用';
|
elements.tempmailStatus.textContent = data.tempmail_available ? '可用' : '不可用';
|
||||||
elements.totalEnabled.textContent = data.enabled_count || 0;
|
elements.totalEnabled.textContent = data.enabled_count || 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -281,10 +286,18 @@ function getCustomServiceTypeBadge(subType) {
|
|||||||
if (subType === 'duckmail') {
|
if (subType === 'duckmail') {
|
||||||
return '<span class="status-badge success">DuckMail</span>';
|
return '<span class="status-badge success">DuckMail</span>';
|
||||||
}
|
}
|
||||||
return '<span class="status-badge" style="background-color:#9c27b0;color:white;">Freemail</span>';
|
if (subType === 'freemail') {
|
||||||
|
return '<span class="status-badge" style="background-color:#9c27b0;color:white;">Freemail</span>';
|
||||||
|
}
|
||||||
|
return '<span class="status-badge" style="background-color:#0288d1;color:white;">IMAP</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCustomServiceAddress(service) {
|
function getCustomServiceAddress(service) {
|
||||||
|
if (service._subType === 'imap') {
|
||||||
|
const host = service.config?.host || '-';
|
||||||
|
const emailAddr = service.config?.email || '';
|
||||||
|
return `${escapeHtml(host)}<div style="color: var(--text-muted); margin-top: 4px;">${escapeHtml(emailAddr)}</div>`;
|
||||||
|
}
|
||||||
const baseUrl = service.config?.base_url || '-';
|
const baseUrl = service.config?.base_url || '-';
|
||||||
const domain = service.config?.default_domain || service.config?.domain;
|
const domain = service.config?.default_domain || service.config?.domain;
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
@@ -296,17 +309,19 @@ function getCustomServiceAddress(service) {
|
|||||||
// 加载自定义邮箱服务(moe_mail + 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, r5] = await Promise.all([
|
||||||
api.get('/email-services?service_type=moe_mail'),
|
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'),
|
||||||
|
api.get('/email-services?service_type=imap_mail')
|
||||||
]);
|
]);
|
||||||
customServices = [
|
customServices = [
|
||||||
...(r1.services || []).map(s => ({ ...s, _subType: 'moemail' })),
|
...(r1.services || []).map(s => ({ ...s, _subType: 'moemail' })),
|
||||||
...(r2.services || []).map(s => ({ ...s, _subType: 'tempmail' })),
|
...(r2.services || []).map(s => ({ ...s, _subType: 'tempmail' })),
|
||||||
...(r3.services || []).map(s => ({ ...s, _subType: 'duckmail' })),
|
...(r3.services || []).map(s => ({ ...s, _subType: 'duckmail' })),
|
||||||
...(r4.services || []).map(s => ({ ...s, _subType: 'freemail' }))
|
...(r4.services || []).map(s => ({ ...s, _subType: 'freemail' })),
|
||||||
|
...(r5.services || []).map(s => ({ ...s, _subType: 'imap' }))
|
||||||
];
|
];
|
||||||
|
|
||||||
if (customServices.length === 0) {
|
if (customServices.length === 0) {
|
||||||
@@ -444,13 +459,22 @@ async function handleAddCustom(e) {
|
|||||||
default_domain: formData.get('dm_domain'),
|
default_domain: formData.get('dm_domain'),
|
||||||
password_length: parseInt(formData.get('dm_password_length'), 10) || 12
|
password_length: parseInt(formData.get('dm_password_length'), 10) || 12
|
||||||
};
|
};
|
||||||
} else {
|
} else if (subType === 'freemail') {
|
||||||
serviceType = 'freemail';
|
serviceType = 'freemail';
|
||||||
config = {
|
config = {
|
||||||
base_url: formData.get('fm_base_url'),
|
base_url: formData.get('fm_base_url'),
|
||||||
admin_token: formData.get('fm_admin_token'),
|
admin_token: formData.get('fm_admin_token'),
|
||||||
domain: formData.get('fm_domain')
|
domain: formData.get('fm_domain')
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
serviceType = 'imap_mail';
|
||||||
|
config = {
|
||||||
|
host: formData.get('imap_host'),
|
||||||
|
port: parseInt(formData.get('imap_port'), 10) || 993,
|
||||||
|
use_ssl: formData.get('imap_use_ssl') !== 'false',
|
||||||
|
email: formData.get('imap_email'),
|
||||||
|
password: formData.get('imap_password')
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
@@ -593,7 +617,9 @@ async function editCustomService(id, subType) {
|
|||||||
? 'duckmail'
|
? 'duckmail'
|
||||||
: service.service_type === 'freemail'
|
: service.service_type === 'freemail'
|
||||||
? 'freemail'
|
? 'freemail'
|
||||||
: 'moemail'
|
: service.service_type === 'imap_mail'
|
||||||
|
? 'imap'
|
||||||
|
: 'moemail'
|
||||||
);
|
);
|
||||||
|
|
||||||
document.getElementById('edit-custom-id').value = service.id;
|
document.getElementById('edit-custom-id').value = service.id;
|
||||||
@@ -619,11 +645,18 @@ async function editCustomService(id, subType) {
|
|||||||
document.getElementById('edit-dm-api-key').placeholder = service.config?.api_key ? '已设置,留空保持不变' : '请输入 API Key(可选)';
|
document.getElementById('edit-dm-api-key').placeholder = service.config?.api_key ? '已设置,留空保持不变' : '请输入 API Key(可选)';
|
||||||
document.getElementById('edit-dm-domain').value = service.config?.default_domain || '';
|
document.getElementById('edit-dm-domain').value = service.config?.default_domain || '';
|
||||||
document.getElementById('edit-dm-password-length').value = service.config?.password_length || 12;
|
document.getElementById('edit-dm-password-length').value = service.config?.password_length || 12;
|
||||||
} else {
|
} else if (resolvedSubType === 'freemail') {
|
||||||
document.getElementById('edit-fm-base-url').value = service.config?.base_url || '';
|
document.getElementById('edit-fm-base-url').value = service.config?.base_url || '';
|
||||||
document.getElementById('edit-fm-admin-token').value = '';
|
document.getElementById('edit-fm-admin-token').value = '';
|
||||||
document.getElementById('edit-fm-admin-token').placeholder = service.config?.admin_token ? '已设置,留空保持不变' : '请输入 Admin Token';
|
document.getElementById('edit-fm-admin-token').placeholder = service.config?.admin_token ? '已设置,留空保持不变' : '请输入 Admin Token';
|
||||||
document.getElementById('edit-fm-domain').value = service.config?.domain || '';
|
document.getElementById('edit-fm-domain').value = service.config?.domain || '';
|
||||||
|
} else {
|
||||||
|
document.getElementById('edit-imap-host').value = service.config?.host || '';
|
||||||
|
document.getElementById('edit-imap-port').value = service.config?.port || 993;
|
||||||
|
document.getElementById('edit-imap-use-ssl').value = service.config?.use_ssl !== false ? 'true' : 'false';
|
||||||
|
document.getElementById('edit-imap-email').value = service.config?.email || '';
|
||||||
|
document.getElementById('edit-imap-password').value = '';
|
||||||
|
document.getElementById('edit-imap-password').placeholder = service.config?.password ? '已设置,留空保持不变' : '请输入密码/授权码';
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.editCustomModal.classList.add('active');
|
elements.editCustomModal.classList.add('active');
|
||||||
@@ -663,13 +696,22 @@ async function handleEditCustom(e) {
|
|||||||
};
|
};
|
||||||
const apiKey = formData.get('dm_api_key');
|
const apiKey = formData.get('dm_api_key');
|
||||||
if (apiKey && apiKey.trim()) config.api_key = apiKey.trim();
|
if (apiKey && apiKey.trim()) config.api_key = apiKey.trim();
|
||||||
} else {
|
} else if (subType === 'freemail') {
|
||||||
config = {
|
config = {
|
||||||
base_url: formData.get('fm_base_url'),
|
base_url: formData.get('fm_base_url'),
|
||||||
domain: formData.get('fm_domain')
|
domain: formData.get('fm_domain')
|
||||||
};
|
};
|
||||||
const token = formData.get('fm_admin_token');
|
const token = formData.get('fm_admin_token');
|
||||||
if (token && token.trim()) config.admin_token = token.trim();
|
if (token && token.trim()) config.admin_token = token.trim();
|
||||||
|
} else {
|
||||||
|
config = {
|
||||||
|
host: formData.get('imap_host'),
|
||||||
|
port: parseInt(formData.get('imap_port'), 10) || 993,
|
||||||
|
use_ssl: formData.get('imap_use_ssl') !== 'false',
|
||||||
|
email: formData.get('imap_email')
|
||||||
|
};
|
||||||
|
const pwd = formData.get('imap_password');
|
||||||
|
if (pwd && pwd.trim()) config.password = pwd.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
|
|||||||
+2
-1
@@ -354,7 +354,8 @@ const statusMap = {
|
|||||||
moe_mail: 'MoeMail',
|
moe_mail: 'MoeMail',
|
||||||
temp_mail: 'Temp-Mail(自部署)',
|
temp_mail: 'Temp-Mail(自部署)',
|
||||||
duck_mail: 'DuckMail',
|
duck_mail: 'DuckMail',
|
||||||
freemail: 'Freemail'
|
freemail: 'Freemail',
|
||||||
|
imap_mail: 'IMAP 邮箱'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,7 @@
|
|||||||
<option value="tempmail">TempMail(自部署 Cloudflare Worker)</option>
|
<option value="tempmail">TempMail(自部署 Cloudflare Worker)</option>
|
||||||
<option value="duckmail">DuckMail(DuckMail API)</option>
|
<option value="duckmail">DuckMail(DuckMail API)</option>
|
||||||
<option value="freemail">Freemail(自部署 Cloudflare Worker)</option>
|
<option value="freemail">Freemail(自部署 Cloudflare Worker)</option>
|
||||||
|
<option value="imap">标准 IMAP(Gmail/QQ/163等)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<!-- MoeMail 字段 -->
|
<!-- MoeMail 字段 -->
|
||||||
@@ -277,6 +278,35 @@
|
|||||||
<input type="text" id="custom-fm-domain" name="fm_domain" placeholder="example.com">
|
<input type="text" id="custom-fm-domain" name="fm_domain" placeholder="example.com">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- IMAP 字段 -->
|
||||||
|
<div id="add-imap-fields" style="display:none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="custom-imap-host">IMAP 服务器</label>
|
||||||
|
<input type="text" id="custom-imap-host" name="imap_host" placeholder="imap.gmail.com / imap.qq.com / imap.163.com">
|
||||||
|
<small style="color: var(--text-muted);">Gmail: imap.gmail.com | QQ: imap.qq.com | 163: imap.163.com</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="custom-imap-port">端口</label>
|
||||||
|
<input type="number" id="custom-imap-port" name="imap_port" value="993" min="1" max="65535">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="custom-imap-use-ssl">加密方式</label>
|
||||||
|
<select id="custom-imap-use-ssl" name="imap_use_ssl">
|
||||||
|
<option value="true">SSL(端口 993)</option>
|
||||||
|
<option value="false">STARTTLS(端口 143)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="custom-imap-email">邮箱地址</label>
|
||||||
|
<input type="email" id="custom-imap-email" name="imap_email" placeholder="your@gmail.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="custom-imap-password">密码 / 授权码</label>
|
||||||
|
<input type="password" id="custom-imap-password" name="imap_password" placeholder="Gmail 需使用 App Password,QQ/163 需使用授权码">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="custom-priority">优先级</label>
|
<label for="custom-priority">优先级</label>
|
||||||
@@ -386,6 +416,35 @@
|
|||||||
<input type="text" id="edit-fm-domain" name="fm_domain" placeholder="example.com">
|
<input type="text" id="edit-fm-domain" name="fm_domain" placeholder="example.com">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- IMAP 字段 -->
|
||||||
|
<div id="edit-imap-fields" style="display:none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-imap-host">IMAP 服务器</label>
|
||||||
|
<input type="text" id="edit-imap-host" name="imap_host" placeholder="imap.gmail.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-imap-port">端口</label>
|
||||||
|
<input type="number" id="edit-imap-port" name="imap_port" value="993" min="1" max="65535">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-imap-use-ssl">加密方式</label>
|
||||||
|
<select id="edit-imap-use-ssl" name="imap_use_ssl">
|
||||||
|
<option value="true">SSL(端口 993)</option>
|
||||||
|
<option value="false">STARTTLS(端口 143)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-imap-email">邮箱地址</label>
|
||||||
|
<input type="email" id="edit-imap-email" name="imap_email" placeholder="your@gmail.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-imap-password">密码 / 授权码</label>
|
||||||
|
<input type="password" id="edit-imap-password" name="imap_password" placeholder="留空则保持原值不变">
|
||||||
|
<small style="color: var(--text-muted);">留空则保持原值不变</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-custom-priority">优先级</label>
|
<label for="edit-custom-priority">优先级</label>
|
||||||
|
|||||||
@@ -181,6 +181,7 @@
|
|||||||
<option value="moe_mail">MoeMail</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="imap_mail">IMAP 邮箱</option>
|
||||||
<option value="outlook_batch:all">Outlook 批量注册</option>
|
<option value="outlook_batch:all">Outlook 批量注册</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user