diff --git a/api/config.py b/api/config.py index f59f2b8..ceaa325 100644 --- a/api/config.py +++ b/api/config.py @@ -12,7 +12,8 @@ CONFIG_KEYS = [ "freemail_api_url", "freemail_admin_token", "freemail_username", "freemail_password", "moemail_api_url", "mail_provider", - "cfworker_api_url", "cfworker_admin_token", "cfworker_custom_auth", "cfworker_domain", "cfworker_fingerprint", + "cfworker_api_url", "cfworker_admin_token", "cfworker_custom_auth", "cfworker_domain", + "cfworker_domains", "cfworker_enabled_domains", "cfworker_fingerprint", "smstome_cookie", "smstome_country_slugs", "smstome_phone_attempts", "smstome_otp_timeout_seconds", "smstome_poll_interval_seconds", "smstome_sync_max_pages_per_country", "luckmail_base_url", "luckmail_api_key", "luckmail_email_type", "luckmail_domain", diff --git a/core/base_mailbox.py b/core/base_mailbox.py index 7b38f4c..bba17c8 100644 --- a/core/base_mailbox.py +++ b/core/base_mailbox.py @@ -1,5 +1,6 @@ """邮箱池基类 - 抽象临时邮箱/收件服务""" import json +import random from abc import ABC, abstractmethod from dataclasses import dataclass @@ -117,6 +118,9 @@ def create_mailbox(provider: str, extra: dict = None, proxy: str = None) -> 'Bas api_url=extra.get("cfworker_api_url", ""), admin_token=extra.get("cfworker_admin_token", ""), domain=extra.get("cfworker_domain", ""), + domain_override=extra.get("cfworker_domain_override", ""), + domains=extra.get("cfworker_domains", ""), + enabled_domains=extra.get("cfworker_enabled_domains", ""), fingerprint=extra.get("cfworker_fingerprint", ""), custom_auth=extra.get("cfworker_custom_auth", ""), proxy=proxy, @@ -411,10 +415,20 @@ class CFWorkerMailbox(BaseMailbox): """Cloudflare Worker 自建临时邮箱服务""" def __init__(self, api_url: str, admin_token: str = "", domain: str = "", - fingerprint: str = "", custom_auth: str = "", proxy: str = None): + domain_override: str = "", domains: Any = None, + enabled_domains: Any = None, fingerprint: str = "", + custom_auth: str = "", proxy: str = None): self.api = api_url.rstrip("/") self.admin_token = admin_token - self.domain = domain + self.domain = self._normalize_domain(domain) + self.domain_override = self._normalize_domain(domain_override) + self.domains = self._parse_domains(domains) + raw_enabled_domains = self._parse_domains(enabled_domains) + if self.domains: + allowed = set(self.domains) + self.enabled_domains = [d for d in raw_enabled_domains if d in allowed] + else: + self.enabled_domains = raw_enabled_domains self.fingerprint = fingerprint self.custom_auth = custom_auth self.proxy = {"http": proxy, "https": proxy} if proxy else None @@ -479,18 +493,67 @@ class CFWorkerMailbox(BaseMailbox): ) from e def _generate_local_part(self) -> str: - import random, string + import string # 避免纯数字开头,提高邮箱格式“像真人”的程度 prefix = "".join(random.choices(string.ascii_lowercase, k=6)) suffix = "".join(random.choices(string.digits, k=4)) return f"{prefix}{suffix}" + @staticmethod + def _normalize_domain(domain: Any) -> str: + value = str(domain or "").strip().lower() + if value.startswith("@"): + value = value[1:] + return value + + @classmethod + def _parse_domains(cls, value: Any) -> list[str]: + if not value: + return [] + + items: list[Any] + if isinstance(value, (list, tuple, set)): + items = list(value) + elif isinstance(value, str): + text = value.strip() + if not text: + return [] + try: + parsed = json.loads(text) + except Exception: + parsed = None + if isinstance(parsed, list): + items = parsed + else: + items = [part for chunk in text.splitlines() for part in chunk.split(",")] + else: + items = [value] + + domains: list[str] = [] + seen = set() + for item in items: + domain = cls._normalize_domain(item) + if not domain or domain in seen: + continue + seen.add(domain) + domains.append(domain) + return domains + + def _pick_domain(self) -> str: + if self.domain_override: + return self.domain_override + if self.enabled_domains: + return random.choice(self.enabled_domains) + return self.domain + def get_email(self) -> MailboxAccount: self._ensure_api_configured() name = self._generate_local_part() payload = {"enablePrefix": True, "name": name} - if self.domain: - payload["domain"] = self.domain + selected_domain = self._pick_domain() + if selected_domain: + payload["domain"] = selected_domain + self._log(f"[CFWorker] 本次使用域名: {selected_domain}") data = self._request_json("POST", "/admin/new_address", payload=payload, timeout=15) email = data.get("email", data.get("address", "")) token = data.get("token", data.get("jwt", "")) @@ -498,7 +561,11 @@ class CFWorkerMailbox(BaseMailbox): raise RuntimeError(f"CFWorker API /admin/new_address 返回缺少 email/jwt: {data}") self._token = token print(f"[CFWorker] 生成邮箱: {email} token={token[:40] if token else 'NONE'}...") - return MailboxAccount(email=email, account_id=token) + return MailboxAccount( + email=email, + account_id=token, + extra={"cfworker_domain": selected_domain} if selected_domain else None, + ) def _get_mails(self, email: str) -> list: self._ensure_api_configured() diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index ab95a35..6c692aa 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -49,7 +49,7 @@ export default function Register() { cfworker_api_url: cfg.cfworker_api_url || '', cfworker_admin_token: cfg.cfworker_admin_token || '', cfworker_custom_auth: cfg.cfworker_custom_auth || '', - cfworker_domain: cfg.cfworker_domain || '', + cfworker_domain_override: '', cfworker_fingerprint: cfg.cfworker_fingerprint || '', smstome_cookie: cfg.smstome_cookie || '', smstome_country_slugs: cfg.smstome_country_slugs || '', @@ -94,7 +94,7 @@ export default function Register() { cfworker_api_url: values.cfworker_api_url, cfworker_admin_token: values.cfworker_admin_token, cfworker_custom_auth: values.cfworker_custom_auth, - cfworker_domain: values.cfworker_domain, + cfworker_domain_override: values.cfworker_domain_override, cfworker_fingerprint: values.cfworker_fingerprint, smstome_cookie: values.smstome_cookie, smstome_country_slugs: values.smstome_country_slugs, @@ -238,7 +238,11 @@ export default function Register() { - + diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 42b8bc1..76f3803 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -10,6 +10,7 @@ import { CheckCircleOutlined, CloseCircleOutlined, SyncOutlined, + PlusOutlined, } from '@ant-design/icons' import { apiFetch } from '@/lib/utils' @@ -103,7 +104,7 @@ const TAB_ITEMS = [ { key: 'cfworker_api_url', label: 'API URL', placeholder: 'https://apimail.example.com' }, { key: 'cfworker_admin_token', label: '管理员 Token', secret: true }, { key: 'cfworker_custom_auth', label: '站点密码', secret: true }, - { key: 'cfworker_domain', label: '邮箱域名', placeholder: 'example.com' }, + { key: 'cfworker_custom_auth', label: '站点密码', secret: true }, { key: 'cfworker_fingerprint', label: 'Fingerprint', placeholder: '6703363b...' }, ], }, @@ -261,6 +262,41 @@ function formatResultText(data: unknown) { } } +function normalizeDomainList(input: unknown): string[] { + const items = Array.isArray(input) ? input : [] + const seen = new Set() + const domains: string[] = [] + for (const item of items) { + const domain = String(item || '').trim().toLowerCase().replace(/^@/, '') + if (!domain || seen.has(domain)) continue + seen.add(domain) + domains.push(domain) + } + return domains +} + +function parseStoredDomainList(value: unknown): string[] { + if (Array.isArray(value)) return normalizeDomainList(value) + if (typeof value !== 'string') return [] + + const text = value.trim() + if (!text) return [] + + try { + const parsed = JSON.parse(text) + if (Array.isArray(parsed)) { + return normalizeDomainList(parsed) + } + } catch {} + + return normalizeDomainList( + text + .split('\n') + .flatMap((line) => line.split(',')) + .map((item) => item.trim()), + ) +} + function ConfigField({ field }: { field: FieldConfig }) { const [showSecret, setShowSecret] = useState(false) const options = SELECT_FIELDS[field.key] @@ -299,6 +335,133 @@ function ConfigSection({ section }: { section: SectionConfig }) { ) } +function CFWorkerDomainPoolSection({ form }: { form: any }) { + const watchedDomains = Form.useWatch('cfworker_domains', form) || [] + const watchedEnabledDomains = Form.useWatch('cfworker_enabled_domains', form) || [] + const normalizedDomains = normalizeDomainList(watchedDomains) + const enabledDomains = normalizeDomainList(watchedEnabledDomains).filter((domain) => normalizedDomains.includes(domain)) + + const updateEnabledDomains = (nextDomains: string[]) => { + form.setFieldValue('cfworker_enabled_domains', normalizeDomainList(nextDomains)) + } + + const toggleEnabledDomain = (domain: string, checked: boolean) => { + if (checked) { + updateEnabledDomains([...enabledDomains, domain]) + return + } + updateEnabledDomains(enabledDomains.filter((item) => item !== domain)) + } + + return ( + 注册时会从已启用域名中随机选择一个} + style={{ marginBottom: 16 }} + > + + {(fields, { add, remove }) => ( +
+ {fields.map((field) => ( + + { + if (!String(value || '').trim()) { + throw new Error('请输入域名') + } + }, + }, + ]} + > + + + + + ))} + {fields.length === 0 ? ( + 还没有配置域名。添加后即可在下方选择启用项。 + ) : null} + +
+ )} +
+ +