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:
@@ -21,6 +21,18 @@ const mockEmails: EmailData[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const mockEmailWithAttachment: EmailData = {
|
||||
...mockEmails[0],
|
||||
attachments: [
|
||||
{
|
||||
id: "550e8400-e29b-41d4-a716-446655440000",
|
||||
filename: "report.pdf",
|
||||
contentType: "application/pdf",
|
||||
size: 12345,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const BASE_URL = "https://test.getmynews.app";
|
||||
const FEED_ID = "abc123";
|
||||
|
||||
@@ -31,6 +43,25 @@ describe("generateRssFeed", () => {
|
||||
expect(result).toContain("<title>Test Newsletter</title>");
|
||||
});
|
||||
|
||||
it("includes <enclosure> element for email with attachment", () => {
|
||||
const result = generateRssFeed(mockFeedConfig, [mockEmailWithAttachment], BASE_URL, FEED_ID);
|
||||
expect(result).toContain("<enclosure");
|
||||
expect(result).toContain("550e8400-e29b-41d4-a716-446655440000");
|
||||
expect(result).toContain("report.pdf");
|
||||
expect(result).toContain("application/pdf");
|
||||
expect(result).toContain("12345");
|
||||
});
|
||||
|
||||
it("does not include <enclosure> for email without attachments", () => {
|
||||
const result = generateRssFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID);
|
||||
expect(result).not.toContain("<enclosure");
|
||||
});
|
||||
|
||||
it("enclosure URL uses /files/{id}/{filename} scheme", () => {
|
||||
const result = generateRssFeed(mockFeedConfig, [mockEmailWithAttachment], BASE_URL, FEED_ID);
|
||||
expect(result).toContain(`${BASE_URL}/files/550e8400-e29b-41d4-a716-446655440000/report.pdf`);
|
||||
});
|
||||
|
||||
it("includes rss self-link in RSS output", () => {
|
||||
const result = generateRssFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID);
|
||||
expect(result).toContain(`${BASE_URL}/rss/${FEED_ID}`);
|
||||
@@ -104,4 +135,16 @@ describe("generateAtomFeed", () => {
|
||||
const result = generateAtomFeed(configWithAuthor, mockEmails, BASE_URL, FEED_ID);
|
||||
expect(result).toContain("Bob");
|
||||
});
|
||||
|
||||
it("includes enclosure link for email with attachment in Atom feed", () => {
|
||||
const result = generateAtomFeed(mockFeedConfig, [mockEmailWithAttachment], BASE_URL, FEED_ID);
|
||||
expect(result).toContain('rel="enclosure"');
|
||||
expect(result).toContain("550e8400-e29b-41d4-a716-446655440000");
|
||||
expect(result).toContain("report.pdf");
|
||||
});
|
||||
|
||||
it("does not include enclosure link for email without attachments in Atom feed", () => {
|
||||
const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID);
|
||||
expect(result).not.toContain('rel="enclosure"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,7 @@ function buildFeed(
|
||||
|
||||
for (const email of emails) {
|
||||
const uniqueId = `${email.receivedAt}-${Buffer.from(email.subject).toString("base64").substring(0, 10)}`;
|
||||
const firstAttachment = email.attachments?.[0];
|
||||
feed.addItem({
|
||||
title: email.subject,
|
||||
id: uniqueId,
|
||||
@@ -50,6 +51,13 @@ function buildFeed(
|
||||
content: email.content,
|
||||
author: [parseFromAddress(email.from)],
|
||||
date: new Date(email.receivedAt),
|
||||
enclosure: firstAttachment
|
||||
? {
|
||||
url: `${baseUrl}/files/${firstAttachment.id}/${encodeURIComponent(firstAttachment.filename)}`,
|
||||
type: firstAttachment.contentType,
|
||||
length: firstAttachment.size,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user