#!/usr/bin/env python3 """Poll 2925 Webmail for an OpenAI/ChatGPT code matching the current generated alias. Why this exists: - 2925 OpenAI mail may land in 垃圾箱 instead of 收件箱. - 垃圾箱/已删除 can contain stale OpenAI codes for older aliases. - Never submit a visible 6-digit code unless the surrounding message matches the current alias/local-part. Outputs compact JSONL and saves the code to --out-file without printing it. """ import argparse import json import pathlib import re import time import urllib.request import websocket import itertools FOLDERS = { "inbox": "https://2925.com/#/mailList?type=%E6%94%B6%E4%BB%B6%E7%AE%B1", "junk": "https://2925.com/#/mailList?type=%E5%9E%83%E5%9C%BE%E7%AE%B1", "deleted": "https://2925.com/#/mailList?type=%E5%B7%B2%E5%88%A0%E9%99%A4", } def log(obj): print(json.dumps({**obj, "ts": int(time.time())}, ensure_ascii=False), flush=True) def cdp_json(base, path): return json.loads(urllib.request.urlopen(base + path, timeout=10).read().decode()) def connect_tab(base, url_substr="2925.com"): tabs = cdp_json(base, "/json") tab = next((t for t in tabs if url_substr in (t.get("url") or "")), tabs[0]) ws = websocket.create_connection(tab["webSocketDebuggerUrl"], timeout=20) counter = itertools.count(1) def call(method, params=None, timeout=25): msg_id = next(counter) ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}})) end = time.time() + timeout while time.time() < end: msg = json.loads(ws.recv()) if msg.get("id") == msg_id: if "error" in msg: raise RuntimeError(msg["error"]) return msg.get("result", {}) raise TimeoutError(method) call("Runtime.enable") call("Page.enable") def eval_js(expr, await_promise=False, timeout=25): result = call( "Runtime.evaluate", {"expression": expr, "awaitPromise": await_promise, "returnByValue": True}, timeout, ) return result.get("result", {}).get("value") return call, eval_js def main(): ap = argparse.ArgumentParser() ap.add_argument("--cdp-port", type=int, required=True) ap.add_argument("--account", required=True, help="Selected 2925 base account, e.g. user@2925.com") ap.add_argument("--alias", required=True, help="Current generated alias used on OpenAI") ap.add_argument("--out-file", default="/tmp/openai_2925_email_code.txt") ap.add_argument("--attempts", type=int, default=15) ap.add_argument("--interval", type=float, default=15.0) ap.add_argument("--folders", default="inbox,junk", help="comma list: inbox,junk,deleted") ap.add_argument("--reject-code", action="append", default=[]) args = ap.parse_args() base = f"http://127.0.0.1:{args.cdp_port}" account = args.account.strip().lower() alias = args.alias.strip().lower() alias_local = alias.split("@", 1)[0] rejected = {str(x).strip() for x in args.reject_code if str(x).strip()} folders = [x.strip() for x in args.folders.split(",") if x.strip()] call, ev = connect_tab(base) for attempt in range(1, args.attempts + 1): for folder in folders: url = FOLDERS.get(folder) if not url: continue call("Page.navigate", {"url": url}) time.sleep(4) js = f""" (() => {{ try {{ const account = {json.dumps(account)}; const aliasLocal = {json.dumps(alias_local)}; const txt = document.body?.innerText || ''; const emails = [...new Set([...txt.matchAll(/[A-Za-z0-9._%+-]+@2925\\.com/g)].map(m => m[0].toLowerCase()))]; const lines = txt.split(/\n+/).map(s => s.trim()).filter(Boolean); const snippets = []; for (let i = 0; i < lines.length; i++) {{ const line = lines[i].toLowerCase(); if (line.includes(aliasLocal) || /openai|chatgpt|tm1\\.openai|验证码|verification/.test(line)) {{ const s = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 9)).join('\n'); if (s.toLowerCase().includes(aliasLocal) && /OpenAI|ChatGPT|tm1\\.openai|验证码|verification/i.test(s)) snippets.push(s); }} }} const codes = [...new Set(snippets.flatMap(s => [...s.matchAll(/(? m[1])))]; return {{ href: location.href, emails, hasAccount: emails.includes(account), hasAliasLocal: txt.toLowerCase().includes(aliasLocal), snippets: snippets.slice(0, 5), codes }}; }} catch (e) {{ return {{ error: String(e), emails: [], snippets: [], codes: [] }}; }} }})() """ data = ev(js) or {"error": "null_eval", "emails": [], "snippets": [], "codes": []} if account not in data.get("emails", []): log({"phase": "account_gate_failed", "folder": folder, "expected": account, "emails": data.get("emails", [])}) raise SystemExit(4) fresh = [c for c in data.get("codes", []) if c not in rejected] log({ "phase": "poll_alias_code", "attempt": attempt, "folder": folder, "hasAliasLocal": bool(data.get("hasAliasLocal")), "snippetCount": len(data.get("snippets", [])), "freshCount": len(fresh), }) if fresh: pathlib.Path(args.out_file).write_text(fresh[0]) log({"phase": "code_saved", "folder": folder, "attempt": attempt, "outFile": args.out_file}) return time.sleep(args.interval) log({"phase": "timeout_no_current_alias_code", "alias": alias}) raise SystemExit(2) if __name__ == "__main__": main()