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) || /
LocalStack SES
A faster local inbox for generated emails with live refresh, manual refresh, search, and raw MIME inspection.