import express from "express"; import fetch from "node-fetch"; import { simpleParser } from "mailparser"; const app = express(); const PORT = Number(process.env.PORT || 3334); const SES_ENDPOINT = process.env.SES_VIEWER_ENDPOINT || "http://localhost:4566/_aws/ses"; const FETCH_TIMEOUT_MS = Number(process.env.SES_VIEWER_FETCH_TIMEOUT_MS || 5000); const DEFAULT_REFRESH_MS = Number(process.env.SES_VIEWER_REFRESH_MS || 10000); app.use((req, res, next) => { res.set("Cache-Control", "no-store"); next(); }); app.get("/", (req, res) => { res.type("html").send(renderHtml()); }); app.get("/app.js", (req, res) => { res.type("application/javascript").send(`(${clientApp.toString()})(${JSON.stringify(getClientConfig())});`); }); app.get("/health", (req, res) => { res.json({ ok: true, endpoint: SES_ENDPOINT, port: PORT, defaultRefreshMs: DEFAULT_REFRESH_MS }); }); app.get("/api/messages", async (req, res) => { try { res.json(await loadMessages()); } catch (error) { console.error("Error fetching messages:", error); res.status(502).json({ error: "Unable to fetch messages from LocalStack SES", details: error.message, endpoint: SES_ENDPOINT }); } }); app.get("/api/messages/:id/raw", async (req, res) => { try { const messages = await fetchSesMessages(); const message = messages.find((candidate) => resolveMessageId(candidate) === req.params.id); if (!message) { res.status(404).type("text/plain").send("Message not found"); return; } res.type("text/plain").send(message.RawData || ""); } catch (error) { console.error("Error fetching raw message:", error); res.status(502).type("text/plain").send(`Unable to fetch raw message: ${error.message}`); } }); async function loadMessages() { const startedAt = Date.now(); const sesMessages = await fetchSesMessages(); const messages = await Promise.all(sesMessages.map((message, index) => toMessageViewModel(message, index))); messages.sort((left, right) => { if ((right.timestampMs || 0) !== (left.timestampMs || 0)) { return (right.timestampMs || 0) - (left.timestampMs || 0); } return right.index - left.index; }); return { endpoint: SES_ENDPOINT, fetchedAt: new Date().toISOString(), fetchDurationMs: Date.now() - startedAt, totalMessages: messages.length, parseErrors: messages.filter((message) => Boolean(message.parseError)).length, latestMessageTimestamp: messages[0]?.timestamp || "", messages }; } async function fetchSesMessages() { const response = await fetch(SES_ENDPOINT, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); if (!response.ok) { throw new Error(`SES endpoint responded with ${response.status}`); } const data = await response.json(); return Array.isArray(data.messages) ? data.messages : []; } async function toMessageViewModel(message, index) { const id = resolveMessageId(message, index); try { const parsed = await simpleParser(message.RawData || ""); const textContent = normalizeText(parsed.text || ""); const renderedHtml = buildRenderedHtml(parsed.html || parsed.textAsHtml || ""); const timestamp = normalizeTimestamp(message.Timestamp || parsed.date); return { id, index, from: formatAddressList(parsed.from) || message.Source || "Unknown sender", to: formatAddressList(parsed.to) || "No To Address", replyTo: formatAddressList(parsed.replyTo), subject: parsed.subject || "No Subject", region: message.Region || "", timestamp, timestampMs: timestamp ? Date.parse(timestamp) : 0, messageId: parsed.messageId || "", rawSizeBytes: Buffer.byteLength(message.RawData || "", "utf8"), attachmentCount: parsed.attachments.length, attachments: parsed.attachments.map((attachment) => ({ filename: attachment.filename || "Unnamed attachment", contentType: attachment.contentType || "application/octet-stream", size: attachment.size || 0 })), preview: buildPreview(textContent, renderedHtml), textContent, renderedHtml, hasHtml: Boolean(renderedHtml), parseError: "" }; } catch (error) { return { id, index, from: message.Source || "Unknown sender", to: "Unknown recipient", replyTo: "", subject: "Unable to parse message", region: message.Region || "", timestamp: normalizeTimestamp(message.Timestamp), timestampMs: message.Timestamp ? Date.parse(message.Timestamp) : 0, messageId: "", rawSizeBytes: Buffer.byteLength(message.RawData || "", "utf8"), attachmentCount: 0, attachments: [], preview: "This message could not be parsed. Open the raw view to inspect the MIME source.", textContent: "", renderedHtml: "", hasHtml: false, parseError: error.message }; } } function resolveMessageId(message, index = 0) { return message.Id || `${message.Timestamp || "unknown"}-${message.Source || "unknown"}-${index}`; } function normalizeTimestamp(value) { if (!value) { return ""; } const date = value instanceof Date ? value : new Date(value); return Number.isNaN(date.getTime()) ? "" : date.toISOString(); } function normalizeText(value) { return String(value || "") .replace(/\r\n/g, "\n") .trim(); } function buildPreview(textContent, renderedHtml) { const source = (textContent || stripTags(renderedHtml)).replace(/\s+/g, " ").trim(); if (!source) { return "No message preview available."; } return source.length > 220 ? `${source.slice(0, 217)}...` : source; } function buildRenderedHtml(html) { if (!html) { return ""; } const value = String(html); const hasDocument = /]/i.test(value) || / ${value} `; } function stripTags(value) { return String(value || "") .replace(//gi, " ") .replace(//gi, " ") .replace(/<[^>]+>/g, " "); } function formatAddressList(addresses) { if (!addresses?.value?.length) { return ""; } return addresses.value .map(({ name, address }) => { if (name && address) { return `${name} <${address}>`; } return address || name || ""; }) .filter(Boolean) .join(", "); } function getClientConfig() { return { defaultRefreshMs: DEFAULT_REFRESH_MS, endpoint: SES_ENDPOINT }; } function renderHtml() { return ` LocalStack Email Viewer

LocalStack SES

Email Viewer

A faster local inbox for generated emails with live refresh, manual refresh, search, and raw MIME inspection.

Waiting for first refresh...
Total00 visible
New0New since last refresh
NewestNo messagesNot refreshed yet
FetchIdleEndpoint: ${escapeHtml(SES_ENDPOINT)}
`; } function renderStyles() { return ` :root{--panel:rgba(255,255,255,.86);--panel-strong:#fff;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--accent:#cf6d3c;--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 18px 48px rgba(35,43,53,.12);} *{box-sizing:border-box} html,body{margin:0;min-height:100%} body{background:radial-gradient(circle at top left,rgba(207,109,60,.18),transparent 28%),radial-gradient(circle at top right,rgba(31,143,101,.14),transparent 24%),linear-gradient(180deg,#f8f5ef,#efe7da);color:var(--ink);font:16px/1.5 "Aptos","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif} button,input,select,textarea{font:inherit} button{cursor:pointer} .page{max-width:1440px;margin:0 auto;padding:24px} .hero{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(320px,.9fr);gap:24px;margin-bottom:24px} .hero-copy,.hero-controls,.stat,.card{background:var(--panel);backdrop-filter:blur(16px);border:1px solid var(--line);box-shadow:var(--shadow)} .hero-copy,.hero-controls{border-radius:24px;padding:24px} .eyebrow{margin:0 0 8px;color:var(--accent);font-size:.8rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase} h1{margin:0;font-size:clamp(2.2rem,5vw,4rem);line-height:.95;letter-spacing:-.05em} .lede{margin:16px 0 0;max-width:60ch;color:var(--muted)} .hero-controls{display:grid;gap:12px} .row{display:flex;flex-wrap:wrap;gap:12px;align-items:center} .primary,.ghost,.mini,.tab{border-radius:999px;border:1px solid transparent;transition:transform .12s ease,background-color .12s ease,border-color .12s ease} .primary,.ghost{min-height:44px;padding:0 18px;font-weight:700} .mini,.tab{min-height:34px;padding:0 12px;font-weight:600} .primary{background:var(--accent);color:#fff7f2} .ghost,.mini{background:rgba(255,255,255,.76);border-color:var(--line);color:var(--ink)} .tab{background:transparent;color:var(--muted)} .tab.active{background:#fff;border-color:var(--line);color:var(--ink)} .primary:hover,.ghost:hover,.mini:hover,.tab:hover{transform:translateY(-1px)} .chip{display:inline-flex;align-items:center;gap:10px;min-height:44px;padding:0 16px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);font-weight:600} .chip input{margin:0;accent-color:var(--accent)} .chip select{border:none;background:transparent;outline:none;color:var(--ink)} .search{flex:1 1 320px;min-height:48px;padding:0 16px;border-radius:16px;border:1px solid var(--line);background:rgba(255,255,255,.82);color:var(--ink);outline:none} .status{display:inline-flex;align-items:center;min-height:40px;padding:0 14px;border-radius:999px;background:rgba(255,255,255,.76);border:1px solid var(--line);color:var(--muted);font-weight:600} .status.ok{color:var(--ok);border-color:rgba(31,143,101,.22);background:rgba(31,143,101,.08)} .status.warn{color:var(--warn);border-color:rgba(157,95,0,.22);background:rgba(157,95,0,.08)} .status.bad{color:var(--bad);border-color:rgba(179,58,58,.22);background:rgba(179,58,58,.08)} .stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px;margin-bottom:18px} .stat{border-radius:18px;padding:18px} .stat span{display:block;margin-bottom:10px;color:var(--muted);font-size:.82rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} .stat strong{display:block;font-size:clamp(2rem,4vw,2.6rem);line-height:1;letter-spacing:-.05em} .stat strong.small{font-size:1.1rem;line-height:1.3;letter-spacing:-.02em} .stat small{display:block;margin-top:10px;color:var(--muted);font-size:.95rem} .banner,.empty{margin:0 0 18px;padding:16px 18px;border-radius:16px;border:1px solid var(--line);background:rgba(255,255,255,.82)} .banner{color:var(--bad);border-color:rgba(179,58,58,.24);background:rgba(179,58,58,.08)} .list{display:grid;gap:18px} .card{overflow:hidden;border-radius:18px} .card.new{border-color:rgba(31,143,101,.28);box-shadow:var(--shadow),0 0 0 1px rgba(31,143,101,.12)} .summary{list-style:none;display:grid;gap:12px;padding:20px;cursor:pointer} .summary::-webkit-details-marker{display:none} .top,.tags,.toolbar,.actions,.attachments{display:flex;flex-wrap:wrap;gap:10px;align-items:center} .top{justify-content:space-between} .head{min-width:0;flex:1 1 320px} .head h2{margin:0;font-size:clamp(1.1rem,2vw,1.45rem);line-height:1.2;letter-spacing:-.03em;word-break:break-word} .meta{margin:6px 0 0;color:var(--muted);font-size:.95rem;word-break:break-word} .time,.tag{display:inline-flex;align-items:center;min-height:28px;padding:0 12px;border-radius:999px;font-size:.84rem;font-weight:700} .time{background:rgba(31,41,51,.06)} .tag{background:rgba(31,41,51,.06);color:var(--muted)} .tag.new{background:rgba(31,143,101,.1);color:var(--ok)} .tag.bad{background:rgba(179,58,58,.1);color:var(--bad)} .preview{margin:0} .body{display:grid;gap:16px;padding:18px 20px 20px;border-top:1px solid var(--line);background:var(--panel-strong)} .toolbar{justify-content:space-between;align-items:flex-start} .tabs{display:inline-flex;gap:6px;padding:6px;border-radius:999px;background:rgba(31,41,51,.05)} .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px} .metaCard{padding:14px 16px;border-radius:14px;background:rgba(31,41,51,.035);border:1px solid rgba(31,41,51,.08)} .metaCard dt{margin:0 0 6px;color:var(--muted);font-size:.8rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase} .metaCard dd{margin:0;word-break:break-word} .attachments{gap:8px} .attachment{padding:9px 12px;border-radius:12px;background:rgba(255,255,255,.72);border:1px solid var(--line);font-size:.92rem} .panel{overflow:hidden;border-radius:14px;border:1px solid var(--line);background:#fff} iframe{width:100%;min-height:720px;border:none;background:#fff} pre{margin:0;padding:16px;white-space:pre-wrap;word-break:break-word;overflow:auto;font:13px/1.55 "Cascadia Code","Consolas",monospace;color:#102030;background:linear-gradient(180deg,rgba(207,109,60,.04),transparent 140px),#fff} .placeholder,.inlineError{padding:16px} .inlineError{color:var(--bad)} @media (max-width:1080px){.hero{grid-template-columns:1fr}.stats{grid-template-columns:repeat(2,minmax(0,1fr))}} @media (max-width:720px){.page{padding:16px}.stats{grid-template-columns:1fr}.primary,.ghost,.chip{width:100%;justify-content:center}.toolbar,.row{align-items:stretch}iframe{min-height:560px}} `; } function escapeHtml(value) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function clientApp(config) { const state = { messages: [], filtered: [], search: "", auto: true, interval: config.defaultRefreshMs, loading: false, error: "", updatedAt: 0, source: "initial", duration: 0, parseErrors: 0, newest: "", newIds: new Set(), knownIds: new Set(), openIds: new Set(), views: {}, raw: {} }; const el = { refreshButton: document.getElementById("refreshButton"), autoToggle: document.getElementById("autoToggle"), intervalSelect: document.getElementById("intervalSelect"), searchInput: document.getElementById("searchInput"), clearSearchButton: document.getElementById("clearSearchButton"), expandAllButton: document.getElementById("expandAllButton"), collapseAllButton: document.getElementById("collapseAllButton"), statusChip: document.getElementById("statusChip"), totalStat: document.getElementById("totalStat"), visibleStat: document.getElementById("visibleStat"), newStat: document.getElementById("newStat"), newestStat: document.getElementById("newestStat"), updatedStat: document.getElementById("updatedStat"), fetchStat: document.getElementById("fetchStat"), fetchDetail: document.getElementById("fetchDetail"), banner: document.getElementById("banner"), empty: document.getElementById("empty"), list: document.getElementById("list") }; el.intervalSelect.value = String(config.defaultRefreshMs); wire(); renderAll(); refreshMessages("initial"); window.setInterval(renderLiveClock, 1000); function wire() { el.refreshButton.addEventListener("click", () => refreshMessages("manual")); el.autoToggle.addEventListener("change", () => { state.auto = el.autoToggle.checked; scheduleRefresh(); renderStatus(); }); el.intervalSelect.addEventListener("change", () => { state.interval = Number(el.intervalSelect.value) || config.defaultRefreshMs; scheduleRefresh(); renderStatus(); }); el.searchInput.addEventListener("input", (event) => { state.search = event.target.value; applyFilter(); }); el.clearSearchButton.addEventListener("click", () => { state.search = ""; el.searchInput.value = ""; applyFilter(); }); el.expandAllButton.addEventListener("click", () => { state.filtered.forEach((message) => state.openIds.add(message.id)); renderList(); }); el.collapseAllButton.addEventListener("click", () => { state.openIds.clear(); renderList(); }); el.list.addEventListener( "toggle", (event) => { const details = event.target; if (!(details instanceof HTMLDetailsElement) || !details.matches(".card")) { return; } const id = details.dataset.id; if (!id) { return; } if (details.open) { state.openIds.add(id); } else { state.openIds.delete(id); } hydrate(details, getMessage(id)); }, true ); el.list.addEventListener("click", async (event) => { const button = event.target.closest("button[data-action]"); if (!button) { return; } const id = button.dataset.id; const message = getMessage(id); if (!message) { return; } if (button.dataset.action === "view") { state.views[id] = button.dataset.view; renderList(); return; } if (button.dataset.action === "load-raw") { await loadRaw(id); renderList(); return; } if (button.dataset.action === "copy-raw") { const raw = await loadRaw(id); if (raw) { await copyText(raw); setStatus("Raw message copied to the clipboard.", "ok"); } } }); document.addEventListener("visibilitychange", () => { window.clearTimeout(state.timer); if (!document.hidden && state.auto) { refreshMessages("visibility"); } else { renderStatus(); } }); window.addEventListener("keydown", (event) => { const isField = event.target instanceof HTMLElement && (event.target.matches("input,textarea,select") || event.target.isContentEditable); if (!isField && event.key.toLowerCase() === "r") { event.preventDefault(); refreshMessages("keyboard"); } }); } async function refreshMessages(source) { if (state.loading) { return; } state.loading = true; state.source = source; state.error = ""; renderStatus(); renderFetch(); try { const response = await fetch("/api/messages", { cache: "no-store" }); if (!response.ok) { const payload = await safeJson(response); throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`); } const payload = await response.json(); const messages = Array.isArray(payload.messages) ? payload.messages : []; const nextIds = new Set(messages.map((message) => message.id)); state.newIds = state.updatedAt ? new Set(messages.filter((message) => !state.knownIds.has(message.id)).map((message) => message.id)) : new Set(); state.knownIds = nextIds; state.messages = messages; state.duration = payload.fetchDurationMs || 0; state.parseErrors = payload.parseErrors || 0; state.newest = payload.latestMessageTimestamp || ""; state.updatedAt = Date.now(); pruneState(); applyFilter(); setStatus(`Updated ${messages.length} message${messages.length === 1 ? "" : "s"}.`, "ok"); } catch (error) { state.error = error.message || "Unknown refresh error"; setStatus(`Refresh failed: ${state.error}`, "bad"); } finally { state.loading = false; scheduleRefresh(); renderAll(); } } function pruneState() { const ids = new Set(state.messages.map((message) => message.id)); state.openIds = new Set([...state.openIds].filter((id) => ids.has(id))); state.newIds = new Set([...state.newIds].filter((id) => ids.has(id))); Object.keys(state.views).forEach((id) => { if (!ids.has(id)) { delete state.views[id]; } }); Object.keys(state.raw).forEach((id) => { if (!ids.has(id)) { delete state.raw[id]; } }); } function applyFilter() { const search = state.search.trim().toLowerCase(); state.filtered = !search ? [...state.messages] : state.messages.filter((message) => haystack(message).includes(search)); renderAll(); } function haystack(message) { return [ message.subject, message.from, message.to, message.replyTo, message.preview, message.textContent, message.region, ...(message.attachments || []).flatMap((attachment) => [attachment.filename, attachment.contentType]) ] .filter(Boolean) .join(" ") .toLowerCase(); } function scheduleRefresh() { window.clearTimeout(state.timer); if (!state.auto || document.hidden || state.loading) { return; } state.timer = window.setTimeout(() => refreshMessages("auto"), state.interval); } function renderAll() { renderStats(); renderFetch(); renderStatus(); renderList(); renderLiveClock(); } function renderStats() { el.totalStat.textContent = String(state.messages.length); el.visibleStat.textContent = `${state.filtered.length} visible${state.search ? " after search" : ""}`; el.newStat.textContent = String(state.newIds.size); el.newestStat.textContent = state.newest ? formatDateTime(state.newest) : "No messages"; } function renderFetch() { if (state.loading) { el.fetchStat.textContent = "Refreshing..."; el.fetchDetail.textContent = `Endpoint: ${config.endpoint}`; return; } if (state.error) { el.fetchStat.textContent = "Needs attention"; el.fetchDetail.textContent = state.error; return; } if (!state.updatedAt) { el.fetchStat.textContent = "Idle"; el.fetchDetail.textContent = `Endpoint: ${config.endpoint}`; return; } el.fetchStat.textContent = `${state.duration}ms`; el.fetchDetail.textContent = `${state.parseErrors} parse error${state.parseErrors === 1 ? "" : "s"}. Endpoint: ${config.endpoint}`; } function renderStatus() { el.statusChip.className = "status"; if (state.loading) { el.statusChip.classList.add("warn"); el.statusChip.textContent = "Refreshing messages..."; return; } if (state.error) { el.statusChip.classList.add("bad"); el.statusChip.textContent = `Refresh failed: ${state.error}`; return; } if (!state.auto) { el.statusChip.textContent = "Live refresh paused"; return; } if (document.hidden) { el.statusChip.classList.add("warn"); el.statusChip.textContent = "Tab hidden, live refresh paused"; return; } if (!state.updatedAt) { el.statusChip.textContent = "Waiting for first refresh..."; return; } const seconds = Math.max(0, Math.ceil((state.updatedAt + state.interval - Date.now()) / 1000)); el.statusChip.classList.add("ok"); el.statusChip.textContent = `Live refresh on, next check in ${seconds}s`; } function renderLiveClock() { if (!state.updatedAt) { el.updatedStat.textContent = "Not refreshed yet"; return; } el.updatedStat.textContent = `Updated ${formatRelative(state.updatedAt)} via ${state.source}`; renderStatus(); } function renderList() { el.banner.hidden = !state.error; el.banner.textContent = state.error ? `Refresh failed: ${state.error}` : ""; if (!state.filtered.length) { el.list.innerHTML = ""; el.empty.hidden = false; el.empty.textContent = state.messages.length ? "No messages match the current search." : "No emails yet. Send one through LocalStack SES and refresh."; return; } el.empty.hidden = true; el.list.innerHTML = state.filtered.map(renderCard).join(""); el.list.querySelectorAll(".card[open]").forEach((details) => hydrate(details, getMessage(details.dataset.id))); } function renderCard(message) { const open = state.openIds.has(message.id); const view = state.views[message.id] || (message.hasHtml ? "rendered" : "text"); const tags = []; if (state.newIds.has(message.id)) { tags.push('New'); } if (message.attachmentCount) { tags.push( `${message.attachmentCount} attachment${message.attachmentCount === 1 ? "" : "s"}` ); } tags.push(`${message.hasHtml ? "HTML" : "Text only"}`); if (message.parseError) { tags.push('Parse issue'); } return `

${escapeHtml(message.subject)}

${escapeHtml(message.from)} to ${escapeHtml(message.to)}

${escapeHtml(formatDateTime(message.timestamp))}
${tags.join("")}

${escapeHtml(message.preview)}

${ message.hasHtml ? `` : "" }
${metaCard("From", message.from)} ${metaCard("To", message.to)} ${metaCard("Reply-To", message.replyTo || "None")} ${metaCard("Sent", formatDateTime(message.timestamp))} ${metaCard("Region", message.region || "Unknown region")} ${metaCard("LocalStack Id", message.id)} ${metaCard("Message-Id", message.messageId || "Not available")} ${metaCard("Raw size", formatBytes(message.rawSizeBytes))} ${message.parseError ? metaCard("Parse error", message.parseError) : ""}
${ message.attachments?.length ? `
${message.attachments .map((attachment) => { const size = attachment.size ? `, ${formatBytes(attachment.size)}` : ""; return `${escapeHtml(attachment.filename)} (${escapeHtml(attachment.contentType)}${escapeHtml(size)})`; }) .join("")}
` : "" }
${renderPanel(message, view)}
`; } function renderPanel(message, view) { if (view === "rendered" && message.hasHtml) { return ``; } if (view === "raw") { const raw = state.raw[message.id]; if (!raw) { return `
Raw MIME source is loaded on demand.
`; } if (raw.status === "loading") { return '
Loading raw source...
'; } if (raw.status === "error") { return `
Unable to load raw source: ${escapeHtml(raw.error)}
`; } return `
${escapeHtml(raw.value)}
`; } return `
${escapeHtml(message.textContent || "No plain-text content available for this message.")}
`; } function metaCard(label, value) { return `
${escapeHtml(label)}
${escapeHtml(value)}
`; } function hydrate(details, message) { if (!details || !details.open || !message) { return; } const view = state.views[message.id] || (message.hasHtml ? "rendered" : "text"); if (view !== "rendered" || !message.hasHtml) { return; } const iframe = details.querySelector("[data-frame]"); if (iframe) { iframe.referrerPolicy = "no-referrer"; iframe.sandbox = ""; iframe.srcdoc = message.renderedHtml || ""; } } function getMessage(id) { return state.messages.find((message) => message.id === id); } async function loadRaw(id) { if (state.raw[id]?.status === "loaded") { return state.raw[id].value; } if (state.raw[id]?.status === "loading") { return null; } state.raw[id] = { status: "loading" }; renderList(); try { const response = await fetch(`/api/messages/${encodeURIComponent(id)}/raw`, { cache: "no-store" }); if (!response.ok) { throw new Error((await response.text()) || `Request failed with ${response.status}`); } const value = await response.text(); state.raw[id] = { status: "loaded", value }; return value; } catch (error) { state.raw[id] = { status: "error", error: error.message || "Unknown raw load error" }; setStatus("Could not load the raw message source.", "bad"); return null; } finally { renderList(); } } async function copyText(value) { try { await navigator.clipboard.writeText(value); } catch { const input = document.createElement("textarea"); input.value = value; input.setAttribute("readonly", ""); input.style.position = "fixed"; input.style.opacity = "0"; document.body.appendChild(input); input.select(); document.execCommand("copy"); document.body.removeChild(input); } } function setStatus(message, tone) { el.statusChip.className = "status"; if (tone) { el.statusChip.classList.add(tone); } el.statusChip.textContent = message; } function formatDateTime(value) { if (!value) { return "Unknown time"; } const date = new Date(value); return Number.isNaN(date.getTime()) ? "Unknown time" : new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date); } function formatRelative(timestampMs) { const seconds = Math.max(1, Math.round((Date.now() - timestampMs) / 1000)); if (seconds < 60) { return `${seconds}s ago`; } const minutes = Math.round(seconds / 60); if (minutes < 60) { return `${minutes}m ago`; } const hours = Math.round(minutes / 60); if (hours < 24) { return `${hours}h ago`; } return `${Math.round(hours / 24)}d ago`; } function formatBytes(value) { if (!value) { return "0 B"; } const units = ["B", "KB", "MB", "GB"]; let size = value; let index = 0; while (size >= 1024 && index < units.length - 1) { size /= 1024; index += 1; } return `${index === 0 ? size : size.toFixed(1)} ${units[index]}`; } function escapeHtml(value) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } async function safeJson(response) { try { return await response.json(); } catch { return null; } } } app.listen(PORT, () => { console.log(`Local email viewer is running on http://localhost:${PORT}`); console.log(`Watching LocalStack SES endpoint at ${SES_ENDPOINT}`); });