diff --git a/workers/index.ts b/workers/index.ts index fd3359ce..23cff0e3 100644 --- a/workers/index.ts +++ b/workers/index.ts @@ -329,6 +329,25 @@ app.get("/api/v1/mailboxes/:mailboxId/emails/:emailId/attachments/:attachmentId" const MAX_EMAIL_SIZE = 25 * 1024 * 1024; +type IncomingEmailMessage = { + raw: ReadableStream; + rawSize: number; + to?: string; +}; + +function normalizeAddress(address: string | null | undefined) { + const normalized = address?.trim().toLowerCase(); + return normalized || undefined; +} + +function parsedAddressList( + addresses: Array<{ address?: string | null }> | undefined, +) { + return (addresses + ?.map((entry) => normalizeAddress(entry.address)) + .filter((address): address is string => Boolean(address))) ?? []; +} + async function streamToArrayBuffer(stream: ReadableStream, streamSize: number) { if (streamSize > MAX_EMAIL_SIZE) throw new Error(`Email too large: ${streamSize} bytes exceeds ${MAX_EMAIL_SIZE} byte limit`); if (streamSize <= 0) throw new Error(`Invalid stream size: ${streamSize}`); @@ -345,22 +364,29 @@ async function streamToArrayBuffer(stream: ReadableStream, streamSize: number) { return result; } -async function receiveEmail(event: { raw: ReadableStream; rawSize: number }, env: Env, ctx: ExecutionContext) { +async function receiveEmail(event: IncomingEmailMessage, env: Env, ctx: ExecutionContext) { const rawEmail = await streamToArrayBuffer(event.raw, event.rawSize); const parsedEmail = await new PostalMime().parse(rawEmail); - if (!parsedEmail.to?.length || !parsedEmail.to[0].address) throw new Error("received email with empty to"); + const envelopeRecipient = normalizeAddress(event.to); + const allRecipients = parsedAddressList(parsedEmail.to); + if (!envelopeRecipient && allRecipients.length === 0) throw new Error("received email with empty to"); - const allowedAddresses = ((env.EMAIL_ADDRESSES ?? []) as string[]).map((a) => a.toLowerCase()); - const allRecipients = parsedEmail.to.map((t) => t.address?.toLowerCase()).filter(Boolean) as string[]; - const ccRecipients = (parsedEmail.cc || []).map((e) => e.address?.toLowerCase()).filter(Boolean) as string[]; - const bccRecipients = (parsedEmail.bcc || []).map((e) => e.address?.toLowerCase()).filter(Boolean) as string[]; + const allowedAddresses = ((env.EMAIL_ADDRESSES ?? []) as string[]) + .map((a) => normalizeAddress(a)) + .filter(Boolean) as string[]; + const ccRecipients = parsedAddressList(parsedEmail.cc); + const bccRecipients = parsedAddressList(parsedEmail.bcc); let mailboxId: string | undefined; if (allowedAddresses.length > 0) { - mailboxId = allRecipients.find((addr) => allowedAddresses.includes(addr)); - if (!mailboxId) { console.log(`Ignoring email: no recipient matches EMAIL_ADDRESSES.`); return; } - } else { mailboxId = allRecipients[0]; } + const routingRecipients = envelopeRecipient ? [envelopeRecipient] : allRecipients; + mailboxId = routingRecipients.find((addr) => allowedAddresses.includes(addr)); + if (!mailboxId) { + console.log(`Ignoring email for ${envelopeRecipient ?? allRecipients.join(", ")}: recipient is not configured in EMAIL_ADDRESSES.`); + return; + } + } else { mailboxId = envelopeRecipient ?? allRecipients[0]; } if (!mailboxId) throw new Error("received email with no valid recipient address"); const messageId = crypto.randomUUID(); @@ -394,7 +420,7 @@ async function receiveEmail(event: { raw: ReadableStream; rawSize: number }, env await stub.createEmail(Folders.INBOX, { id: messageId, subject: parsedEmail.subject || "", - sender: (parsedEmail.from?.address || "").toLowerCase(), recipient: allRecipients.join(", "), + sender: (parsedEmail.from?.address || "").toLowerCase(), recipient: (allRecipients.length > 0 ? allRecipients : [mailboxId]).join(", "), cc: ccRecipients.join(", ") || null, bcc: bccRecipients.join(", ") || null, date: new Date().toISOString(), // uses receive time, not the email's Date header body: parsedEmail.html || parsedEmail.text || "",