diff --git a/src/index.ts b/src/index.ts index e079c79..ce91cc8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { Hono } from "hono"; import { handle as handleInbound } from "./routes/inbound"; import { handle as handleRSS } from "./routes/rss"; import { handle as handleAdmin } from "./routes/admin"; +import { handle as handleEntry } from "./routes/entries"; import { handleCloudflareEmail } from "./lib/cloudflare-email"; import { Env } from "./types"; @@ -101,6 +102,7 @@ app.use("*", async (c, next) => { // Group routes by functionality const api = new Hono(); const rss = new Hono(); +const entries = new Hono(); const admin = new Hono(); // Webhook security middleware for /inbound - verify ForwardEmail.net IP @@ -131,12 +133,16 @@ api.post("/inbound", handleInbound); // RSS feed routes (public) rss.get("/:feedId", handleRSS); +// Email entry HTML view (public) +entries.get("/:feedId/:entryId", handleEntry); + // Admin routes (protected) admin.route("/", handleAdmin); // Mount the route groups app.route("/api", api); app.route("/rss", rss); +app.route("/entries", entries); app.route("/admin", admin); // Root path redirects to admin dashboard diff --git a/src/routes/entries.ts b/src/routes/entries.ts new file mode 100644 index 0000000..2c221ed --- /dev/null +++ b/src/routes/entries.ts @@ -0,0 +1,75 @@ +import { Context } from "hono"; +import { Env, FeedMetadata, EmailData } from "../types"; + +function escapeHtml(str: string): string { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +export async function handle(c: Context): Promise { + const env = c.env as unknown as Env; + const feedId = c.req.param("feedId"); + const receivedAt = parseInt(c.req.param("entryId"), 10); + + if (!feedId || isNaN(receivedAt)) { + return new Response("Not Found", { status: 404 }); + } + + const emailStorage = env.EMAIL_STORAGE; + + const feedMetadata = (await emailStorage.get( + `feed:${feedId}:metadata`, + "json", + )) as FeedMetadata | null; + if (!feedMetadata) { + return new Response("Feed not found", { status: 404 }); + } + + const metaEntry = feedMetadata.emails.find((e) => e.receivedAt === receivedAt); + if (!metaEntry) { + return new Response("Entry not found", { status: 404 }); + } + + const emailData = (await emailStorage.get( + metaEntry.key, + "json", + )) as EmailData | null; + if (!emailData) { + return new Response("Entry not found", { status: 404 }); + } + + const html = ` + + + + + ${escapeHtml(emailData.subject)} + + + +

${escapeHtml(emailData.subject)}

+
+
From:
${escapeHtml(emailData.from)}
+
Date:
${escapeHtml(new Date(emailData.receivedAt).toUTCString())}
+
+
${emailData.content}
+ +`; + + return new Response(html, { + status: 200, + headers: { + "Content-Type": "text/html; charset=utf-8", + "Content-Security-Policy": + "default-src 'none'; style-src 'unsafe-inline'; img-src *; frame-src 'none'", + }, + }); +}