Add bblbb mail provider and CPA push queue; sync runtime artifacts.

Switch default email provider to mail.bblbb.com, queue failed CPA auth pushes for retry, and refresh local auth/account outputs.
This commit is contained in:
chaos committed 2026-07-14 09:14:06 +08:00
1 parent a4dbe948b2
commit df9213463d
710 files changed
+742 -13418

No files matched your search

+259
View File
@@ -36,6 +36,12 @@ DEFAULT_CONFIG = {
"cloudflare_path_accounts": "/api/new_address",
"cloudflare_path_token": "/api/token",
"cloudflare_path_messages": "/api/mails",
# ===== 自建临时邮箱 mail.bblbb.com(任意 local-part 可收,Header X-Api-Key)=====
"email_provider": "bblbb",
"bblbb_api_base": "https://mail.bblbb.com",
"bblbb_api_key": "",
"bblbb_domain": "mail.bblbb.com",
"bblbb_poll_interval": 2.0,
"proxy": "",
# ===== 代理池(注册浏览器 + CPA 铸造共用线程绑定出口)=====
"proxy_pool_enabled": False, # True=启用代理池;False=仅用 proxy 单代理/直连
@@ -681,6 +687,8 @@ def _is_mail_api_url(url):
"temp-email",
"workers.dev/code",
"grok-xai-mail-codes",
"mail.bblbb.com",
"/api/v1/mailbox/",
)
return any(m in u for m in markers)
@@ -1098,6 +1106,240 @@ def get_email_provider():
return str(config.get("email_provider", "duckmail") or "duckmail").strip().lower()
def get_bblbb_api_base():
return str(config.get("bblbb_api_base", "") or "https://mail.bblbb.com").strip().rstrip("/")
def get_bblbb_api_key():
return str(config.get("bblbb_api_key", "") or "").strip()
def get_bblbb_domain():
domain = str(config.get("bblbb_domain", "") or "mail.bblbb.com").strip().lower()
# 兼容 defaultDomains 里只写了 bblbb 域名的情况
if not domain:
domains = _parse_domain_list(config.get("defaultDomains") or "")
for d in domains:
if "bblbb" in d or d.endswith("mail.bblbb.com"):
return d
return "mail.bblbb.com"
return domain.lstrip("@")
def bblbb_headers(api_key=None):
key = (api_key or get_bblbb_api_key()).strip()
if not key:
raise Exception("bblbb_api_key 未配置")
return {"X-Api-Key": key, "Accept": "application/json"}
def bblbb_local_part(email):
"""从完整邮箱或 token 里取出 local-part(@ 前)。"""
s = str(email or "").strip()
if s.lower().startswith("bblbb:"):
s = s.split(":", 1)[1]
if "@" in s:
return s.rsplit("@", 1)[0].strip()
return s
def bblbb_get_email_and_token(api_key=None):
"""自建临时邮箱:任意 local-part 可收,无需注册账号。
返回 (address, token),token 形如 bblbb:<local> 供 get_oai_code 识别。
"""
domain = get_bblbb_domain()
# 优先用 defaultDomains 里的 bblbb 域名(若配置了)
domains = _parse_domain_list(config.get("defaultDomains") or "")
for d in domains:
if d == domain or d.endswith(domain) or "bblbb" in d:
domain = d
break
local = generate_username(10)
address = f"{local}@{domain}"
# 触发一次 list 确认 API/Key 可用(空邮箱返回 [])
base = get_bblbb_api_base()
headers = bblbb_headers(api_key=api_key)
try:
resp = http_get(
f"{base}/api/v1/mailbox/{local}",
headers=headers,
timeout=15,
proxies={},
)
if resp.status_code in (401, 403):
raise Exception(f"bblbb 鉴权失败 HTTP {resp.status_code}")
# 非 2xx 也不阻断发信(域名 catch-all 本身可收),只记状态
_ = resp.status_code
except Exception as exc:
# 网络异常时仍返回地址,取码阶段再报错
if "鉴权失败" in str(exc):
raise
return address, f"bblbb:{local}"
def bblbb_list_messages(local, api_key=None):
base = get_bblbb_api_base()
headers = bblbb_headers(api_key=api_key)
resp = http_get(
f"{base}/api/v1/mailbox/{local}",
headers=headers,
timeout=15,
proxies={},
)
resp.raise_for_status()
data = resp.json()
if isinstance(data, list):
return data
if isinstance(data, dict):
for k in ("messages", "mails", "items", "data"):
if isinstance(data.get(k), list):
return data[k]
return []
def bblbb_get_message_detail(local, msg_id, api_key=None):
base = get_bblbb_api_base()
headers = bblbb_headers(api_key=api_key)
resp = http_get(
f"{base}/api/v1/mailbox/{local}/{msg_id}",
headers=headers,
timeout=15,
proxies={},
)
resp.raise_for_status()
return resp.json() if resp.content else {}
def bblbb_message_text(detail):
"""从 bblbb 邮件详情拼出可解析正文。"""
parts = []
if not isinstance(detail, dict):
return ""
subject = str(detail.get("subject") or "")
if subject:
parts.append(subject)
body = detail.get("body")
if isinstance(body, dict):
text = body.get("text") or ""
html = body.get("html") or ""
if text:
parts.append(str(text))
if html:
parts.append(re.sub(r"<[^>]+>", " ", str(html)))
elif isinstance(body, str) and body.strip():
parts.append(body)
for field in ("text", "html", "raw", "content", "snippet"):
value = detail.get(field)
if isinstance(value, str) and value.strip():
if field == "html":
parts.append(re.sub(r"<[^>]+>", " ", value))
else:
parts.append(value)
elif isinstance(value, list):
for item in value:
if isinstance(item, str) and item.strip():
parts.append(
re.sub(r"<[^>]+>", " ", item) if field == "html" else item
)
return "\n".join(parts)
def bblbb_get_oai_code(
dev_token,
email,
timeout=180,
poll_interval=None,
log_callback=None,
cancel_callback=None,
resend_callback=None,
api_key=None,
):
"""轮询 mail.bblbb.com 收件箱,从 body.text/html 提取验证码。"""
local = bblbb_local_part(dev_token) or bblbb_local_part(email)
if not local:
raise Exception("bblbb 邮箱 local-part 为空")
if poll_interval is None:
try:
poll_interval = float(config.get("bblbb_poll_interval", 2.0) or 2.0)
except Exception:
poll_interval = 2.0
poll_interval = max(0.5, min(float(poll_interval), 10.0))
deadline = time.time() + max(10.0, float(timeout))
seen_ids = set()
next_resend_at = time.time() + 35
if log_callback:
log_callback(
f"[*] 取码:bblbb ({local}@{get_bblbb_domain()}, "
f"timeout={timeout}s interval={poll_interval}s)"
)
while time.time() < deadline:
raise_if_cancelled(cancel_callback)
if resend_callback and time.time() >= next_resend_at:
try:
resend_callback()
if log_callback:
log_callback("[Debug] bblbb: 已触发重新发送验证码")
except Exception as exc:
if log_callback:
log_callback(f"[Debug] bblbb: 触发重发失败: {exc}")
next_resend_at = time.time() + 35
try:
messages = bblbb_list_messages(local, api_key=api_key)
except Exception as exc:
if log_callback:
log_callback(f"[Debug] bblbb 拉取邮件列表失败: {exc}")
sleep_with_cancel(poll_interval, cancel_callback)
continue
if log_callback and messages:
log_callback(f"[Debug] bblbb 本轮邮件数量: {len(messages)}")
# 优先新邮件:按 posix-millis / date 倒序
def _msg_sort_key(m):
if not isinstance(m, dict):
return 0
try:
return int(m.get("posix-millis") or 0)
except Exception:
return 0
ordered = sorted(
[m for m in messages if isinstance(m, dict)],
key=_msg_sort_key,
reverse=True,
) or [m for m in messages if isinstance(m, dict)]
for msg in ordered:
msg_id = msg.get("id") or msg.get("msgid")
if not msg_id or msg_id in seen_ids:
continue
seen_ids.add(msg_id)
subject = str(msg.get("subject") or "")
if log_callback:
log_callback(f"[Debug] bblbb 收到邮件: {subject or msg_id}")
try:
detail = bblbb_get_message_detail(local, msg_id, api_key=api_key)
except Exception as exc:
if log_callback:
log_callback(f"[Debug] bblbb 获取邮件详情失败: {exc}")
# 列表项本身可能没有正文,跳过
continue
combined = bblbb_message_text(detail)
if not combined.strip():
# 兜底:把列表字段也拼进去
combined = bblbb_message_text(msg)
code = extract_verification_code(combined, subject or detail.get("subject", ""))
if code:
if log_callback:
log_callback(f"[Debug] bblbb 提取验证码: {code}")
return code
if log_callback:
log_callback(
f"[Debug] bblbb 邮件无验证码可解析 id={msg_id} "
f"preview={(combined or '')[:80]!r}"
)
sleep_with_cancel(poll_interval, cancel_callback)
raise Exception(f"bblbb 在 {timeout}s 内未收到验证码邮件: {local}@{get_bblbb_domain()}")
def _config_bool(value, default=False):
if value is None:
return default
@@ -1316,6 +1558,8 @@ def cf_worker_get_oai_code(
def get_email_and_token(api_key=None):
provider = get_email_provider()
if provider in ("bblbb", "mail.bblbb", "mail.bblbb.com", "bblbb_mail"):
return bblbb_get_email_and_token(api_key=api_key)
if provider == "yyds":
return yyds_get_email_and_token(api_key=api_key, jwt=get_yyds_jwt())
if provider in ("gmail", "cf_worker", "cf-email-worker"):
@@ -1385,6 +1629,21 @@ def get_oai_code(
email_domain = ""
if email and "@" in email:
email_domain = email.rsplit("@", 1)[-1].strip().lower()
tok = str(dev_token or "").strip().lower()
if (
tok.startswith("bblbb:")
or provider in ("bblbb", "mail.bblbb", "mail.bblbb.com", "bblbb_mail")
or (email_domain and ("bblbb.com" in email_domain or email_domain == get_bblbb_domain()))
):
return bblbb_get_oai_code(
dev_token,
email,
timeout=timeout,
poll_interval=poll_interval,
log_callback=log_callback,
cancel_callback=cancel_callback,
resend_callback=resend_callback,
)
if (
is_cf_worker_token(dev_token)
or provider in ("gmail", "cf_worker", "cf-email-worker")