// @ts-check // 設定値をグループ化 const CONFIG = { BLOCKLIST: ["hacker@example.com", "spammer@example.com"], FORWARD_TO: "mymail@example.com", }; const WEBHOOK = { URL: "https://discord.com/api/webhooks/xxx/xxx", ICON: "https://git.v-sli.me/HidemaruOwO/email-worker/raw/branch/main/assets/webhook_icon.jpg", SENDER: "contact@trendcreate.net", }; const ICONS = { AUTHOR: "https://git.v-sli.me/HidemaruOwO/email-worker/raw/branch/main/assets/email_icon.png", FOOTER: "https://git.v-sli.me/HidemaruOwO/email-worker/raw/branch/main/assets/cloudflare_icon.ico", }; export default { async email(message, env, ctx) { if (CONFIG.BLOCKLIST.includes(message.from)) { message.setReject("Address is blocked"); return; } try { const [notifyResult] = await Promise.allSettled([ notify(message), message.forward(CONFIG.FORWARD_TO), ]); if (notifyResult.status === "fulfilled" && !notifyResult.value.ok) { console.error("Discord通知失敗:", await notifyResult.value.text()); } } catch (err) { console.error("処理エラー:", err); } }, }; async function notify(message) { const from = message.headers.get("from") || "名前なし"; const subject = message.headers.get("subject") || "件名なし"; const date = message.headers.get("date"); const text = await getMailText(message); const payload = { username: WEBHOOK.SENDER, avatar_url: WEBHOOK.ICON, content: `**${from}**からお問い合わせメールが届いております。`, embeds: [ { author: { name: from, icon_url: ICONS.AUTHOR }, title: `**${subject}**`, description: text || "本文はありません。", timestamp: date ? new Date(date).toISOString() : new Date().toISOString(), footer: { text: "Powered by Cloudflare Worker and Email Routing", icon_url: ICONS.FOOTER, }, }, ], }; return fetch(WEBHOOK.URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }).catch((err) => { console.error("Webhook送信エラー:", err); throw err; }); } async function getMailText(message) { if (!message.raw) return ""; try { const buffer = await streamToArrayBuffer(message.raw); return parseEmail(buffer); } catch (err) { console.error("メール解析エラー:", err); return `エラー: ${err.message}`; } } async function streamToArrayBuffer(stream) { const reader = stream.getReader(); const chunks = []; let totalSize = 0; while (true) { const { done, value } = await reader.read(); if (done) break; if (value) { chunks.push(value); totalSize += value.length; } } if (chunks.length === 1) { return chunks[0]; } const result = new Uint8Array(totalSize); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } return result; } function parseEmail(buffer) { const text = new TextDecoder().decode(buffer); const headerEnd = text.indexOf("\r\n\r\n"); if (headerEnd === -1) return ""; const header = text.substring(0, headerEnd); const body = text.substring(headerEnd + 4); const boundaryMatch = header.match(/boundary="?([^"\r\n]+)"?/i); if (!boundaryMatch) return body.trim(); const boundary = `--${boundaryMatch[1]}`; const parts = body.split(boundary); for (const part of parts) { if (!part.includes("Content-Type: text/plain")) continue; const partHeaderEnd = part.indexOf("\r\n\r\n"); if (partHeaderEnd === -1) continue; const partHeader = part.substring(0, partHeaderEnd); const partBody = part.substring(partHeaderEnd + 4).trim(); if (!partBody) continue; if (partHeader.includes("Content-Transfer-Encoding: base64")) { try { const cleanBase64 = partBody.replace(/[\r\n\s]/g, ""); return decodeBase64(cleanBase64); } catch { continue; } } return partBody; } return ""; } function decodeBase64(base64) { try { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return new TextDecoder().decode(bytes); } catch (e) { return ""; } }