feat(cfworker): support configurable domain pools

This commit is contained in:
小易先生
2026-03-30 22:52:33 +08:00
parent 4d90d6c4c7
commit 7e307bd11d
4 changed files with 271 additions and 13 deletions

View File

@@ -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",

View File

@@ -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()

View File

@@ -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() {
<Form.Item name="cfworker_custom_auth" label="Site Password">
<Input.Password placeholder="private site password" />
</Form.Item>
<Form.Item name="cfworker_domain" label="域名">
<Form.Item
name="cfworker_domain_override"
label="单次任务指定域名(可选)"
extra="留空时将从设置页已启用的域名列表中随机选择。"
>
<Input placeholder="example.com" />
</Form.Item>
<Form.Item name="cfworker_fingerprint" label="Fingerprint (可选)">

View File

@@ -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<string>()
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 (
<Card
title="CF Worker 域名池"
extra={<span style={{ fontSize: 12, color: '#7a8ba3' }}></span>}
style={{ marginBottom: 16 }}
>
<Form.List name="cfworker_domains">
{(fields, { add, remove }) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{fields.map((field) => (
<Space key={field.key} align="start" style={{ display: 'flex' }}>
<Form.Item
{...field}
label={field.name === 0 ? '全部域名' : ''}
style={{ flex: 1, marginBottom: 0 }}
rules={[
{
validator: async (_, value) => {
if (!String(value || '').trim()) {
throw new Error('请输入域名')
}
},
},
]}
>
<Input placeholder="example.com" />
</Form.Item>
<Button
danger
onClick={() => {
const currentDomains = Array.isArray(form.getFieldValue('cfworker_domains'))
? [...form.getFieldValue('cfworker_domains')]
: []
const removedDomain = String(currentDomains[field.name] || '').trim().toLowerCase().replace(/^@/, '')
remove(field.name)
if (!removedDomain) return
const enabledDomains = normalizeDomainList(form.getFieldValue('cfworker_enabled_domains'))
form.setFieldValue(
'cfworker_enabled_domains',
enabledDomains.filter((domain) => domain !== removedDomain),
)
}}
>
</Button>
</Space>
))}
{fields.length === 0 ? (
<Typography.Text type="secondary"></Typography.Text>
) : null}
<Button type="dashed" onClick={() => add('')} icon={<PlusOutlined />} block>
</Button>
</div>
)}
</Form.List>
<Form.Item name="cfworker_enabled_domains" hidden>
<Select mode="multiple" options={normalizedDomains.map((domain) => ({ label: domain, value: domain }))} />
</Form.Item>
<div style={{ marginTop: 16 }}>
<div style={{ marginBottom: 8, fontWeight: 500 }}></div>
{enabledDomains.length > 0 ? (
<Space wrap>
{enabledDomains.map((domain) => (
<Tag
key={domain}
color="blue"
closable
onClose={(event) => {
event.preventDefault()
updateEnabledDomains(enabledDomains.filter((item) => item !== domain))
}}
>
{domain}
</Tag>
))}
</Space>
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
</div>
<div style={{ marginTop: 16 }}>
<div style={{ marginBottom: 8, fontWeight: 500 }}></div>
{normalizedDomains.length > 0 ? (
<Space wrap>
{normalizedDomains.map((domain) => (
<Tag.CheckableTag
key={domain}
checked={enabledDomains.includes(domain)}
onChange={(checked) => toggleEnabledDomain(domain, checked)}
>
{domain}
</Tag.CheckableTag>
))}
</Space>
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
</div>
<Typography.Text type="secondary" style={{ display: 'block', marginTop: 12 }}>
</Typography.Text>
</Card>
)
}
function SolverStatus() {
const [running, setRunning] = useState<boolean | null>(null)
@@ -549,15 +712,37 @@ export default function Settings() {
if (!data.luckmail_base_url) {
data.luckmail_base_url = 'https://mails.luckyous.com/'
}
data.cfworker_domains = parseStoredDomainList(data.cfworker_domains)
data.cfworker_enabled_domains = parseStoredDomainList(data.cfworker_enabled_domains)
form.setFieldsValue(data)
})
}, [])
}, [form])
const save = async () => {
setSaving(true)
try {
const values = form.getFieldsValue()
const values = form.getFieldsValue(true)
const domains = normalizeDomainList(values.cfworker_domains)
const enabledDomains = normalizeDomainList(values.cfworker_enabled_domains).filter((domain) => domains.includes(domain))
if (domains.length > 0 && enabledDomains.length === 0) {
setActiveTab('mailbox')
message.error('CF Worker 至少需要启用一个域名')
return
}
values.cfworker_domains = JSON.stringify(domains)
values.cfworker_enabled_domains = JSON.stringify(enabledDomains)
if (domains.length > 0) {
values.cfworker_domain = ''
}
await apiFetch('/config', { method: 'PUT', body: JSON.stringify({ data: values }) })
form.setFieldsValue({
cfworker_domains: domains,
cfworker_enabled_domains: enabledDomains,
cfworker_domain: domains.length > 0 ? '' : values.cfworker_domain,
})
message.success('保存成功')
setSaved(true)
setTimeout(() => setSaved(false), 2000)
@@ -602,6 +787,7 @@ export default function Settings() {
{currentTab.sections.map((section) => (
<ConfigSection key={section.title} section={section} />
))}
{activeTab === 'mailbox' ? <CFWorkerDomainPoolSection form={form} /> : null}
<Button type="primary" icon={<SaveOutlined />} onClick={save} loading={saving} block>
{saved ? '已保存 ✓' : '保存配置'}
</Button>