// @ts-check // いじりやすいようにここに変数を集約させたい const BLOCKLIST = ["hacker@example.com", "spammer@example.com"]; const FORWARD_TO = "mymail@example.com"; const WEBHOOK_URL = "https://discord.com/api/webhooks/xxx/xxx"; // trendcreate icon const WEBHOOK_ICON = "https://git.v-sli.me/HidemaruOwO/email-worker/raw/branch/main/assets/webhook_icon.jpg"; // mail letter icon const AUTHOR_ICON = "https://git.v-sli.me/HidemaruOwO/email-worker/raw/branch/main/assets/email_icon.png"; // cloudflare icon const FOOTER_ICON = "https://git.v-sli.me/HidemaruOwO/email-worker/raw/branch/main/assets/cloudflare_icon.ico"; export default { async email(message, env, ctx) { if (BLOCKLIST.includes(message.from)) { message.setReject("Address is blocked"); return; } try { const [result] = await Promise.all([ notify(message), message.forward(FORWARD_TO), ]); if (!result.ok) { console.log(await result.text()); console.log(await result.json()); return; } } catch (err) { console.error("処理エラー:", err); } }, }; // async function notify(from, subject, text, date) { 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: "contact@trendcreate.net", avatar_url: WEBHOOK_ICON, content: `**${from}**からお問い合わせメールが届いております。`, embeds: [ { author: { name: from || "名前なし", icon_url: AUTHOR_ICON }, title: `**${subject || "件名なし"}**`, description: text || "本文はありません。", timestamp: new Date(date).toISOString(), footer: { text: "Powered by Cloudflare Worker and Email Routing", icon_url: FOOTER_ICON, }, }, ], }; // console.log(JSON.stringify(payload)) try { const result = await fetch(WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); return result; } catch (err) { console.log(err); throw new Error(err); } } async function getMailText(message) { try { const buf = await readStream(message.raw); if (buf === "NO_CONTENT") { return ""; } return parseEmail(buf); } catch (err) { return `エラー: ${err.message}`; } } async function readStream(stream) { if (typeof stream === "undefined") { return "NO_CONTENT"; } const chunks = []; const reader = stream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); } const size = chunks.reduce((sum, chunk) => sum + chunk.length, 0); const result = new Uint8Array(size); let pos = 0; for (const chunk of chunks) { result.set(chunk, pos); pos += 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")) { const isBase64 = part.includes("Content-Transfer-Encoding: base64"); const partBody = part.split("\r\n\r\n")[1]?.trim(); if (!partBody) continue; if (isBase64) { try { const cleanBase64 = partBody.replace(/[\r\n\s]/g, ""); return decodeBase64(cleanBase64); } catch { continue; } } else { return partBody; } } } return ""; } function decodeBase64(base64) { 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); }