diff --git a/_reference/localEmailViewer/README.md b/_reference/localEmailViewer/README.md
index 3f8a6fc71..6054686fd 100644
--- a/_reference/localEmailViewer/README.md
+++ b/_reference/localEmailViewer/README.md
@@ -1,7 +1,33 @@
-This will connect to your dockers local stack session and render the email in HTML.
+This app connects to your Docker LocalStack SES endpoint and gives you a local inbox-style viewer
+for generated emails.
+
+```shell
+npm start
+```
+
+Or:
```shell
node index.js
```
-http://localhost:3334
+Open: http://localhost:3334
+
+Features:
+
+- Manual refresh
+- Live refresh with adjustable polling interval
+- Search across subject, addresses, preview text, and attachment names
+- Expand/collapse all messages
+- Rendered HTML, plain-text, and raw MIME views
+- Copy raw MIME source
+- New-message highlighting plus fetch timing and parse-error stats
+
+Optional environment variables:
+
+```shell
+PORT=3334
+SES_VIEWER_ENDPOINT=http://localhost:4566/_aws/ses
+SES_VIEWER_REFRESH_MS=10000
+SES_VIEWER_FETCH_TIMEOUT_MS=5000
+```
diff --git a/_reference/localEmailViewer/index.js b/_reference/localEmailViewer/index.js
index 1dd17f779..4106d2903 100644
--- a/_reference/localEmailViewer/index.js
+++ b/_reference/localEmailViewer/index.js
@@ -1,96 +1,1016 @@
-// index.js
-
import express from "express";
import fetch from "node-fetch";
import { simpleParser } from "mailparser";
const app = express();
-const PORT = 3334;
-app.get("/", async (req, res) => {
+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 {
- const response = await fetch("http://localhost:4566/_aws/ses");
- if (!response.ok) {
- throw new Error("Network response was not ok");
- }
- const data = await response.json();
- const messagesHtml = await parseMessages(data.messages);
- res.send(renderHtml(messagesHtml));
+ res.json(await loadMessages());
} catch (error) {
console.error("Error fetching messages:", error);
- res.status(500).send("Error fetching messages");
+ res.status(502).json({
+ error: "Unable to fetch messages from LocalStack SES",
+ details: error.message,
+ endpoint: SES_ENDPOINT
+ });
}
});
-async function parseMessages(messages) {
- const parsedMessages = await Promise.all(
- messages.map(async (message, index) => {
- try {
- const parsed = await simpleParser(message.RawData);
- return `
-
-
-
Message ${index + 1}
-
From: ${message.Source}
-
To: ${parsed.to.text || "No To Address"}
-
Subject: ${parsed.subject || "No Subject"}
-
Region: ${message.Region}
-
Timestamp: ${message.Timestamp}
-
-
${parsed.html || parsed.textAsHtml || "No HTML content available"}
-
- `;
- } catch (error) {
- console.error("Error parsing email:", error);
- return `
-
-
Message ${index + 1}
-
From: ${message.Source}
-
Region: ${message.Region}
-
Timestamp: ${message.Timestamp}
-
Error parsing email content
-
- `;
- }
- })
- );
- return parsedMessages.join("");
+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
+ };
}
-function renderHtml(messagesHtml) {
- return `
-
-
-
-
-
- Email Messages Viewer
-
-
-
-
-
-
Email Messages Viewer
-
${messagesHtml}
+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(/
+
+
+
+
+
+
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)}
+
+
+
+
+
+
+
+
+
+