From 71a5c20e62eaa0840b3f8b69f8539e8c1f81fcc6 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Wed, 20 May 2026 23:05:26 +0200 Subject: [PATCH] refactor: parallelize KV writes, cap metadata, remove redundant normalization - email-processor: run email put + metadata get in parallel (saves one KV round-trip per email) - email-processor: add missing 50-email metadata cap (was unbounded unlike storage.ts) - email-processor: remove redundant normalizeEmail call on already-normalized allowedSender - email-parser: strip WHAT-comments and boilerplate JSDoc throughout Co-Authored-By: Claude Sonnet 4.6 --- src/lib/email-processor.ts | 33 ++++++++++++++------------ src/utils/email-parser.ts | 48 ++++---------------------------------- 2 files changed, 22 insertions(+), 59 deletions(-) diff --git a/src/lib/email-processor.ts b/src/lib/email-processor.ts index a75ca3c..6fa8925 100644 --- a/src/lib/email-processor.ts +++ b/src/lib/email-processor.ts @@ -17,21 +17,20 @@ function normalizeEmail(value: string): string { function senderMatchesAllowlist( sender: string, - allowedSender: string, + allowedSender: string, // already normalized by caller ): boolean { + if (!allowedSender) return false; + const normalizedSender = normalizeEmail(sender); - const normalizedAllowed = normalizeEmail(allowedSender); - if (!normalizedAllowed) return false; - - if (normalizedAllowed.includes("@")) { - return normalizedSender === normalizedAllowed; + if (allowedSender.includes("@")) { + return normalizedSender === allowedSender; } const senderDomain = normalizedSender.split("@")[1] || ""; - const normalizedDomain = normalizedAllowed.startsWith("@") - ? normalizedAllowed.slice(1) - : normalizedAllowed; + const normalizedDomain = allowedSender.startsWith("@") + ? allowedSender.slice(1) + : allowedSender; return senderDomain === normalizedDomain; } @@ -84,13 +83,14 @@ export async function processEmail( }; const emailKey = `feed:${feedId}:${Date.now()}`; - await env.EMAIL_STORAGE.put(emailKey, JSON.stringify(emailData)); - const feedMetadataKey = `feed:${feedId}:metadata`; - const feedMetadata = ((await env.EMAIL_STORAGE.get( - feedMetadataKey, - "json", - )) || { + + const [, rawMetadata] = await Promise.all([ + env.EMAIL_STORAGE.put(emailKey, JSON.stringify(emailData)), + env.EMAIL_STORAGE.get(feedMetadataKey, "json"), + ]); + + const feedMetadata = ((rawMetadata as FeedMetadata | null) || { emails: [], }) as FeedMetadata; feedMetadata.emails.unshift({ @@ -98,6 +98,9 @@ export async function processEmail( subject: emailData.subject, receivedAt: emailData.receivedAt, }); + if (feedMetadata.emails.length > 50) { + feedMetadata.emails = feedMetadata.emails.slice(0, 50); + } await env.EMAIL_STORAGE.put(feedMetadataKey, JSON.stringify(feedMetadata)); console.log(`Successfully processed email for feed ${feedId}`); diff --git a/src/utils/email-parser.ts b/src/utils/email-parser.ts index e94dcb1..ebd8478 100644 --- a/src/utils/email-parser.ts +++ b/src/utils/email-parser.ts @@ -1,48 +1,26 @@ import { EmailData } from "../types"; -/** - * Simple email parser specialized for ForwardEmail.net's webhook format - */ export class EmailParser { - /** - * Extract the feed ID from an email address - * @param emailAddress The email address (e.g., apple.mountain.42@domain.com) - * @returns The feed ID or null if not found - */ + // Matches noun1.noun2.XY (the feed ID format) before the @ symbol static extractFeedId(emailAddress: string): string | null { - // Match pattern for noun1.noun2.XY before the @ symbol const match = emailAddress.match(/^([a-z]+\.[a-z]+\.\d{2})@/i); return match ? match[1] : null; } - /** - * Parse email data from ForwardEmail.net's webhook payload - * @param payload ForwardEmail.net webhook payload - */ static parseForwardEmailPayload(payload: any): EmailData { if (!payload) { throw new Error("Missing or invalid webhook payload"); } - // Extract the "to" address - const toAddress = payload.recipients?.[0] || ""; - - // Extract the sender information using ForwardEmail's structure const fromAddress = payload.from?.text || (payload.from?.value?.[0]?.address ? `${payload.from.value[0].name || ""} <${payload.from.value[0].address}>` : "Unknown Sender"); - // Extract subject - let subject = payload.subject || "No Subject"; - // Decode any encoded words in the subject - subject = this.decodeEncodedWords(subject); - - // Get content, preferring HTML over plain text + const subject = this.decodeEncodedWords(payload.subject || "No Subject"); const content = payload.html || payload.text || ""; - // Create simple email data object return { subject, from: fromAddress, @@ -52,13 +30,9 @@ export class EmailParser { }; } - /** - * Extract headers from ForwardEmail payload - */ private static extractHeaders(payload: any): Record { const headers: Record = {}; - // Extract headers from headerLines if available if (payload.headerLines && Array.isArray(payload.headerLines)) { payload.headerLines.forEach((h: { key: string; line: string }) => { const key = h.key.toLowerCase(); @@ -67,9 +41,7 @@ export class EmailParser { .trim(); headers[key] = value; }); - } - // Or from headers string if provided - else if (typeof payload.headers === "string") { + } else if (typeof payload.headers === "string") { payload.headers.split(/\r?\n/).forEach((line: string) => { const match = line.match(/^([^:]+):\s*(.*)$/); if (match) { @@ -81,27 +53,19 @@ export class EmailParser { return headers; } - /** - * Decode RFC 2047 encoded words in headers - * @param text Text that may contain encoded words like =?UTF-8?Q?Hello_World?= - */ static decodeEncodedWords(text: string): string { if (!text) return ""; - // Simple RFC 2047 encoded-word decoder return text.replace( /=\?([^?]+)\?([BQ])\?([^?]+)\?=/gi, (_, charset, encoding, text) => { if (encoding.toUpperCase() === "B") { - // Base64 encoding try { - const decoded = atob(text); - return decoded; + return atob(text); } catch (e) { return text; } } else if (encoding.toUpperCase() === "Q") { - // Quoted-printable encoding return this.decodeQuotedPrintable(text.replace(/_/g, " ")); } return text; @@ -109,10 +73,6 @@ export class EmailParser { ); } - /** - * Decode quoted-printable encoded text - * @param text Quoted-printable encoded text - */ private static decodeQuotedPrintable(text: string): string { return text.replace(/=([0-9A-F]{2})/gi, (_, hex) => { return String.fromCharCode(parseInt(hex, 16));