#!/usr/bin/env python3 """ 自动注册脚本 v2(Cloudflare catch-all + Gmail IMAP) 流程:生成随机别名 -> Chromium注册 -> 直接IMAP取验证码 -> 填码完成注册 """ import argparse import email as email_lib import imaplib import json import os import random import re import string import sys import time from datetime import datetime, timezone from email.header import decode_header as _decode_header from email.utils import parsedate_to_datetime from pathlib import Path from DrissionPage import Chromium, ChromiumOptions # ---- 配置 ---- BASE_URL = "https://accounts.x.ai" SIGNUP_URL = "https://accounts.x.ai/sign-up?redirect=grok-com" ACCOUNTS_FILE = "accounts_cli.txt" CPA_DIR = "cpa_auths" DOMAIN = "zhangyunuo.net" CODE_TIMEOUT = 180 CODE_POLL = 3 IMAP_HOST = "imap.gmail.com" IMAP_PORT = 993 EXTENSION_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "turnstilePatch") CHROMIUM_SLIM_FLAGS = [ "--disable-gpu", "--disable-software-rasterizer", "--no-sandbox", "--disable-dev-shm-usage", "--disable-images", "--mute-audio", "--disable-background-networking", "--no-first-run", ] CODE_PATTERNS = [ re.compile(r"verification\s+code[:\s]+(\d{4,8})", re.I), re.compile(r"your\s+code[:\s]+(\d{4,8})", re.I), re.compile(r"confirm(?:ation)?\s+code[:\s]+(\d{4,8})", re.I), re.compile(r"code[:\s]+(\d{4,8})", re.I), re.compile(r"验证码[::\\s]+(\d{4,8})"), ] def load_env(path=".env"): data = {} if not os.path.exists(path): return data with open(path) as f: for line in f: line = line.strip() if not line or line.startswith("#"): continue if "=" in line: k, v = line.split("=", 1) data[k.strip()] = v.strip().strip('"').strip("'") return data def gen_alias(domain: str, length: int = 10) -> str: alias = "".join(random.choices(string.ascii_lowercase + string.digits, k=length)) return f"{alias}@{domain}" def gen_password(length: int = 16) -> str: # Avoid form-hostile punctuation; keep strong enough for xAI signup rules. lower = string.ascii_lowercase upper = string.ascii_uppercase digits = string.digits special = "!@#$%^&*" if length < 8: length = 8 base = [ random.choice(lower), random.choice(upper), random.choice(digits), random.choice(special), ] pool = lower + upper + digits + special base += random.choices(pool, k=length - len(base)) random.shuffle(base) return "".join(base) def decode_header(raw: str) -> str: parts = _decode_header(raw or "") out = [] for part, charset in parts: if isinstance(part, bytes): out.append(part.decode(charset or "utf-8", errors="replace")) else: out.append(part) return "".join(out) def message_body(msg) -> str: body = "" if msg.is_multipart(): for part in msg.walk(): ct = part.get_content_type() if ct == "text/plain": payload = part.get_payload(decode=True) if payload: body += payload.decode("utf-8", errors="replace") else: payload = msg.get_payload(decode=True) if payload: body += payload.decode("utf-8", errors="replace") return body def extract_code(text: str) -> str | None: for pat in CODE_PATTERNS: m = pat.search(text) if m: return m.group(1) return None def imap_login(user: str, password: str): imap = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT, timeout=45) imap.login(user, password) imap.select("INBOX") return imap def fetch_code(imap, target_email: str, timeout: int = CODE_TIMEOUT, poll: int = CODE_POLL) -> str | None: deadline = time.time() + timeout target_lower = target_email.lower().strip() while time.time() < deadline: try: status, data = imap.search(None, "ALL") if status != "OK" or not data or not data[0]: time.sleep(poll) continue msg_ids = data[0].split()[-30:] filter_ts = int((time.time() - 900) * 1000) for mid in reversed(msg_ids): try: _, msg_data = imap.fetch(mid, "(RFC822)") except Exception: continue if not msg_data or not msg_data[0] or not isinstance(msg_data[0][1], bytes): continue msg = email_lib.message_from_bytes(msg_data[0][1]) date_str = msg.get("Date") if date_str: try: dt = parsedate_to_datetime(date_str) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) if int(dt.timestamp() * 1000) < filter_ts: continue except Exception: pass subject = decode_header(msg.get("Subject", "")) sender = decode_header(msg.get("From", "")) recipient_blob = " ".join( decode_header(msg.get(h, "")) for h in ("To", "Cc", "Delivered-To", "X-Original-To", "Original-Recipient", "Envelope-To") ).lower() if target_lower not in recipient_blob: continue body = message_body(msg) combined = f"{subject}\n{sender}\n{recipient_blob}\n{body}".lower() if not any(kw in combined for kw in ["x.ai", "xai", "grok", "verification", "code", "confirm", "验证码", "确认"]): continue code = extract_code(combined) if code: print(f"[+] Code found for {target_email}: {code}") return code except Exception as e: print(f"[!] IMAP poll error: {e}") time.sleep(poll) return None def save_account(email: str, password: str, sso: str = ""): Path(ACCOUNTS_FILE).parent.mkdir(parents=True, exist_ok=True) with open(ACCOUNTS_FILE, "a") as f: f.write(f"{email}----{password}----{sso}\n") print(f"[+] Saved: {email}") def create_browser_options(headless: bool = False, proxy: str = "") -> ChromiumOptions: opts = ChromiumOptions() opts.auto_port() for flag in CHROMIUM_SLIM_FLAGS: opts.set_argument(flag) if os.path.exists(EXTENSION_PATH): opts.add_extension(EXTENSION_PATH) if headless: opts.headless() if proxy: try: from urllib.parse import urlparse u = urlparse(proxy if "://" in proxy else f"http://{proxy}") host = u.hostname or "" if host: port = u.port or (443 if (u.scheme or "http") == "https" else 80) scheme = u.scheme or "http" opts.set_argument(f"--proxy-server={scheme}://{host}:{port}") except Exception as e: print(f"[!] Proxy setup failed: {e}") return opts def wait_for_tab(browser: Chromium, timeout: int = 15) -> any: """Wait for browser tab to be ready and return it.""" deadline = time.time() + timeout while time.time() < deadline: try: tab_ids = browser.tab_ids if tab_ids: tab = browser.get_tab(tab_ids[0]) if tab is not None: return tab except Exception: pass time.sleep(0.5) # Fallback return browser.latest_tab def bypass_turnstile(page) -> None: """Try to bypass Cloudflare Turnstile using multiple methods.""" try: # Method 1: Check if Turnstile token already exists token = page.run_js(""" try { const byInput = String((document.querySelector('input[name="cf-turnstile-response"]') || {}).value || '').trim(); if (byInput) return byInput; if (window.turnstile && typeof turnstile.getResponse === 'function') { return String(turnstile.getResponse() || '').trim(); } return ''; } catch(e) { return ''; } """) if token and len(token) >= 80: print(f"[+] Turnstile token already present (len={len(token)})") return except Exception: pass try: # Method 2: Try to find and click Turnstile checkbox challenge_input = page.ele("@name=cf-turnstile-response", timeout=2) if challenge_input: try: wrapper = challenge_input.parent() iframe = None try: iframe = wrapper.shadow_root.ele("tag:iframe") except Exception: pass if iframe: try: iframe.run_js(""" window.dtp = 1; function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } let sx = getRandomInt(800, 1200); let sy = getRandomInt(400, 700); Object.defineProperty(MouseEvent.prototype, 'screenX', { value: sx }); Object.defineProperty(MouseEvent.prototype, 'screenY', { value: sy }); """) except Exception: pass try: body_sr = iframe.ele("tag:body").shadow_root btn = body_sr.ele("tag:input") if btn: btn.click() print("[+] Clicked Turnstile checkbox via shadow DOM") except Exception: pass except Exception: pass except Exception: pass try: # Method 3: Click any visible Turnstile container page.run_js(""" const nodes = Array.from(document.querySelectorAll('div,span,iframe')).filter((n) => { const txt = (n.className || '') + ' ' + (n.id || '') + ' ' + (n.getAttribute?.('src') || ''); return String(txt).toLowerCase().includes('turnstile'); }); if (nodes.length && typeof nodes[0].click === 'function') nodes[0].click(); """) except Exception: pass def click_email_signup_button(page, timeout: int = 15) -> bool: """Click the 'Sign up with email' button on the landing page.""" deadline = time.time() + timeout while time.time() < deadline: clicked = page.run_js(""" const candidates = Array.from(document.querySelectorAll('button, a, [role="button"]')); const target = candidates.find((node) => { const text = (node.innerText || node.textContent || '').replace(/\\s+/g, ''); const lower = text.toLowerCase(); return ( text.includes('使用邮箱注册') || lower.includes('signupwithemail') || lower.includes('continuewithemail') || lower.includes('email') ); }); if (!target) return false; target.click(); return true; """) if clicked: print("[+] Clicked 'Sign up with email' button") return True time.sleep(1) return False def fill_email_js(page, email: str) -> bool: """Fill email input using React-compatible JS injection.""" deadline = time.time() + 15 while time.time() < deadline: result = page.run_js(""" function isVisible(node) { if (!node) return false; const style = window.getComputedStyle(node); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false; const rect = node.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } const input = Array.from(document.querySelectorAll('input[data-testid="email"], input[name="email"], input[type="email"], input[autocomplete="email"]')).find((node) => isVisible(node) && !node.disabled && !node.readOnly) || null; if (!input) return 'not-ready'; input.focus(); input.click(); const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; const tracker = input._valueTracker; if (tracker) tracker.setValue(arguments[0]); if (valueSetter) valueSetter.call(input, arguments[0]); else input.value = arguments[0]; input.dispatchEvent(new Event('focus', { bubbles: true })); input.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: arguments[0], inputType: 'insertText' })); input.dispatchEvent(new InputEvent('input', { bubbles: true, data: arguments[0], inputType: 'insertText' })); input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('blur', { bubbles: true })); const current = (input.value || '').trim(); if (current === arguments[0]) return 'filled'; input.value = ''; input.dispatchEvent(new Event('input', { bubbles: true })); for (const ch of arguments[0]) { input.dispatchEvent(new KeyboardEvent('keydown', { key: ch, bubbles: true })); input.value += ch; input.dispatchEvent(new InputEvent('input', { bubbles: true, data: ch, inputType: 'insertText' })); input.dispatchEvent(new KeyboardEvent('keyup', { key: ch, bubbles: true })); } input.dispatchEvent(new Event('change', { bubbles: true })); if ((input.value || '').trim() === arguments[0]) return 'filled'; return input.value; """, email) if result == "not-ready": time.sleep(0.5) continue if result == "filled": return True time.sleep(0.5) return False def click_continue_button(page) -> bool: """Click continue/signup button after email is filled.""" clicked = page.run_js(r""" function isVisible(node) { if (!node) return false; const style = window.getComputedStyle(node); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false; const rect = node.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } const input = Array.from(document.querySelectorAll('input[data-testid="email"], input[name="email"], input[type="email"], input[autocomplete="email"]')).find((node) => isVisible(node) && !node.disabled && !node.readOnly) || null; if (!input || !(input.value || '').trim()) return false; const buttons = Array.from(document.querySelectorAll('button[type="submit"], button')).filter((node) => isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true'); const submitButton = buttons.find((node) => { const raw = (node.innerText || node.textContent || '').trim(); const compact = raw.replace(/\s+/g, '').toLowerCase(); return ( raw.includes('注册') || compact === 'signup' || compact.includes('signup') || compact.includes('continue') || compact.includes('next') || compact.includes('createaccount') || compact.includes('submit') ); }); if (!submitButton || submitButton.disabled) return false; submitButton.click(); return true; """) return bool(clicked) def gen_name() -> tuple[str, str]: first = ["Alex", "Sam", "Jordan", "Taylor", "Casey", "Riley", "Morgan", "Avery", "Quinn", "Jamie"] last = ["Smith", "Johnson", "Brown", "Taylor", "Anderson", "Thomas", "Jackson", "White", "Harris", "Martin"] return random.choice(first), random.choice(last) def fill_profile_js(page, password: str, timeout: int = 120) -> dict: """Fill given/family name + password and submit after Turnstile is ready.""" given_name, family_name = gen_name() deadline = time.time() + timeout form_filled = False while time.time() < deadline: state = page.run_js( r""" const givenName = arguments[0]; const familyName = arguments[1]; const password = arguments[2]; function isVisible(node) { if (!node) return false; const style = window.getComputedStyle(node); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false; const rect = node.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } function pick(sel) { return Array.from(document.querySelectorAll(sel)).find((n) => isVisible(n) && !n.disabled && !n.readOnly) || null; } function setVal(input, value) { if (!input) return false; input.focus(); input.click(); const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; const tracker = input._valueTracker; if (tracker) tracker.setValue(''); if (setter) setter.call(input, value); else input.value = value; input.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: value, inputType: 'insertText' })); input.dispatchEvent(new InputEvent('input', { bubbles: true, data: value, inputType: 'insertText' })); input.dispatchEvent(new Event('change', { bubbles: true })); input.blur(); return String(input.value || '').trim() === String(value || '').trim(); } const givenInput = pick('input[data-testid="givenName"], input[name="givenName"], input[autocomplete="given-name"]'); const familyInput = pick('input[data-testid="familyName"], input[name="familyName"], input[autocomplete="family-name"]'); const passwordInput = pick('input[data-testid="password"], input[name="password"], input[type="password"], input[autocomplete="new-password"]'); if (!givenInput || !familyInput || !passwordInput) return 'not-ready'; const ok1 = setVal(givenInput, givenName); const ok2 = setVal(familyInput, familyName); const ok3 = setVal(passwordInput, password); if (!ok1 || !ok2 || !ok3) return 'fill-failed'; const cfInput = document.querySelector('input[name="cf-turnstile-response"]'); const cfPresent = !!cfInput || !!document.querySelector('iframe[src*="turnstile"], div.cf-turnstile, [data-sitekey]'); if (cfPresent) { const token = String((cfInput && cfInput.value) || '').trim(); if (token.length < 80) return 'wait-cf:' + token.length; } const buttons = Array.from(document.querySelectorAll('button[type="submit"], button')).filter((n) => isVisible(n) && !n.disabled && n.getAttribute('aria-disabled') !== 'true'); const submitBtn = buttons.find((n) => { const t = (n.innerText || n.textContent || '').replace(/\s+/g, '').toLowerCase(); return t.includes('完成注册') || t.includes('创建账户') || t.includes('signup') || t.includes('signup') || t.includes('createaccount') || t.includes('continue') || t.includes('confirm'); }); if (!submitBtn) return 'filled-no-submit'; submitBtn.click(); return 'submitted'; """, given_name, family_name, password, ) if state == "not-ready": time.sleep(0.8) continue if state == "fill-failed": time.sleep(0.5) continue if isinstance(state, str) and state.startswith("wait-cf"): form_filled = True bypass_turnstile(page) time.sleep(1.5) continue if state == "filled-no-submit": form_filled = True time.sleep(1) continue if state == "submitted": return {"ok": True, "given_name": given_name, "family_name": family_name, "password": password} time.sleep(0.8) return {"ok": False, "error": "timeout waiting profile/turnstile", "given_name": given_name, "family_name": family_name} def wait_for_sso(page, timeout: int = 180) -> str: deadline = time.time() + timeout last_click = 0.0 while time.time() < deadline: # periodically re-click final submit if still on complete-registration page now = time.time() if now - last_click >= 3: try: page.run_js(r""" function isVisible(node) { if (!node) return false; const style = window.getComputedStyle(node); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false; const rect = node.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } const titleHit = !!Array.from(document.querySelectorAll('h1,h2,div,span')).find((el) => { const t = (el.textContent || '').replace(/\s+/g, ''); return t.includes('完成注册') || t.toLowerCase().includes('complete'); }); if (!titleHit) return false; const cfInput = document.querySelector('input[name="cf-turnstile-response"]'); if (cfInput && String(cfInput.value || '').trim().length < 80) return false; const buttons = Array.from(document.querySelectorAll('button[type="submit"], button')).filter((n) => isVisible(n) && !n.disabled); const submitBtn = buttons.find((n) => { const t = (n.innerText || n.textContent || '').replace(/\s+/g, '').toLowerCase(); return t.includes('完成注册') || t.includes('创建账户') || t.includes('signup') || t.includes('signup') || t.includes('createaccount'); }); if (submitBtn) { submitBtn.click(); return true; } return false; """) except Exception: pass last_click = now try: cookies = None for getter in ( lambda: page.cookies(all_domains=True, all_info=True), lambda: page.cookies(all_domains=True), lambda: page.cookies(), ): try: cookies = getter() if cookies: break except TypeError: continue except Exception: continue if isinstance(cookies, list): for c in cookies: if not isinstance(c, dict): continue if str(c.get("name", "")).strip() == "sso" and c.get("value"): return str(c.get("value")).strip() except Exception: pass time.sleep(1) return "" def maybe_export_cpa(email: str, password: str, sso: str, page) -> str | None: """Best-effort CPA export using project cpa_export + config.json.""" cfg_path = Path(__file__).resolve().parent / "config.json" if not cfg_path.is_file(): return None try: cfg = json.loads(cfg_path.read_text(encoding="utf-8")) except Exception: return None if not cfg.get("cpa_export_enabled", True): return None try: import cpa_export cookies = cpa_export.export_cookies_from_page(page) result = cpa_export.export_cpa_xai_for_account( email, password, page=None, cookies=cookies, sso=sso, config=cfg, log_callback=lambda m: print(f"[cpa] {m}"), ) if result.get("ok") and result.get("path"): return str(result["path"]) except Exception as exc: print(f"[!] CPA export exception: {exc}") return None def do_register(alias: str, password: str, imap_user: str, imap_password: str, headless: bool = False, proxy: str = "") -> dict: email = alias print(f"[*] Registering: {email}") opts = create_browser_options(headless=headless, proxy=proxy) browser = None imap_conn = None try: browser = Chromium(opts) page = wait_for_tab(browser, timeout=20) if page is None: raise Exception("无法获取浏览器标签页") page.get(SIGNUP_URL) page.wait.doc_loaded(timeout=20) time.sleep(2) print(f"[+] Page loaded: {page.url}") # 尝试绕过 Turnstile bypass_turnstile(page) time.sleep(1) # 点击"使用邮箱注册"按钮 if not click_email_signup_button(page, timeout=10): print("[!] No email signup button found, continuing anyway...") time.sleep(2) # 填写邮箱 if not fill_email_js(page, email): raise Exception("未找到邮箱输入框或写入失败") print(f"[+] Email submitted: {email}") time.sleep(0.8) # 点击继续 if not click_continue_button(page): raise Exception("未找到注册按钮") print(f"[+] Continue clicked") time.sleep(3) # 连接 IMAP 准备取验证码 print(f"[*] Connecting IMAP {imap_user}...") imap_conn = imap_login(imap_user, imap_password) # 等待验证码 print(f"[*] Waiting for code...") code = fetch_code(imap_conn, email, timeout=CODE_TIMEOUT, poll=CODE_POLL) if not code: raise Exception(f"未收到验证码: {email}") # 填写验证码 code_input = page.ele( "css:input[data-input-otp='true'], input[name='code'], input[autocomplete='one-time-code']", timeout=5, ) if code_input: code_input.clear() code_input.input(code) else: boxes = page.eles("css:input[inputmode='numeric']") for i, ch in enumerate(code): if i < len(boxes): boxes[i].input(ch) time.sleep(0.1) print(f"[+] Code filled: {code}") time.sleep(0.5) # 点击确认 page.run_js(""" const btns = Array.from(document.querySelectorAll('button')); const btn = btns.find(b => { const t = (b.innerText || b.textContent || '').replace(/\\s+/g, '').toLowerCase(); return t.includes('continue') || t.includes('confirm') || t.includes('下一步'); }); if (btn) btn.click(); """) time.sleep(3) # 验证码后通常进入资料页:姓名 + 密码 + Turnstile print("[*] Filling profile (name/password)...") profile = fill_profile_js(page, password, timeout=120) if not profile.get("ok"): raise Exception(f"资料填写失败: {profile.get('error') or 'unknown'}") print(f"[+] Profile filled: {profile.get('given_name')} {profile.get('family_name')}") # 等待 sso cookie(最终提交后) print("[*] Waiting for sso cookie...") sso_cookie = wait_for_sso(page, timeout=180) current_url = page.url print(f"[+] Current URL: {current_url}") if not sso_cookie: raise Exception("未获取到 sso cookie") save_account(email, password, sso_cookie) result = {"ok": True, "email": email, "password": password, "sso": sso_cookie, "url": current_url} # 可选:协议优先 CPA mint if os.environ.get("CPA_EXPORT", "1") not in ("0", "false", "False"): try: cpa_path = maybe_export_cpa(email, password, sso_cookie, page) if cpa_path: result["cpa_path"] = cpa_path print(f"[+] CPA auth: {cpa_path}") except Exception as cpa_exc: print(f"[!] CPA export failed: {cpa_exc}") result["cpa_error"] = str(cpa_exc) return result except Exception as e: print(f"[-] Failed: {e}") try: if browser is not None: tab = wait_for_tab(browser, timeout=5) if tab is not None: tab.get_screenshot(path="register_error.png", full_page=False) print(f"[!] Screenshot: register_error.png") except Exception: pass return {"ok": False, "error": str(e), "email": email} finally: if imap_conn: try: imap_conn.close() except Exception: pass try: imap_conn.logout() except Exception: pass if browser is not None: try: browser.quit() except Exception: pass def main(): parser = argparse.ArgumentParser(description="Grok 自动注册(Cloudflare catch-all + Gmail IMAP)") parser.add_argument("--count", type=int, default=1) parser.add_argument("--threads", type=int, default=1) parser.add_argument("--headless", action="store_true") parser.add_argument("--proxy", default=None) parser.add_argument("--domain", default=DOMAIN) parser.add_argument("--imap-user", default=None) parser.add_argument("--imap-password", default=None) args = parser.parse_args() env = load_env() proxy = args.proxy or env.get("proxy") or env.get("cpa_proxy") or "" if proxy == "http://127.0.0.1:7890": proxy = "" imap_user = args.imap_user or env.get("GMAIL_USER") or "" imap_password = args.imap_password or env.get("GMAIL_PASSWORD") or "" if not imap_user or not imap_password: print("Error: IMAP credentials required via --imap-user/--imap-password or GMAIL_USER/GMAIL_PASSWORD") return 1 print(f"[*] Domain: {args.domain}") print(f"[*] Proxy: {proxy or 'direct'}") print(f"[*] IMAP: {imap_user}") print(f"[*] Count: {args.count}, Threads: {args.threads}") tasks = [] for i in range(args.count): tasks.append((i + 1, gen_alias(args.domain), gen_password())) results = [] threads = max(1, min(int(args.threads or 1), 10)) if threads == 1: for idx, alias, password in tasks: print(f"\n=== Account {idx}/{args.count} ===") res = do_register(alias, password, imap_user, imap_password, headless=args.headless, proxy=proxy or None) results.append(res) if idx < args.count: time.sleep(3) else: from concurrent.futures import ThreadPoolExecutor, as_completed print(f"[*] Running with {threads} threads") with ThreadPoolExecutor(max_workers=threads) as ex: futs = { ex.submit( do_register, alias, password, imap_user, imap_password, args.headless, proxy or None ): (idx, alias) for idx, alias, password in tasks } for fut in as_completed(futs): idx, alias = futs[fut] try: res = fut.result() except Exception as exc: res = {"ok": False, "email": alias, "error": str(exc)} results.append(res) status = "OK" if res.get("ok") else "FAIL" print(f"[{status}] #{idx} {res.get('email')} {res.get('error', '')[:80]}") print(f"\n=== Summary ===") ok = sum(1 for r in results if r.get("ok")) print(f"Success: {ok}/{len(results)}") for r in results: status = "OK" if r.get("ok") else "FAIL" extra = r.get("cpa_path") or r.get("error", "") print(f" [{status}] {r.get('email')} - {str(extra)[:100]}") return 0 if ok else 1 if __name__ == "__main__": raise SystemExit(main() or 0)