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:
1 parent
a4dbe948b2
commit
df9213463d
710 files changed
+742
-13418
No files matched your search
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user