email-worker/worker.js

168 lines
4.2 KiB
JavaScript

// @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);
}