// いじりやすいようにここに変数を集約させたい 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 [text] = await Promise.all([ getMailText(message), message.forward(FORWARD_TO) ]); const result = await notify(message.headers.get("from"), message.headers.get('subject'), text, message.headers.get("date")); 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) { 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) } return; } 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); }