mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
feat: store email attachments in R2 and expose as RSS enclosures
Attachments from incoming emails are uploaded to an optional Cloudflare R2
bucket and exposed as <enclosure> elements in RSS and <link rel="enclosure">
in Atom feeds, served at /files/{id}/{filename} with immutable caching.
R2 is opt-in: if ATTACHMENT_BUCKET is not bound the feature is a no-op.
Attachments are cleaned up from R2 on email/feed deletion and during
size-based feed trimming. Adds MockR2 to the test setup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+37
-2
@@ -1,6 +1,16 @@
|
||||
import { EmailParser } from "../utils/email-parser";
|
||||
import { Env } from "../types";
|
||||
import { processEmail } from "./email-processor";
|
||||
import { processEmail, RawAttachment } from "./email-processor";
|
||||
|
||||
export interface ForwardEmailAttachment {
|
||||
filename?: string;
|
||||
contentType?: string;
|
||||
size?: number;
|
||||
content?:
|
||||
| { type: "Buffer"; data: number[] }
|
||||
| ArrayBuffer
|
||||
| ArrayBufferView;
|
||||
}
|
||||
|
||||
export interface ForwardEmailPayload {
|
||||
recipients?: string[];
|
||||
@@ -17,7 +27,7 @@ export interface ForwardEmailPayload {
|
||||
headerLines?: Array<{ key: string; line: string }>;
|
||||
headers?: string;
|
||||
raw?: string;
|
||||
attachments?: Array<any>;
|
||||
attachments?: ForwardEmailAttachment[];
|
||||
}
|
||||
|
||||
function normalizeEmail(value: string): string {
|
||||
@@ -41,12 +51,36 @@ function extractSenderAddresses(payload: ForwardEmailPayload): string[] {
|
||||
return Array.from(new Set(matches.map(normalizeEmail)));
|
||||
}
|
||||
|
||||
function toArrayBuffer(
|
||||
content: ForwardEmailAttachment["content"],
|
||||
): ArrayBuffer | null {
|
||||
if (!content) return null;
|
||||
if (content instanceof ArrayBuffer) return content;
|
||||
if (ArrayBuffer.isView(content)) return (content as ArrayBufferView).buffer as ArrayBuffer;
|
||||
if (typeof content === "object" && content.type === "Buffer" && Array.isArray(content.data)) {
|
||||
return Uint8Array.from(content.data).buffer as ArrayBuffer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function handleForwardEmail(
|
||||
payload: ForwardEmailPayload,
|
||||
env: Env,
|
||||
): Promise<Response> {
|
||||
const emailData = EmailParser.parseForwardEmailPayload(payload);
|
||||
|
||||
const rawAttachments: RawAttachment[] = (payload.attachments ?? [])
|
||||
.map((a) => {
|
||||
const buffer = toArrayBuffer(a.content);
|
||||
if (!buffer) return null;
|
||||
return {
|
||||
filename: a.filename || "attachment",
|
||||
contentType: a.contentType || "application/octet-stream",
|
||||
content: buffer,
|
||||
};
|
||||
})
|
||||
.filter((a): a is RawAttachment => a !== null);
|
||||
|
||||
return processEmail(
|
||||
{
|
||||
toAddress: payload.recipients?.[0] || "",
|
||||
@@ -56,6 +90,7 @@ export async function handleForwardEmail(
|
||||
content: emailData.content,
|
||||
receivedAt: emailData.receivedAt,
|
||||
headers: emailData.headers,
|
||||
attachments: rawAttachments,
|
||||
},
|
||||
env,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user