Add Gmail+Email Worker hybrid mail path and private runtime config

Prefer Cloudflare Worker KV for verification codes with optional short IMAP
fallback disabled for private deploy; track config.json/.env for private repo.
Also add worker project, protocol mint backoff, and fail-fast Turnstile/mail
timeouts from batch runs.
This commit is contained in:
chaos committed 2026-07-11 23:21:23 +08:00
1 parent 187792d6be
commit 55f56d27c9
19 files changed
+1225 -85

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Helper: create KV + print next steps for Email Worker
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT/cf-email-worker"
if ! command -v npx >/dev/null 2>&1; then
echo "需要 Node.js / npx。先安装: https://nodejs.org/"
exit 1
fi
echo "[1/4] wrangler login (如已登录可跳过)"
npx wrangler login || true
echo "[2/4] 创建 KV namespace GROK_MAIL_CODES"
OUT=$(npx wrangler kv namespace create GROK_MAIL_CODES 2>&1 || true)
echo "$OUT"
ID=$(echo "$OUT" | sed -n 's/.*id *= *"\([^"]*\)".*/\1/p' | head -1)
if [[ -z "${ID}" ]]; then
ID=$(echo "$OUT" | grep -oE '[a-f0-9]{32}' | head -1 || true)
fi
if [[ -n "${ID}" ]]; then
echo "检测到 KV id=$ID — 写入 wrangler.toml"
if grep -q 'REPLACE_WITH_KV_NAMESPACE_ID' wrangler.toml; then
sed -i "s/REPLACE_WITH_KV_NAMESPACE_ID/${ID}/g" wrangler.toml
fi
else
echo "未能自动解析 KV id,请手动填 wrangler.toml"
fi
echo "[3/4] 设置 CODE_API_KEY secret"
echo "将提示你输入密钥(与 config gmail_worker_api_key 一致)"
npx wrangler secret put CODE_API_KEY
if [[ -n "${GMAIL_FORWARD_TO:-}" ]]; then
echo "[3b] 设置 GMAIL_FORWARD_TO=$GMAIL_FORWARD_TO"
printf '%s' "$GMAIL_FORWARD_TO" | npx wrangler secret put GMAIL_FORWARD_TO
else
echo "[3b] 跳过 GMAIL_FORWARD_TO(可 export GMAIL_FORWARD_TO=you@gmail.com 后重跑)"
fi
echo "[4/4] deploy"
npx wrangler deploy
echo
echo "完成。接下来:"
echo " 1) Email Routing catch-all → 绑定本 Worker"
echo " 2) config.json 设置:"
echo " gmail_worker_enabled=true"
echo " gmail_worker_url=https://<worker>.workers.dev"
echo " gmail_worker_api_key=<你的 CODE_API_KEY>"
echo " 3) 测试: python scripts/test_gmail_worker.py --to test@yourdomain.com"
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Test Cloudflare Email Worker code API (and optional local hybrid path)."""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from pathlib import Path
from urllib.parse import urlencode
from urllib.request import Request, urlopen
ROOT = Path(__file__).resolve().parents[1]
def load_cfg():
p = ROOT / "config.json"
if p.is_file():
return json.loads(p.read_text(encoding="utf-8"))
return {}
def main():
cfg = load_cfg()
ap = argparse.ArgumentParser(description="Test gmail Email Worker /code API")
ap.add_argument("--url", default=cfg.get("gmail_worker_url") or os.getenv("GMAIL_WORKER_URL") or "")
ap.add_argument("--key", default=cfg.get("gmail_worker_api_key") or os.getenv("GMAIL_WORKER_API_KEY") or "")
ap.add_argument("--to", required=True, help="alias@domain to query")
ap.add_argument("--timeout", type=float, default=20)
ap.add_argument("--interval", type=float, default=0.5)
ap.add_argument("--ingest-demo", action="store_true", help="POST a fake code then GET it")
ap.add_argument("--no-consume", action="store_true")
args = ap.parse_args()
base = (args.url or "").rstrip("/")
key = args.key or ""
if not base or not key:
print("需要 --url/--key 或 config.json 的 gmail_worker_url / gmail_worker_api_key")
return 1
def get(path, params=None):
q = dict(params or {})
q["key"] = key
url = f"{base}{path}?{urlencode(q)}"
req = Request(url, headers={"x-api-key": key})
with urlopen(req, timeout=10) as r:
return r.status, json.loads(r.read().decode())
def post(path, body):
url = f"{base}{path}?{urlencode({'key': key})}"
data = json.dumps(body).encode()
req = Request(url, data=data, headers={"content-type": "application/json", "x-api-key": key}, method="POST")
with urlopen(req, timeout=10) as r:
return r.status, json.loads(r.read().decode())
st, health = get("/health")
print("health", st, health)
if args.ingest_demo:
code = "AB1-CD2"
st, body = post("/ingest", {"to": args.to, "code": code, "subject": f"{code} xAI confirmation code"})
print("ingest", st, body)
deadline = time.time() + args.timeout
while time.time() < deadline:
try:
st, body = get(
"/code",
{"to": args.to.lower(), "consume": "0" if args.no_consume else "1"},
)
print("code", st, body)
if st == 200 and body.get("code"):
return 0
except Exception as e:
print("wait", e)
time.sleep(args.interval)
print("timeout — 若是真实邮件,检查 Email Routing 是否绑到 Worker")
return 2
if __name__ == "__main__":
sys.exit(main())