#!/usr/bin/env python # -*- coding: utf-8 -*- import argparse import re import secrets import string import time from typing import Any, Dict, List, Optional, Tuple from curl_cffi import requests def extract_code(text: str, subject: str = "") -> Optional[str]: if subject: m = re.search(r"\b([A-Z0-9]{3}-[A-Z0-9]{3})\b", subject, re.IGNORECASE) if m: return m.group(1) m = re.search(r"\b([A-Z0-9]{3}-[A-Z0-9]{3})\b", text, re.IGNORECASE) if m: return m.group(1) for p in [ r"verification\s+code[:\s]+(\d{4,8})", r"your\s+code[:\s]+(\d{4,8})", r"confirm(?:ation)?\s+code[:\s]+(\d{4,8})", ]: m = re.search(p, text, re.IGNORECASE) if m: return m.group(1) return None def json_or_text(resp: requests.Response) -> Tuple[Optional[Dict[str, Any]], str]: try: data = resp.json() return data, "" except Exception: return None, (resp.text or "")[:400] def generate_username(length: int = 10) -> str: """生成 cloudflare_temp_email admin API 需要的随机邮箱名称。""" chars = string.ascii_lowercase + string.digits return "".join(secrets.choice(chars) for _ in range(length)) def normalize_path(path: str, default_path: str) -> str: """标准化 API 路径,避免漏写开头斜杠。""" raw = (path or default_path).strip() or default_path return raw if raw.startswith("/") else f"/{raw}" def build_auth_headers(auth_mode: str, api_key: str, content_type: bool = False) -> Dict[str, str]: """按调试参数构造 Cloudflare 临时邮箱接口鉴权请求头。""" headers = {"Content-Type": "application/json"} if content_type else {} key = (api_key or "").strip() mode = (auth_mode or "none").strip().lower() if not key: return headers if mode == "x-admin-auth": headers["x-admin-auth"] = key elif mode == "x-api-key": headers["X-API-Key"] = key elif mode == "bearer": headers["Authorization"] = f"Bearer {key}" return headers def create_address( api_base: str, auth_mode: str = "none", api_key: str = "", create_path: str = "/api/new_address", domain: str = "", name: str = "", ) -> Tuple[str, str]: """创建 Cloudflare 临时邮箱地址,支持匿名 API 和 admin API。""" path = normalize_path(create_path, "/api/new_address") is_admin_create = path.rstrip("/").lower() == "/admin/new_address" if is_admin_create: payload: Dict[str, Any] = { "name": name.strip() if name.strip() else generate_username(), "enablePrefix": True, } if domain.strip(): payload["domain"] = domain.strip() headers = build_auth_headers(auth_mode, api_key, content_type=True) else: payload = {} if domain.strip(): payload["domain"] = domain.strip() headers = {"Content-Type": "application/json"} resp = requests.post( f"{api_base.rstrip('/')}{path}", json=payload, headers=headers, timeout=20, ) resp.raise_for_status() data, raw = json_or_text(resp) if not data: raise RuntimeError(f"{path} 非JSON: {raw}") address = str(data.get("address", "")).strip() jwt = str(data.get("jwt", "")).strip() if not address or not jwt: raise RuntimeError(f"{path} 缺少 address/jwt: {data}") return address, jwt def fetch_box(api_base: str, jwt: str, path: str, params: Dict[str, Any]) -> List[Dict[str, Any]]: resp = requests.get( f"{api_base.rstrip('/')}{path}", params=params, headers={"Authorization": f"Bearer {jwt}"}, timeout=20, ) if resp.status_code >= 400: return [] data, _ = json_or_text(resp) if not isinstance(data, dict): return [] if isinstance(data.get("results"), list): return data["results"] if isinstance(data.get("data"), list): return data["data"] if isinstance(data.get("messages"), list): return data["messages"] return [] def probe_all_boxes(api_base: str, jwt: str) -> List[Tuple[str, List[Dict[str, Any]]]]: probes = [ ("/api/mails", {"limit": 20, "offset": 0}), ("/api/sendbox", {"limit": 20, "offset": 0}), ("/api/mails", {"limit": 20, "offset": 0, "box": "trash"}), ("/api/mails", {"limit": 20, "offset": 0, "folder": "trash"}), ("/api/mails", {"limit": 20, "offset": 0, "deleted": "1"}), ("/api/mails", {"limit": 20, "offset": 0, "status": "deleted"}), ] out: List[Tuple[str, List[Dict[str, Any]]]] = [] for path, params in probes: mails = fetch_box(api_base, jwt, path, params) out.append((f"{path}?{params}", mails)) return out def get_detail(api_base: str, jwt: str, mail_id: Any) -> Dict[str, Any]: for url in [ f"{api_base.rstrip('/')}/api/mail/{mail_id}", f"{api_base.rstrip('/')}/api/mails/{mail_id}", ]: try: resp = requests.get(url, headers={"Authorization": f"Bearer {jwt}"}, timeout=20) if resp.status_code >= 400: continue data, _ = json_or_text(resp) if isinstance(data, dict): return data except Exception: continue return {} def flatten_mail_text(item: Dict[str, Any], detail: Dict[str, Any]) -> Tuple[str, str]: subject = str(item.get("subject") or detail.get("subject") or "") parts: List[str] = [] for src in (item, detail): for k in ("text", "raw", "content", "intro", "body", "snippet"): v = src.get(k) if isinstance(v, str) and v.strip(): parts.append(v) html_val = src.get("html") if isinstance(html_val, str): html_val = [html_val] if isinstance(html_val, list): for h in html_val: if isinstance(h, str): parts.append(re.sub(r"<[^>]+>", " ", h)) return subject, "\n".join(parts) def main(): ap = argparse.ArgumentParser() ap.add_argument("--api-base", required=True) ap.add_argument("--address", default="") ap.add_argument("--credential", default="") ap.add_argument("--auth-mode", default="none", choices=["none", "bearer", "x-api-key", "x-admin-auth"]) ap.add_argument("--api-key", default="") ap.add_argument("--create-path", default="/api/new_address") ap.add_argument("--domain", default="") ap.add_argument("--name", default="") ap.add_argument("--timeout", type=int, default=180) ap.add_argument("--interval", type=int, default=3) args = ap.parse_args() address = args.address.strip() credential = args.credential.strip() if not credential: address, credential = create_address( args.api_base, auth_mode=args.auth_mode, api_key=args.api_key, create_path=args.create_path, domain=args.domain, name=args.name, ) print(f"[NEW] address={address}") print(f"[NEW] credential(jwt)={credential}") else: print(f"[USE] address={address or '(unknown, from credential)'}") deadline = time.time() + max(args.timeout, 1) seen_ids = set() while time.time() < deadline: boxes = probe_all_boxes(args.api_base, credential) total = 0 for name, mails in boxes: if mails: print(f"[BOX] {name} -> {len(mails)}") total += len(mails) for m in mails: mail_id = m.get("id") or m.get("mail_id") if not mail_id or mail_id in seen_ids: continue seen_ids.add(mail_id) detail = get_detail(args.api_base, credential, mail_id) subj, text = flatten_mail_text(m, detail) code = extract_code(text, subj) print(f"[MAIL] id={mail_id} subject={subj!r} code={code!r}") if code: print(f"[FOUND] {code}") return if total == 0: print("[INFO] no mails yet") time.sleep(max(args.interval, 1)) print("[TIMEOUT] no code found") if __name__ == "__main__": main()