// Temp Mail Worker + Dashboard + Cron Clean + Gmail-style HTML Render
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const method = request.method;
const path = url.pathname;
if (path === "/" && method === "GET") {
return new Response(renderPage(), {
headers: { "Content-Type": "text/html; charset=UTF-8" }
});
}
if (path === "/events" && method === "GET") {
const token = url.searchParams.get("token") || "";
const account = verifyToken(token);
if (!account) return json({error: "Invalid token"}, 401);
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
let lastCount = 0;
const sendEvent = async () => {
try {
const { results } = await env.MAIL_DB.prepare(
"SELECT COUNT(*) as count FROM messages WHERE account_id = ? AND is_deleted = 0"
).bind(account.id).all();
const currentCount = results[0]?.count || 0;
if (currentCount !== lastCount) {
lastCount = currentCount;
controller.enqueue(encoder.encode(`data: ${JSON.stringify({count: currentCount})}\n\n`));
}
} catch (e) {}
};
await sendEvent();
const interval = setInterval(sendEvent, 3000);
ctx.waitUntil(new Promise(() => {}));
}
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive"
}
});
}
if (path === "/domains" && method === "GET") {
const { results } = await env.MAIL_DB.prepare("SELECT * FROM domains WHERE is_active = 1").all();
return json({"hydra:member": results.map(r => ({domain: r.domain, id: r.id, isActive: true, isPrivate: false, createdAt: r.created_at, updatedAt: r.created_at}))});
}
if (path === "/accounts" && method === "POST") {
const body = await request.json().catch(() => ({}));
const address = body.address;
const password = body.password || "Password123!";
if (!address) return json({error: "Address required"}, 400);
const accountId = crypto.randomUUID().replace(/-/g, '').substring(0, 24);
const now = new Date().toISOString();
const expires = new Date(Date.now() + 24*60*60*1000).toISOString();
try {
await env.MAIL_DB.prepare("INSERT INTO accounts (id, address, password, created_at, updated_at, expires_at) VALUES (?, ?, ?, ?, ?, ?)")
.bind(accountId, address, password, now, now, expires).run();
return json({id: accountId, address, quota: 40000000, used: 0, createdAt: now, expiresAt: expires}, 201);
} catch (e) {
return json({error: "Address already exists"}, 409);
}
}
if (path === "/token" && method === "POST") {
const body = await request.json().catch(() => ({}));
const address = body.address;
const password = body.password || "Password123!";
const { results } = await env.MAIL_DB.prepare("SELECT * FROM accounts WHERE address = ?").bind(address).all();
if (results.length === 0) return json({error: "Account not found"}, 404);
const account = results[0];
if (account.password !== password) return json({error: "Invalid password"}, 401);
const token = btoa(JSON.stringify({accountId: account.id, address: account.address, createdAt: Date.now()}));
return json({token, id: account.id});
}
if (path === "/messages" && method === "GET") {
const token = (request.headers.get("Authorization") || "").replace("Bearer ", "");
const account = verifyToken(token);
if (!account) return json({error: "Invalid token"}, 401);
const { results } = await env.MAIL_DB.prepare("SELECT * FROM messages WHERE account_id = ? AND is_deleted = 0 ORDER BY created_at DESC LIMIT 20").bind(account.id).all();
return json({
"hydra:member": results.map(m => {
const parsed = parseEmailContent(m.text_content || "", m.html_content || "");
return {
id: m.id, accountId: m.account_id,
from: {address: m.from_address, name: m.from_name},
to: [{address: m.to_address, name: ""}],
subject: m.subject, intro: parsed.cleanText.substring(0, 100),
seen: m.seen === 1, isDeleted: false,
hasAttachments: m.has_attachments === 1, size: m.size,
createdAt: m.created_at, updatedAt: m.updated_at,
otpCode: parsed.otpCode
};
}),
"hydra:totalItems": results.length
});
}
const msgMatch = path.match(/^\/messages\/([a-zA-Z0-9]+)$/);
if (msgMatch && method === "GET") {
const msgId = msgMatch[1];
const token = (request.headers.get("Authorization") || "").replace("Bearer ", "");
const account = verifyToken(token);
if (!account) return json({error: "Invalid token"}, 401);
const { results } = await env.MAIL_DB.prepare("SELECT * FROM messages WHERE id = ? AND account_id = ?").bind(msgId, account.id).all();
if (results.length === 0) return json({error: "Message not found"}, 404);
const m = results[0];
await env.MAIL_DB.prepare("UPDATE messages SET seen = 1, updated_at = ? WHERE id = ?").bind(new Date().toISOString(), msgId).run();
const parsed = parseEmailContent(m.text_content || "", m.html_content || "");
return json({
id: m.id, accountId: m.account_id,
from: {address: m.from_address, name: m.from_name},
to: [{address: m.to_address, name: ""}],
subject: m.subject, intro: parsed.cleanText.substring(0, 100),
text: parsed.cleanText,
html: parsed.decodedHtml || m.html_content || "",
rawText: m.text_content || "",
rawHtml: m.html_content || "",
seen: true, isDeleted: false,
hasAttachments: m.has_attachments === 1, size: m.size,
createdAt: m.created_at, updatedAt: m.updated_at,
otpCode: parsed.otpCode, verificationLinks: parsed.links
});
}
if (msgMatch && method === "DELETE") {
const msgId = msgMatch[1];
const token = (request.headers.get("Authorization") || "").replace("Bearer ", "");
const account = verifyToken(token);
if (!account) return json({error: "Invalid token"}, 401);
await env.MAIL_DB.prepare("UPDATE messages SET is_deleted = 1 WHERE id = ? AND account_id = ?").bind(msgId, account.id).run();
return json({success: true});
}
if (path === "/receive" && method === "POST") {
const body = await request.json().catch(() => ({}));
const result = await saveEmail(env, ctx, body);
if (!result.success) return json({error: result.error}, result.status);
return json(result.message, 201);
}
if (path === "/webhook" && method === "POST") {
const body = await request.json().catch(() => ({}));
const result = await saveEmail(env, ctx, {to: body.to, from: body.from, fromName: body.from_name || body.fromName, subject: body.subject, text: body.text, html: body.html, webhookUrl: body.webhook_url});
if (!result.success) return json({error: result.error}, result.status);
return json({success: true});
}
if (path === "/me" && method === "GET") {
const token = (request.headers.get("Authorization") || "").replace("Bearer ", "");
const account = verifyToken(token);
if (!account) return json({error: "Invalid token"}, 401);
const { results } = await env.MAIL_DB.prepare("SELECT * FROM accounts WHERE id = ?").bind(account.id).all();
if (results.length === 0) return json({error: "Account not found"}, 404);
const a = results[0];
return json({id: a.id, address: a.address, quota: a.quota, used: a.used, createdAt: a.created_at, expiresAt: a.expires_at});
}
return json({error: "Not found"}, 404);
},
async scheduled(event, env, ctx) {
const now = new Date().toISOString();
ctx.waitUntil(Promise.all([
env.MAIL_DB.prepare("DELETE FROM messages WHERE expires_at < ? OR is_deleted = 1").bind(now).run(),
env.MAIL_DB.prepare("DELETE FROM accounts WHERE expires_at < ?").bind(now).run()
]));
},
async email(message, env, ctx) {
try {
const from = message.from;
const to = message.to;
const subject = message.headers.get("subject") || "";
const rawEmail = await new Response(message.raw).text();
const parsed = parseRawEmail(rawEmail);
await saveEmail(env, ctx, {to, from, fromName: "", subject, text: parsed.text, html: parsed.html});
} catch (e) {}
}
};
function json(data, status = 200) {
return new Response(JSON.stringify(data), {status, headers: {"Content-Type": "application/json"}});
}
function verifyToken(token) {
try {
const d = JSON.parse(atob(token));
return {id: d.accountId, address: d.address};
} catch (e) { return null; }
}
function decodeQuotedPrintable(str) {
if (!str) return "";
return str
.replace(/=\r?\n/g, '')
.replace(/=([0-9A-Fa-f]{2})/g, (match, hex) => String.fromCharCode(parseInt(hex, 16)))
.replace(/=3D/g, '=')
.replace(/=20/g, ' ')
.replace(/=0A/g, '\n')
.replace(/=C2=A9/g, '©')
.replace(/=E2=80=99/g, '\'')
.replace(/=E2=80=9C/g, '"')
.replace(/=E2=80=9D/g, '"')
.replace(/=E2=80=93/g, '-')
.replace(/=E2=80=94/g, '—')
.replace(/=C2=AB/g, '«')
.replace(/=C2=BB/g, '»')
.replace(/=C2=A0/g, ' ')
.replace(/=C3=A9/g, 'é')
.replace(/=C3=A8/g, 'è')
.replace(/=C3=A7/g, 'ç')
.replace(/=C3=A0/g, 'à')
.replace(/=C3=B9/g, 'ù')
.replace(/=C3=A2/g, 'â')
.replace(/=C3=AA/g, 'ê')
.replace(/=C3=AE/g, 'î')
.replace(/=C3=B4/g, 'ô')
.replace(/=C3=BB/g, 'û');
}
function parseRawEmail(raw) {
let text = "";
let html = "";
const textMatch = raw.match(/Content-Type: text\/plain[\s\S]*?(?=--[a-zA-Z0-9]+|Content-Type: text\/html|$)/i);
if (textMatch) {
text = textMatch[0]
.replace(/Content-Type:[^\n]+/gi, '')
.replace(/Content-Transfer-Encoding:[^\n]+/gi, '')
.replace(/Content-Disposition:[^\n]+/gi, '')
.trim();
text = decodeQuotedPrintable(text);
}
const htmlMatch = raw.match(/Content-Type: text\/html[\s\S]*?(?=--[a-zA-Z0-9]+|$)/i);
if (htmlMatch) {
html = htmlMatch[0]
.replace(/Content-Type:[^\n]+/gi, '')
.replace(/Content-Transfer-Encoding:[^\n]+/gi, '')
.replace(/Content-Disposition:[^\n]+/gi, '')
.trim();
html = decodeQuotedPrintable(html);
}
if (!text && !html) {
const parts = raw.split(/\r?\n\r?\n/);
text = decodeQuotedPrintable(parts.slice(1).join('\n\n').trim());
}
return {text, html};
}
function parseEmailContent(text, html) {
let cleanText = decodeQuotedPrintable(text || "");
let decodedHtml = decodeQuotedPrintable(html || "");
let searchContent = cleanText;
if (!searchContent && decodedHtml) {
searchContent = decodedHtml.replace(/<[^>]+>/g, ' ');
}
searchContent = searchContent.replace(/--[a-zA-Z0-9]+/g, '')
.replace(/Content-Type:[^\n]+/gi, '')
.replace(/Content-Transfer-Encoding:[^\n]+/gi, '')
.trim();
let otpCode = null;
const otpPatterns = [
/(?:OTP|One.?Time.?Password|verifikas?i|verification|verify|code|kode|pin|token)[:\s-]*([0-9]{4,8})/i,
/(?:kode|code|otp|pin)\s*(?:is|adalah)?[:\s-]*([A-Z0-9]{4,8})/i,
/\b([0-9]{4,8})\b/
];
for (const pattern of otpPatterns) {
const match = searchContent.match(pattern);
if (match && match[1]) {
const code = match[1];
if (!/^(19|20)\d{2}$/.test(code)) {
otpCode = code;
break;
}
}
}
const links = [];
const linkRegex = /https?:\/\/[^\s"'<>]+/g;
let linkMatch;
while ((linkMatch = linkRegex.exec(searchContent)) !== null) {
const url = linkMatch[0].replace(/[.,;:]+$/, '');
if (url.length > 20 && url.length < 1000 && /verify|verification|token|confirm|activate|reset|password|email/i.test(url)) {
links.push(url);
}
}
if (decodedHtml) {
const htmlLinks = decodedHtml.match(/href=["']([^"']+)["']/gi) || [];
for (const hl of htmlLinks) {
const url = hl.replace(/href=["']/gi, '').replace(/["']$/g, '');
if (url.startsWith('http') && /verify|verification|token|confirm|activate|reset|password|email/i.test(url)) {
if (!links.includes(url)) links.push(url);
}
}
}
return {cleanText, decodedHtml, otpCode, links};
}
async function saveEmail(env, ctx, emailData) {
const toAddress = emailData.to;
const fromAddress = emailData.from || "unknown@example.com";
const fromName = emailData.fromName || "";
const subject = emailData.subject || "";
const text = emailData.text || "";
const html = emailData.html || "";
if (!toAddress) return {success: false, error: "To address required", status: 400};
const { results } = await env.MAIL_DB.prepare("SELECT * FROM accounts WHERE address = ?").bind(toAddress).all();
if (results.length === 0) return {success: false, error: "Account not found", status: 404};
const account = results[0];
const msgId = crypto.randomUUID().replace(/-/g, '').substring(0, 24);
const now = new Date().toISOString();
const expires = new Date(Date.now() + 60*60*1000).toISOString();
const parsed = parseEmailContent(text, html);
await env.MAIL_DB.prepare(
`INSERT INTO messages (id, account_id, from_address, from_name, to_address, subject, text_content, html_content, intro, size, created_at, updated_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).bind(msgId, account.id, fromAddress, fromName, toAddress, subject, text, html, parsed.cleanText.substring(0, 100), text.length + html.length, now, now, expires).run();
if (emailData.webhookUrl || env.GLOBAL_WEBHOOK_URL) {
const targetWebhook = emailData.webhookUrl || env.GLOBAL_WEBHOOK_URL;
ctx.waitUntil(
fetch(targetWebhook, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: msgId, to: toAddress, from: fromAddress, subject, otpCode: parsed.otpCode, links: parsed.links
})
}).catch(() => {})
);
}
return {success: true, message: {
id: msgId, accountId: account.id,
from: {address: fromAddress, name: fromName},
to: [{address: toAddress, name: ""}],
subject, intro: parsed.cleanText.substring(0, 100), text, html,
seen: false, isDeleted: false, hasAttachments: false,
size: text.length + html.length, createdAt: now, updatedAt: now,
otpCode: parsed.otpCode, verificationLinks: parsed.links
}};
}
function renderPage() {
return `
📧 Temp Mail Pro
📧 Temp Mail Pro
Buat Email Sementara
📥 Email Masuk
👆 Buat email dulu untuk mulai
🔧 Temp Mail API
List domain
curl -X GET "https://domain.com/domains"
Buat akun
curl -X POST "https://domain.com/accounts" \\
-H "Content-Type: application/json" \\
-d '{"address":"nama@domain.com","password":"Password123!"}'
Login & token
curl -X POST "https://domain.com/token" \\
-H "Content-Type: application/json" \\
-d '{"address":"nama@domain.com","password":"Password123!"}'
List email
curl -X GET "https://domain.com/messages" \\
-H "Authorization: Bearer TOKEN"
Baca email
curl -X GET "https://domain.com/messages/MSG_ID" \\
-H "Authorization: Bearer TOKEN"
`;
}