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:
1 parent
187792d6be
commit
55f56d27c9
19 files changed
+1225
-85
No files matched your search
@@ -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 (_) {}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user