// 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); }, // Poin 1: Cron Trigger Auto-Cleanup 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(); // Deteksi OTP Presisi 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(); // Poin 4: Dynamic Outbound Webhook (Opsional) 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
📥 Email Masuk
👆 Buat email dulu untuk mulai

🔧 Temp Mail API

GET/domains

List domain

curl -X GET "https://domain.com/domains"
POST/accounts

Buat akun

curl -X POST "https://domain.com/accounts" \\
  -H "Content-Type: application/json" \\
  -d '{"address":"nama@domain.com","password":"Password123!"}'
POST/token

Login & token

curl -X POST "https://domain.com/token" \\
  -H "Content-Type: application/json" \\
  -d '{"address":"nama@domain.com","password":"Password123!"}'
GET/messages

List email

curl -X GET "https://domain.com/messages" \\
  -H "Authorization: Bearer TOKEN"
GET/messages/:id

Baca email

curl -X GET "https://domain.com/messages/MSG_ID" \\
  -H "Authorization: Bearer TOKEN"
`; }