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

+98
View File
@@ -0,0 +1,98 @@
# Grok xAI 验证码 Email Worker
```
xAI 发信
→ Cloudflare Email Routing(catch-all)
→ 本 Worker:提取验证码写入 KV
→ (可选)forward 到 Gmail 作 IMAP 兜底
→ 注册机 HTTP GET Worker /code?to=alias@domain&key=...
```
## 1. 前置
- 域名 DNS 在 Cloudflare
- 开通 **Email Routing**
- 安装 Node:`npm i -g wrangler` 或用 `npx wrangler`
## 2. 创建 KV + 配置
```bash
cd cf-email-worker
npx wrangler login
npx wrangler kv namespace create GROK_MAIL_CODES
# 把返回的 id 填进 wrangler.toml 的 kv_namespaces.id
```
编辑 `wrangler.toml`,或在 Dashboard → Workers → Settings → Variables 设置:
| 变量 | 含义 |
|------|------|
| `CODE_API_KEY` | 注册机查询密钥(必填) |
| `GMAIL_FORWARD_TO` | 如 `you@gmail.com`,保留 IMAP 兜底 |
| `CODE_TTL_SEC` | KV 过期秒数,默认 600 |
```bash
npx wrangler secret put CODE_API_KEY
# 可选:也可用 vars;secret 更安全
```
## 3. 部署
```bash
npx wrangler deploy
# 记下 workers.dev 地址,例如:
# https://grok-xai-mail-codes.<subdomain>.workers.dev
```
## 4. Email Routing 绑定
Cloudflare Dashboard → **Email** → **Email Routing** → **Routing rules**:
1. 启用 Catch-all
2. Action = **Send to a Worker** → 选择 `grok-xai-mail-codes`
3. 域名 MX 按 Cloudflare 提示配置(若尚未)
> 若 catch-all 已转发到 Gmail,可改为:Worker 处理 + Worker 内 `GMAIL_FORWARD_TO` 再转到 Gmail。
> 不要只转 Gmail 而不进 Worker,否则没有秒级路径。
## 5. 注册机配置
`config.json`:
```json
{
"email_provider": "gmail",
"defaultDomains": "your-domain.com",
"gmail_imap_user": "you@gmail.com",
"gmail_imap_password": "app-password",
"gmail_worker_enabled": true,
"gmail_worker_url": "https://grok-xai-mail-codes.<subdomain>.workers.dev",
"gmail_worker_api_key": "与 CODE_API_KEY 相同",
"gmail_worker_timeout_sec": 25,
"gmail_worker_poll_interval": 0.4,
"gmail_imap_fallback": true
}
```
## 6. 自测
```bash
# 健康检查
curl -sS "https://YOUR_WORKER/health"
# 模拟入库
curl -sS -X POST "https://YOUR_WORKER/ingest?key=YOUR_KEY" \
-H 'content-type: application/json' \
-d '{"to":"test@your-domain.com","code":"ABC-DEF","subject":"ABC-DEF xAI confirmation code"}'
# 取码(consume=1 用后即删)
curl -sS "https://YOUR_WORKER/code?to=test@your-domain.com&key=YOUR_KEY"
```
真实链路:给 `random@your-domain.com` 发一封带 `ABC-DEF xAI confirmation code` 主题的信,几秒内应能 GET 到。
## 7. 安全
- `CODE_API_KEY` 用足够长的随机串
- 不要把 key 提交进 git(写 config.json / .env,二者已在 .gitignore)
- KV 中验证码默认约 10 分钟过期,且 `/code` 默认 consume-once
+11
View File
@@ -0,0 +1,11 @@
{
"name": "grok-xai-mail-codes",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev"
},
"devDependencies": {
"wrangler": "^3.99.0"
}
}
+280
View File
@@ -0,0 +1,280 @@
/**
* grok-xai-mail-codes — hardened for clearer errors (no bare 1101 if possible)
*
* Bindings:
* KV name MUST be: CODES
* Secrets/vars:
* CODE_API_KEY (required)
* GMAIL_FORWARD_TO (optional)
* CODE_TTL_SEC (optional, default 600)
*/
const CODE_PATTERNS = [
/^([A-Z0-9]{3}-[A-Z0-9]{3})\s+xAI/i,
/\b([A-Z0-9]{3}-[A-Z0-9]{3})\b/i,
/verification\s+code[:\s]+(\d{4,8})/i,
/your\s+code[:\s]+(\d{4,8})/i,
/confirm(?:ation)?\s+code[:\s]+(\d{4,8})/i,
/验证码[::\s]+(\d{4,8})/,
];
const KEYWORDS = ["x.ai", "xai", "grok", "verification", "code", "confirm", "验证码", "确认"];
function json(data, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: { "content-type": "application/json; charset=utf-8" },
});
}
function extractCode(text, subject = "") {
const blob = `${subject || ""}\n${text || ""}`;
for (const re of CODE_PATTERNS) {
const m = blob.match(re);
if (m && m[1]) return String(m[1]).trim();
}
return null;
}
function emailsFromList(list) {
if (!list) return [];
const arr = Array.isArray(list) ? list : [list];
const out = [];
for (const item of arr) {
if (!item) continue;
if (typeof item === "string") {
const m = item.match(/[\w.+-]+@[\w.-]+\.\w+/g);
if (m) out.push(...m.map((x) => x.toLowerCase()));
continue;
}
if (item.address) out.push(String(item.address).toLowerCase());
if (item.email) out.push(String(item.email).toLowerCase());
}
return [...new Set(out.filter(Boolean))];
}
async function parseEmail(message) {
const raw = await new Response(message.raw).arrayBuffer();
const bytes = new Uint8Array(raw);
let rawText = "";
try {
rawText = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
} catch {
const n = Math.min(bytes.length, 500000);
let s = "";
for (let i = 0; i < n; i++) s += String.fromCharCode(bytes[i]);
rawText = s;
}
const headerBlob = rawText.split(/\r?\n\r?\n/, 1)[0] || "";
let subject = "";
const subjMatch = headerBlob.match(/^Subject:\s*(.+)$/im);
if (subjMatch) subject = subjMatch[1].trim();
let from = "";
const fromMatch = headerBlob.match(/^From:\s*(.+)$/im);
if (fromMatch) from = fromMatch[1].trim();
const recipients = new Set();
for (const h of ["To", "Cc", "Delivered-To", "X-Original-To", "Envelope-To"]) {
const re = new RegExp(`^${h}:\\s*(.+)$`, "gim");
let m;
while ((m = re.exec(headerBlob))) {
const found = m[1].match(/[\w.+-]+@[\w.-]+\.\w+/g) || [];
for (const e of found) recipients.add(e.toLowerCase());
}
}
try {
for (const e of emailsFromList(message.to)) recipients.add(e);
} catch (_) {}
return {
subject,
from,
recipients: [...recipients],
rawText: rawText.slice(0, 200000),
};
}
function isXaiMail(subject, from, body) {
const blob = `${subject}\n${from}\n${body}`.toLowerCase();
return KEYWORDS.some((k) => blob.includes(k));
}
function kv(env) {
// Accept common mistaken binding names
return env.CODES || env.GROK_MAIL_CODES || env.CODE || null;
}
async function storeCode(env, toEmail, payload) {
const ns = kv(env);
if (!ns || typeof ns.put !== "function") {
throw new Error("KV binding missing: add KV namespace with variable name CODES");
}
const ttl = Math.max(60, parseInt(String(env.CODE_TTL_SEC || "600"), 10) || 600);
const key = `code:${String(toEmail).toLowerCase().trim()}`;
await ns.put(key, JSON.stringify(payload), { expirationTtl: ttl });
try {
const prev = JSON.parse((await ns.get("recent")) || "[]");
prev.unshift({
to: toEmail,
code: payload.code,
ts: payload.ts,
subject: payload.subject,
});
await ns.put("recent", JSON.stringify(prev.slice(0, 30)), { expirationTtl: ttl });
} catch (_) {}
}
function checkKey(request, env, url) {
const key = url.searchParams.get("key") || request.headers.get("x-api-key") || "";
const expected = env.CODE_API_KEY || "";
return Boolean(expected) && key === expected;
}
export default {
async fetch(request, env, ctx) {
try {
const url = new URL(request.url);
const path = url.pathname.replace(/\/+$/, "") || "/";
if (path === "/health") {
const ns = kv(env);
return json({
ok: true,
service: "grok-xai-mail-codes",
kv_bound: Boolean(ns && typeof ns.get === "function"),
has_api_key: Boolean(env.CODE_API_KEY),
});
}
if (!checkKey(request, env, url)) {
return json({ error: "unauthorized" }, 401);
}
if (path === "/code" && request.method === "GET") {
const to = (url.searchParams.get("to") || url.searchParams.get("target") || "")
.trim()
.toLowerCase();
if (!to) return json({ error: "missing to" }, 400);
const ns = kv(env);
if (!ns || typeof ns.get !== "function") {
return json(
{
error: "kv not bound",
hint: "Workers → grok-xai-mail-codes → Settings → Bindings → add KV, Variable name must be CODES",
},
500,
);
}
const raw = await ns.get(`code:${to}`);
if (!raw) return json({ to, error: "not found" }, 404);
let data;
try {
data = JSON.parse(raw);
} catch {
return json({ to, error: "bad kv value" }, 500);
}
const consume = (url.searchParams.get("consume") || "1") !== "0";
if (consume) {
try {
await ns.delete(`code:${to}`);
} catch (_) {}
}
return json({
ok: true,
to,
code: data.code,
subject: data.subject || "",
from: data.from || "",
ts: data.ts || 0,
source: "worker-kv",
});
}
if (path === "/recent" && request.method === "GET") {
const ns = kv(env);
if (!ns) return json({ items: [], kv_bound: false });
const raw = await ns.get("recent");
return json({ items: raw ? JSON.parse(raw) : [], kv_bound: true });
}
if (path === "/ingest" && request.method === "POST") {
let body;
try {
body = await request.json();
} catch {
return json({ error: "invalid json" }, 400);
}
const to = String(body.to || body.target || "").toLowerCase().trim();
const code = String(body.code || "").trim();
if (!to || !code) return json({ error: "missing to/code" }, 400);
try {
await storeCode(env, to, {
code,
subject: body.subject || `${code} xAI confirmation code`,
from: body.from || "",
ts: Date.now(),
});
} catch (e) {
return json({ error: String(e && e.message ? e.message : e) }, 500);
}
return json({ ok: true, to, code });
}
return json({ error: "not found" }, 404);
} catch (e) {
return json(
{
error: "worker exception",
message: String(e && e.message ? e.message : e),
},
500,
);
}
},
async email(message, env, ctx) {
let parsed;
try {
parsed = await parseEmail(message);
} catch (_) {
const forwardTo = String(env.GMAIL_FORWARD_TO || "").trim();
if (forwardTo) {
try {
await message.forward(forwardTo);
} catch (_) {}
}
return;
}
const { subject, from, recipients, rawText } = parsed;
const code = extractCode(rawText, subject);
const interesting = isXaiMail(subject, from, rawText) || !!code;
if (interesting && code && recipients.length) {
const payload = { code, subject, from, ts: Date.now() };
try {
await Promise.all(recipients.map((to) => storeCode(env, to, payload)));
} catch (_) {
// still forward
}
}
const forwardTo = String(env.GMAIL_FORWARD_TO || "").trim();
if (forwardTo) {
try {
await message.forward(forwardTo);
} catch (e) {
try {
message.setReject?.(`forward failed: ${e}`);
} catch (_) {}
}
}
},
};
+26
View File
@@ -0,0 +1,26 @@
# Cloudflare Email Worker for xAI verification codes
# Deploy: cd cf-email-worker && npx wrangler deploy
#
# Required:
# 1) Cloudflare Email Routing enabled for your domain
# 2) Routing rule: catch-all → this Worker
# 3) Optional Gmail forward destination (keeps IMAP fallback)
name = "grok-xai-mail-codes"
main = "src/worker.js"
compatibility_date = "2024-11-01"
# KV namespace for short-lived codes (create once, then bind id)
# npx wrangler kv namespace create GROK_MAIL_CODES
[[kv_namespaces]]
binding = "CODES"
id = "REPLACE_WITH_KV_NAMESPACE_ID"
preview_id = "REPLACE_WITH_KV_NAMESPACE_ID"
[vars]
# Shared secret for GET /code (also set via dashboard secrets if preferred)
# CODE_API_KEY = "change-me"
# Forward original mail to Gmail so IMAP fallback still works
# GMAIL_FORWARD_TO = "you@gmail.com"
# Code TTL seconds in KV
CODE_TTL_SEC = "600"