// Temp Mail API + Dashboard + API Docs // Quoted-printable decoder + HTML rendering + OTP detection 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, 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, {to: body.to, from: body.from, fromName: body.from_name || body.fromName, subject: body.subject, text: body.text, html: body.html}); 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 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, {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 = ""; // Cari text/plain section 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); } // Cari text/html section 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 || ""); cleanText = cleanText.replace(/--[a-zA-Z0-9]+/g, ''); cleanText = cleanText.replace(/Content-Type:[^\n]+/gi, ''); cleanText = cleanText.replace(/Content-Transfer-Encoding:[^\n]+/gi, ''); cleanText = cleanText.trim(); let otpCode = null; const otpPatterns = [ /(?:OTP|otp|One.?Time.?Password|verification code|verify code|kode verifikasi|kode OTP|kode otp|verification token)[:\s]*([0-9]{4,8})/i, /(?:verification|verify|otp|code)[:\s]*([0-9]{4,8})/i, /(?:kode|code|otp)[:\s]*([A-Z0-9]{4,8})/i ]; for (const pattern of otpPatterns) { const match = cleanText.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(cleanText)) !== 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, 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(); 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
📥 Email Masuk
👆 Buat email dulu untuk mulai

🔧 Temp Mail API

GET/domains

List domain

curl -X GET "https://cair.eu.cc/domains"
POST/accounts

Buat akun

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

Login & token

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

List email

curl -X GET "https://cair.eu.cc/messages" \\
  -H "Authorization: Bearer TOKEN"
GET/messages/:id

Baca email

curl -X GET "https://cair.eu.cc/messages/MSG_ID" \\
  -H "Authorization: Bearer TOKEN"
DELETE/messages/:id

Hapus email

curl -X DELETE "https://cair.eu.cc/messages/MSG_ID" \\
  -H "Authorization: Bearer TOKEN"
GET/me

Info akun

curl -X GET "https://cair.eu.cc/me" \\
  -H "Authorization: Bearer TOKEN"
POST/receive

Kirim email

curl -X POST "https://cair.eu.cc/receive" \\
  -H "Content-Type: application/json" \\
  -d '{"to":"nama@cair.eu.cc","from":"sender@gmail.com","subject":"Test","text":"Hello"}'
`; }