From ed7d4044bc24f28d7d283c5f306d8bc692b6f8b3 Mon Sep 17 00:00:00 2001 From: jjaw Date: Sun, 22 Mar 2026 02:18:41 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20CloudMail=20=E9=82=AE?= =?UTF-8?q?=E7=AE=B1=E6=9C=8D=E5=8A=A1=E5=AE=9E=E7=8E=B0=E5=8F=8A=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/constants.py | 11 ++ src/services/__init__.py | 3 + src/services/cloudmail.py | 361 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 375 insertions(+) create mode 100644 src/services/cloudmail.py diff --git a/src/config/constants.py b/src/config/constants.py index 9e787a2e..50135b20 100644 --- a/src/config/constants.py +++ b/src/config/constants.py @@ -37,6 +37,7 @@ class EmailServiceType(str, Enum): TEMP_MAIL = "temp_mail" DUCK_MAIL = "duck_mail" FREEMAIL = "freemail" + CLOUDMAIL = "cloudmail" IMAP_MAIL = "imap_mail" @@ -132,6 +133,16 @@ class EmailServiceType(str, Enum): "timeout": 30, "max_retries": 3, }, + "cloudmail": { + "base_url": "", + "login_email": "", + "login_password": "", + "default_domain": "", + "proxy_url": None, + "poll_interval": 3, + "timeout": 30, + "max_retries": 3, + }, "imap_mail": { "host": "", "port": 993, diff --git a/src/services/__init__.py b/src/services/__init__.py index ad29d3e5..c73366c3 100644 --- a/src/services/__init__.py +++ b/src/services/__init__.py @@ -16,6 +16,7 @@ from .temp_mail import TempMailService from .duck_mail import DuckMailService from .freemail import FreemailService +from .cloudmail import CloudMailService from .imap_mail import ImapMailService # 注册服务 @@ -25,6 +26,7 @@ EmailServiceFactory.register(EmailServiceType.TEMP_MAIL, TempMailService) EmailServiceFactory.register(EmailServiceType.DUCK_MAIL, DuckMailService) EmailServiceFactory.register(EmailServiceType.FREEMAIL, FreemailService) +EmailServiceFactory.register(EmailServiceType.CLOUDMAIL, CloudMailService) EmailServiceFactory.register(EmailServiceType.IMAP_MAIL, ImapMailService) # 导出 Outlook 模块的额外内容 @@ -58,6 +60,7 @@ 'TempMailService', 'DuckMailService', 'FreemailService', + 'CloudMailService', 'ImapMailService', # Outlook 模块 'ProviderType', diff --git a/src/services/cloudmail.py b/src/services/cloudmail.py new file mode 100644 index 00000000..fbc47aa5 --- /dev/null +++ b/src/services/cloudmail.py @@ -0,0 +1,361 @@ +""" +CloudMail 邮箱服务实现 +基于 CloudMail Web API +""" + +import logging +import random +import re +import string +import time +from datetime import datetime +from typing import Any, Dict, List, Optional + +from .base import BaseEmailService, EmailServiceError, EmailServiceType +from ..config.constants import OTP_CODE_PATTERN +from ..core.http_client import HTTPClient, RequestConfig + + +logger = logging.getLogger(__name__) + + +class CloudMailService(BaseEmailService): + """CloudMail 邮箱服务。""" + + def __init__(self, config: Dict[str, Any] = None, name: str = None): + super().__init__(EmailServiceType.CLOUDMAIL, name or "cloudmail_service") + + required_keys = ["base_url", "login_email", "login_password"] + missing_keys = [key for key in required_keys if not (config or {}).get(key)] + if missing_keys: + raise ValueError(f"缺少必需配置: {missing_keys}") + + default_config = { + "timeout": 30, + "max_retries": 3, + "proxy_url": None, + "default_domain": "", + "poll_interval": 3, + "login_email": "", + "login_password": "", + } + self.config = {**default_config, **(config or {})} + self.config["base_url"] = str(self.config["base_url"]).rstrip("/") + + http_config = RequestConfig( + timeout=self.config["timeout"], + max_retries=self.config["max_retries"], + ) + self.http_client = HTTPClient( + proxy_url=self.config.get("proxy_url"), + config=http_config, + ) + + self._domains: List[str] = [] + self._accounts_by_id: Dict[str, Dict[str, Any]] = {} + self._accounts_by_email: Dict[str, Dict[str, Any]] = {} + self._authorization_jwt: Optional[str] = None + + def _build_headers(self, authorization: Optional[str] = None) -> Dict[str, str]: + headers = { + "accept": "application/json, text/plain, */*", + "accept-language": "zh", + } + if authorization is not None: + headers["authorization"] = authorization + return headers + + def _make_request( + self, + method: str, + path: str, + authorization: Optional[str] = None, + **kwargs, + ) -> Any: + url = f"{self.config['base_url']}{path}" + headers = self._build_headers(authorization=authorization) + extra_headers = kwargs.pop("headers", None) or {} + headers.update(extra_headers) + + try: + response = self.http_client.request(method, url, headers=headers, **kwargs) + + if response.status_code >= 400: + message = f"请求失败: {response.status_code}" + try: + message = f"{message} - {response.text[:300]}" + except Exception: + pass + raise EmailServiceError(message) + + try: + data = response.json() + except Exception: + return {"raw_response": response.text} + + if isinstance(data, dict) and data.get("code") not in (None, 200): + raise EmailServiceError(f"接口返回异常: {data}") + + return data + except Exception as e: + self.update_status(False, e) + if isinstance(e, EmailServiceError): + raise + raise EmailServiceError(f"请求失败: {method} {path} - {e}") + + def _get_authorization_jwt(self, refresh: bool = False) -> str: + if not refresh and self._authorization_jwt: + return self._authorization_jwt + + token = self.login( + self.config["login_email"], + self.config["login_password"], + ) + self._authorization_jwt = token + return token + + def _cache_account(self, account_info: Dict[str, Any]) -> None: + account_id = str(account_info.get("accountId") or account_info.get("service_id") or "").strip() + email = str(account_info.get("email") or "").strip().lower() + + if account_id: + self._accounts_by_id[account_id] = account_info + if email: + self._accounts_by_email[email] = account_info + + def _get_account(self, email: Optional[str] = None, email_id: Optional[str] = None) -> Optional[Dict[str, Any]]: + if email_id: + cached = self._accounts_by_id.get(str(email_id)) + if cached: + return cached + if email: + cached = self._accounts_by_email.get(str(email).strip().lower()) + if cached: + return cached + return None + + def _parse_time(self, value: Any) -> Optional[float]: + if not value: + return None + if isinstance(value, (int, float)): + return float(value) + text = str(value).strip() + for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"): + try: + return datetime.strptime(text, fmt).timestamp() + except Exception: + continue + return None + + def _generate_local_part(self, length: int = 10) -> str: + first = random.choice(string.ascii_lowercase) + rest = "".join(random.choices(string.ascii_lowercase + string.digits, k=max(1, length - 1))) + return f"{first}{rest}" + + def login(self, email: str, password: str) -> str: + data = self._make_request( + "POST", + "/api/login", + json={"email": email, "password": password}, + headers={ + "content-type": "application/json", + "cache-control": "no-cache", + "pragma": "no-cache", + }, + ) + token = ((data or {}).get("data") or {}).get("token") + if not token: + raise EmailServiceError(f"登录返回异常: {data}") + return str(token) + + def get_domain_list(self) -> List[str]: + data = self._make_request( + "GET", + "/api/setting/websiteConfig", + authorization="null", + headers={ + "cache-control": "no-cache", + "pragma": "no-cache", + }, + ) + domains = ((data or {}).get("data") or {}).get("domainList") or [] + return [str(domain).strip() for domain in domains if str(domain).strip()] + + def _ensure_domains(self) -> None: + if not self._domains: + try: + self._domains = self.get_domain_list() + except Exception as e: + logger.warning(f"获取 CloudMail 域名列表失败: {e}") + + def create_email(self, config: Dict[str, Any] = None) -> Dict[str, Any]: + req_config = config or {} + self._ensure_domains() + + domain = str(req_config.get("domain") or self.config.get("default_domain") or "").strip() + if not domain and self._domains: + domain = self._domains[0] + if domain and not domain.startswith("@"): + domain = f"@{domain}" + + email = str(req_config.get("email") or "").strip() + if not email: + prefix = str(req_config.get("name") or req_config.get("prefix") or self._generate_local_part()).strip() + if not domain: + raise EmailServiceError("未配置可用域名,无法创建 CloudMail 邮箱") + email = f"{prefix}{domain}" + + data = self._make_request( + "POST", + "/api/account/add", + authorization=self._get_authorization_jwt(), + json={"email": email, "token": ""}, + headers={"content-type": "application/json"}, + ) + + account_data = (data or {}).get("data") or {} + account_id = account_data.get("accountId") + email_address = account_data.get("email") or email + if not account_id or not email_address: + raise EmailServiceError(f"创建邮箱失败,返回数据不完整: {data}") + + email_info = { + "id": str(account_id), + "service_id": str(account_id), + "accountId": str(account_id), + "email": str(email_address), + "created_at": time.time(), + "raw_data": account_data, + } + self._cache_account(email_info) + self.update_status(True) + return email_info + + def get_email_messages(self, email_id: str, **kwargs) -> List[Dict[str, Any]]: + account = self._get_account(email_id=email_id) or self._get_account(email=email_id) + if not account: + return [] + + account_id = account.get("accountId") or account.get("service_id") + if not account_id: + return [] + + params = { + "accountId": str(account_id), + "allReceive": str(kwargs.get("allReceive", 0)), + "emailId": str(kwargs.get("emailId", 0)), + "timeSort": str(kwargs.get("timeSort", 0)), + "size": str(kwargs.get("size", 20)), + "type": str(kwargs.get("type", 0)), + } + + data = self._make_request( + "GET", + "/api/email/list", + authorization=self._get_authorization_jwt(), + params=params, + headers={ + "cache-control": "no-cache", + "pragma": "no-cache", + }, + ) + payload = (data or {}).get("data") or {} + messages = payload.get("list") or [] + return messages if isinstance(messages, list) else [] + + def get_verification_code( + self, + email: str, + email_id: str = None, + timeout: int = 120, + pattern: str = OTP_CODE_PATTERN, + otp_sent_at: Optional[float] = None, + ) -> Optional[str]: + account = self._get_account(email=email, email_id=email_id) + if not account: + logger.warning(f"CloudMail 未找到邮箱缓存: {email}, {email_id}") + return None + + start_time = time.time() + seen_message_ids = set() + poll_interval = max(1, int(self.config.get("poll_interval") or 3)) + + while time.time() - start_time < timeout: + try: + messages = self.get_email_messages(account.get("accountId") or account.get("service_id")) + messages = sorted( + messages, + key=lambda item: str(item.get("createTime") or ""), + reverse=True, + ) + + for message in messages: + message_id = str(message.get("emailId") or "").strip() + if not message_id or message_id in seen_message_ids: + continue + + created_at = self._parse_time(message.get("createTime")) + if otp_sent_at and created_at and created_at + 1 < otp_sent_at: + continue + + seen_message_ids.add(message_id) + + sender = str(message.get("sendEmail") or "") + subject = str(message.get("subject") or "") + text = str(message.get("text") or "") + content = str(message.get("content") or "") + merged = "\n".join(part for part in [sender, subject, text, content] if part) + merged_lower = merged.lower() + + if "openai" not in merged_lower and "chatgpt" not in merged_lower: + continue + + match = re.search(pattern, merged) + if match: + self.update_status(True) + return match.group(1) + except Exception as e: + logger.debug(f"CloudMail 轮询验证码失败: {e}") + + time.sleep(poll_interval) + + logger.warning(f"等待 CloudMail 验证码超时: {email}") + return None + + def list_emails(self, **kwargs) -> List[Dict[str, Any]]: + return list(self._accounts_by_email.values()) + + def delete_email(self, email_id: str) -> bool: + account = self._get_account(email_id=email_id) or self._get_account(email=email_id) + if not account: + return False + + account_id = account.get("accountId") or account.get("service_id") + if not account_id: + return False + + try: + self._make_request( + "DELETE", + "/api/account/delete", + authorization=self._get_authorization_jwt(), + params={"accountId": str(account_id)}, + ) + self._accounts_by_id.pop(str(account_id), None) + self._accounts_by_email.pop(str(account.get("email") or "").lower(), None) + self.update_status(True) + return True + except Exception as e: + logger.warning(f"CloudMail 删除邮箱失败: {e}") + self.update_status(False, e) + return False + + def check_health(self) -> bool: + try: + self.get_domain_list() + self.update_status(True) + return True + except Exception as e: + logger.warning(f"CloudMail 健康检查失败: {e}") + self.update_status(False, e) + return False From e191072d5857826ebadea1dce812be8bbde98543 Mon Sep 17 00:00:00 2001 From: jjaw Date: Sun, 22 Mar 2026 03:18:15 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20CloudMail=20=E9=82=AE?= =?UTF-8?q?=E7=AE=B1=E6=9C=8D=E5=8A=A1=E5=8F=8A=E7=9B=B8=E5=85=B3=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/web/routes/email.py | 25 ++++- src/web/routes/registration.py | 24 +++++ static/js/app.js | 25 ++++- static/js/email_services.js | 69 +++++++++++-- static/js/utils.js | 1 + templates/accounts.html | 1 + templates/email_services.html | 48 +++++++++ templates/index.html | 1 + tests/test_email_service_cloudmail_routes.py | 100 +++++++++++++++++++ 9 files changed, 283 insertions(+), 11 deletions(-) create mode 100644 tests/test_email_service_cloudmail_routes.py diff --git a/src/web/routes/email.py b/src/web/routes/email.py index 5f0123cf..68632d4e 100644 --- a/src/web/routes/email.py +++ b/src/web/routes/email.py @@ -84,7 +84,15 @@ class OutlookBatchImportResponse(BaseModel): # ============== Helper Functions ============== # 敏感字段列表,返回响应时需要过滤 -SENSITIVE_FIELDS = {'password', 'api_key', 'refresh_token', 'access_token', 'admin_token'} +SENSITIVE_FIELDS = { + 'password', + 'api_key', + 'refresh_token', + 'access_token', + 'admin_token', + 'admin_password', + 'login_password', +} def filter_sensitive_config(config: Dict[str, Any]) -> Dict[str, Any]: """过滤敏感配置信息""" @@ -146,6 +154,7 @@ async def get_email_services_stats(): 'temp_mail_count': 0, 'duck_mail_count': 0, 'freemail_count': 0, + 'cloudmail_count': 0, 'imap_mail_count': 0, 'tempmail_available': True, # 临时邮箱始终可用 'enabled_count': enabled_count @@ -162,6 +171,8 @@ async def get_email_services_stats(): stats['duck_mail_count'] = count elif service_type == 'freemail': stats['freemail_count'] = count + elif service_type == 'cloudmail': + stats['cloudmail_count'] = count elif service_type == 'imap_mail': stats['imap_mail_count'] = count @@ -235,6 +246,18 @@ async def get_service_types(): {"name": "domain", "label": "邮箱域名", "required": False, "placeholder": "example.com"}, ] }, + { + "value": "cloudmail", + "label": "CloudMail", + "description": "CloudMail Web API 邮箱服务,使用后台账户创建临时邮箱", + "config_fields": [ + {"name": "base_url", "label": "站点地址", "required": True, "placeholder": "https://mail.example.com"}, + {"name": "login_email", "label": "登录邮箱", "required": True}, + {"name": "login_password", "label": "登录密码", "required": True, "secret": True}, + {"name": "default_domain", "label": "默认域名", "required": False, "placeholder": "example.com"}, + {"name": "poll_interval", "label": "轮询间隔", "required": False, "default": 3}, + ] + }, { "value": "imap_mail", "label": "IMAP 邮箱", diff --git a/src/web/routes/registration.py b/src/web/routes/registration.py index daa92e5e..8a4c2603 100644 --- a/src/web/routes/registration.py +++ b/src/web/routes/registration.py @@ -1125,6 +1125,11 @@ async def get_available_email_services(): "count": 0, "services": [] }, + "cloudmail": { + "available": False, + "count": 0, + "services": [] + }, "imap_mail": { "available": False, "count": 0, @@ -1238,6 +1243,25 @@ async def get_available_email_services(): result["freemail"]["count"] = len(freemail_services) result["freemail"]["available"] = len(freemail_services) > 0 + cloudmail_services = db.query(EmailServiceModel).filter( + EmailServiceModel.service_type == "cloudmail", + EmailServiceModel.enabled == True + ).order_by(EmailServiceModel.priority.asc()).all() + + for service in cloudmail_services: + config = service.config or {} + result["cloudmail"]["services"].append({ + "id": service.id, + "name": service.name, + "type": "cloudmail", + "default_domain": config.get("default_domain"), + "login_email": config.get("login_email"), + "priority": service.priority + }) + + result["cloudmail"]["count"] = len(cloudmail_services) + result["cloudmail"]["available"] = len(cloudmail_services) > 0 + imap_mail_services = db.query(EmailServiceModel).filter( EmailServiceModel.service_type == "imap_mail", EmailServiceModel.enabled == True diff --git a/static/js/app.js b/static/js/app.js index 543dc0b4..b948a071 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -24,7 +24,8 @@ let availableServices = { moe_mail: { available: false, services: [] }, temp_mail: { available: false, services: [] }, duck_mail: { available: false, services: [] }, - freemail: { available: false, services: [] } + freemail: { available: false, services: [] }, + cloudmail: { available: false, services: [] } }; // WebSocket 相关变量 @@ -372,6 +373,23 @@ function updateEmailServiceOptions() { select.appendChild(optgroup); } + + // CloudMail + if (availableServices.cloudmail && availableServices.cloudmail.available) { + const optgroup = document.createElement('optgroup'); + optgroup.label = `☁️ CloudMail (${availableServices.cloudmail.count} 个服务)`; + + availableServices.cloudmail.services.forEach(service => { + const option = document.createElement('option'); + option.value = `cloudmail:${service.id}`; + option.textContent = service.name + (service.default_domain ? ` (@${service.default_domain})` : ''); + option.dataset.type = 'cloudmail'; + option.dataset.serviceId = service.id; + optgroup.appendChild(option); + }); + + select.appendChild(optgroup); + } } // 处理邮箱服务切换 @@ -422,6 +440,11 @@ function handleServiceChange(e) { if (service) { addLog('info', `[系统] 已选择 Freemail 服务: ${service.name}`); } + } else if (type === 'cloudmail') { + const service = availableServices.cloudmail.services.find(s => s.id == id); + if (service) { + addLog('info', `[系统] 已选择 CloudMail 服务: ${service.name}`); + } } } diff --git a/static/js/email_services.js b/static/js/email_services.js index fafd85b4..08ce3044 100644 --- a/static/js/email_services.js +++ b/static/js/email_services.js @@ -4,7 +4,7 @@ // 状态 let outlookServices = []; -let customServices = []; // 合并 moe_mail + temp_mail + duck_mail + freemail + imap_mail +let customServices = []; // 合并 moe_mail + temp_mail + duck_mail + freemail + cloudmail + imap_mail let selectedOutlook = new Set(); let selectedCustom = new Set(); @@ -52,6 +52,7 @@ const elements = { addTempmailFields: document.getElementById('add-tempmail-fields'), addDuckmailFields: document.getElementById('add-duckmail-fields'), addFreemailFields: document.getElementById('add-freemail-fields'), + addCloudmailFields: document.getElementById('add-cloudmail-fields'), addImapFields: document.getElementById('add-imap-fields'), // 编辑自定义域名模态框 @@ -63,6 +64,7 @@ const elements = { editTempmailFields: document.getElementById('edit-tempmail-fields'), editDuckmailFields: document.getElementById('edit-duckmail-fields'), editFreemailFields: document.getElementById('edit-freemail-fields'), + editCloudmailFields: document.getElementById('edit-cloudmail-fields'), editImapFields: document.getElementById('edit-imap-fields'), editCustomTypeBadge: document.getElementById('edit-custom-type-badge'), editCustomSubTypeHidden: document.getElementById('edit-custom-sub-type-hidden'), @@ -79,6 +81,7 @@ const CUSTOM_SUBTYPE_LABELS = { tempmail: '📮 TempMail(自部署 Cloudflare Worker)', duckmail: '🦆 DuckMail(DuckMail API)', freemail: 'Freemail(自部署 Cloudflare Worker)', + cloudmail: '☁️ CloudMail(自部署 Cloudflare Worker)', imap: '📧 IMAP 邮箱(Gmail/QQ/163等)' }; @@ -185,6 +188,7 @@ function switchAddSubType(subType) { elements.addTempmailFields.style.display = subType === 'tempmail' ? '' : 'none'; elements.addDuckmailFields.style.display = subType === 'duckmail' ? '' : 'none'; elements.addFreemailFields.style.display = subType === 'freemail' ? '' : 'none'; + elements.addCloudmailFields.style.display = subType === 'cloudmail' ? '' : 'none'; elements.addImapFields.style.display = subType === 'imap' ? '' : 'none'; } @@ -195,6 +199,7 @@ function switchEditSubType(subType) { elements.editTempmailFields.style.display = subType === 'tempmail' ? '' : 'none'; elements.editDuckmailFields.style.display = subType === 'duckmail' ? '' : 'none'; elements.editFreemailFields.style.display = subType === 'freemail' ? '' : 'none'; + elements.editCloudmailFields.style.display = subType === 'cloudmail' ? '' : 'none'; elements.editImapFields.style.display = subType === 'imap' ? '' : 'none'; elements.editCustomTypeBadge.textContent = CUSTOM_SUBTYPE_LABELS[subType] || CUSTOM_SUBTYPE_LABELS.moemail; } @@ -204,7 +209,7 @@ async function loadStats() { try { const data = await api.get('/email-services/stats'); 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) + (data.imap_mail_count || 0); + elements.customCount.textContent = (data.custom_count || 0) + (data.temp_mail_count || 0) + (data.duck_mail_count || 0) + (data.freemail_count || 0) + (data.cloudmail_count || 0) + (data.imap_mail_count || 0); elements.tempmailStatus.textContent = data.tempmail_available ? '可用' : '不可用'; elements.totalEnabled.textContent = data.enabled_count || 0; } catch (error) { @@ -289,6 +294,9 @@ function getCustomServiceTypeBadge(subType) { if (subType === 'freemail') { return 'Freemail'; } + if (subType === 'cloudmail') { + return 'CloudMail'; + } return 'IMAP'; } @@ -298,6 +306,20 @@ function getCustomServiceAddress(service) { const emailAddr = service.config?.email || ''; return `${escapeHtml(host)}
${escapeHtml(emailAddr)}
`; } + if (service._subType === 'cloudmail') { + const baseUrl = service.config?.base_url || '-'; + const details = []; + if (service.config?.login_email) { + details.push(escapeHtml(service.config.login_email)); + } + if (service.config?.default_domain) { + details.push(`默认域名:@${escapeHtml(service.config.default_domain)}`); + } + if (details.length === 0) { + return escapeHtml(baseUrl); + } + return `${escapeHtml(baseUrl)}
${details.join(' | ')}
`; + } const baseUrl = service.config?.base_url || '-'; const domain = service.config?.default_domain || service.config?.domain; if (!domain) { @@ -306,14 +328,15 @@ function getCustomServiceAddress(service) { return `${escapeHtml(baseUrl)}
默认域名:@${escapeHtml(domain)}
`; } -// 加载自定义邮箱服务(moe_mail + temp_mail + duck_mail + freemail 合并) +// 加载自定义邮箱服务(moe_mail + temp_mail + duck_mail + freemail + cloudmail + imap_mail 合并) async function loadCustomServices() { try { - const [r1, r2, r3, r4, r5] = await Promise.all([ + const [r1, r2, r3, r4, r5, r6] = await Promise.all([ api.get('/email-services?service_type=moe_mail'), api.get('/email-services?service_type=temp_mail'), api.get('/email-services?service_type=duck_mail'), api.get('/email-services?service_type=freemail'), + api.get('/email-services?service_type=cloudmail'), api.get('/email-services?service_type=imap_mail') ]); customServices = [ @@ -321,7 +344,8 @@ async function loadCustomServices() { ...(r2.services || []).map(s => ({ ...s, _subType: 'tempmail' })), ...(r3.services || []).map(s => ({ ...s, _subType: 'duckmail' })), ...(r4.services || []).map(s => ({ ...s, _subType: 'freemail' })), - ...(r5.services || []).map(s => ({ ...s, _subType: 'imap' })) + ...(r5.services || []).map(s => ({ ...s, _subType: 'cloudmail' })), + ...(r6.services || []).map(s => ({ ...s, _subType: 'imap' })) ]; if (customServices.length === 0) { @@ -466,6 +490,15 @@ async function handleAddCustom(e) { admin_token: formData.get('fm_admin_token'), domain: formData.get('fm_domain') }; + } else if (subType === 'cloudmail') { + serviceType = 'cloudmail'; + config = { + base_url: formData.get('cm_base_url'), + login_email: formData.get('cm_login_email'), + login_password: formData.get('cm_login_password'), + default_domain: formData.get('cm_domain'), + poll_interval: parseInt(formData.get('cm_poll_interval'), 10) || 3 + }; } else { serviceType = 'imap_mail'; config = { @@ -606,7 +639,7 @@ function escapeHtml(text) { // ============== 编辑功能 ============== -// 编辑自定义邮箱服务(支持 moemail / tempmail / duckmail) +// 编辑自定义邮箱服务(支持 moemail / tempmail / duckmail / freemail / cloudmail / imap) async function editCustomService(id, subType) { try { const service = await api.get(`/email-services/${id}/full`); @@ -617,9 +650,11 @@ async function editCustomService(id, subType) { ? 'duckmail' : service.service_type === 'freemail' ? 'freemail' - : service.service_type === 'imap_mail' - ? 'imap' - : 'moemail' + : service.service_type === 'cloudmail' + ? 'cloudmail' + : service.service_type === 'imap_mail' + ? 'imap' + : 'moemail' ); document.getElementById('edit-custom-id').value = service.id; @@ -650,6 +685,13 @@ async function editCustomService(id, subType) { document.getElementById('edit-fm-admin-token').value = ''; document.getElementById('edit-fm-admin-token').placeholder = service.config?.admin_token ? '已设置,留空保持不变' : '请输入 Admin Token'; document.getElementById('edit-fm-domain').value = service.config?.domain || ''; + } else if (resolvedSubType === 'cloudmail') { + document.getElementById('edit-cm-base-url').value = service.config?.base_url || ''; + document.getElementById('edit-cm-login-email').value = service.config?.login_email || ''; + document.getElementById('edit-cm-login-password').value = ''; + document.getElementById('edit-cm-login-password').placeholder = service.config?.login_password ? '已设置,留空保持不变' : '请输入 CloudMail 登录密码'; + document.getElementById('edit-cm-domain').value = service.config?.default_domain || ''; + document.getElementById('edit-cm-poll-interval').value = service.config?.poll_interval || 3; } else { document.getElementById('edit-imap-host').value = service.config?.host || ''; document.getElementById('edit-imap-port').value = service.config?.port || 993; @@ -703,6 +745,15 @@ async function handleEditCustom(e) { }; const token = formData.get('fm_admin_token'); if (token && token.trim()) config.admin_token = token.trim(); + } else if (subType === 'cloudmail') { + config = { + base_url: formData.get('cm_base_url'), + login_email: formData.get('cm_login_email'), + default_domain: formData.get('cm_domain'), + poll_interval: parseInt(formData.get('cm_poll_interval'), 10) || 3 + }; + const password = formData.get('cm_login_password'); + if (password && password.trim()) config.login_password = password.trim(); } else { config = { host: formData.get('imap_host'), diff --git a/static/js/utils.js b/static/js/utils.js index b7b5dab0..ca2d1702 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -355,6 +355,7 @@ const statusMap = { temp_mail: 'Temp-Mail(自部署)', duck_mail: 'DuckMail', freemail: 'Freemail', + cloudmail: 'CloudMail', imap_mail: 'IMAP 邮箱' } }; diff --git a/templates/accounts.html b/templates/accounts.html index 32b67314..d1c4cbac 100644 --- a/templates/accounts.html +++ b/templates/accounts.html @@ -110,6 +110,7 @@

账号管理

+ diff --git a/templates/email_services.html b/templates/email_services.html index 4b18766f..0db6b531 100644 --- a/templates/email_services.html +++ b/templates/email_services.html @@ -211,6 +211,7 @@

➕ 添加自定义邮箱服务

+ @@ -278,6 +279,29 @@

➕ 添加自定义邮箱服务

+ + + +