Files
ni-mail/worker.js
2026-03-22 17:56:51 +08:00

116 lines
3.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import PostalMime from "postal-mime";
const MAX_MAILS = 50;
const STORAGE_KEY = "inbox";
// ─── 收信 Handler ────────────────────────────────────────────────
export async function email(message, env) {
const parser = new PostalMime();
const raw = await streamToArrayBuffer(message.raw);
const parsed = await parser.parse(raw);
const mail = {
id: crypto.randomUUID(),
receivedAt: new Date().toISOString(),
from: message.from,
to: message.to,
subject: parsed.subject ?? "(no subject)",
text: parsed.text ?? "",
html: parsed.html ?? "",
// 只保留附件 metadata不存 base64 內容,避免撞 KV 25MB 限制
attachments: (parsed.attachments ?? []).map((a) => ({
filename: a.filename ?? "unknown",
mimeType: a.mimeType ?? "application/octet-stream",
size: a.content?.byteLength ?? 0,
})),
};
const existing = await loadInbox(env);
const updated = [mail, ...existing].slice(0, MAX_MAILS);
await env.MAIL_KV.put(STORAGE_KEY, JSON.stringify(updated));
}
// ─── HTTP API Handler ─────────────────────────────────────────────
export default {
email,
async fetch(request, env) {
if (request.headers.get("X-Auth-Key") !== env.AUTH_KEY) {
return json({ error: "unauthorized" }, 401);
}
const url = new URL(request.url);
const mails = await loadInbox(env);
// GET /latest → 最新一封完整郵件
if (url.pathname === "/latest") {
return mails.length ? json(mails[0]) : json({ error: "no mail" }, 404);
}
// GET /mails?limit=10 → 最近 N 封列表(不含正文)
if (url.pathname === "/mails" && request.method === "GET") {
const limit = Math.min(
parseInt(url.searchParams.get("limit") ?? "10"),
MAX_MAILS
);
const list = mails
.slice(0, limit)
.map(({ id, receivedAt, from, to, subject, attachments }) => ({
id,
receivedAt,
from,
to,
subject,
attachments,
}));
return json(list);
}
// DELETE /mails → 清空收件匣
if (url.pathname === "/mails" && request.method === "DELETE") {
await env.MAIL_KV.delete(STORAGE_KEY);
return json({ ok: true });
}
// GET /mail/:id → 單封完整內容
const match = url.pathname.match(/^\/mail\/(.+)$/);
if (match) {
const found = mails.find((x) => x.id === match[1]);
return found ? json(found) : json({ error: "not found" }, 404);
}
return json({ error: "unknown route" }, 404);
},
};
// ─── 工具函數 ─────────────────────────────────────────────────────
async function loadInbox(env) {
const raw = await env.MAIL_KV.get(STORAGE_KEY);
return raw ? JSON.parse(raw) : [];
}
function json(data, status = 200) {
return new Response(JSON.stringify(data, null, 2), {
status,
headers: { "Content-Type": "application/json" },
});
}
async function streamToArrayBuffer(stream) {
const reader = stream.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const total = chunks.reduce((n, c) => n + c.byteLength, 0);
const buf = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
buf.set(chunk, offset);
offset += chunk.byteLength;
}
return buf.buffer;
}