feat(unsubscribe): RFC 8058 one-click unsubscribe on feed deletion
Capture each sender's List-Unsubscribe one-click URL during ingestion (stored per sender in feed metadata, mirroring the iconDomain pattern) and fire one-click POSTs via ctx.waitUntil when a feed is deleted, so newsletters stop mailing the now-dead address. Tracked with a new unsubscribes_sent counter surfaced on the status page and /api/stats. Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
eb12f21894
commit
3ad0188bc0
14 files changed
+558
-4
No files matched your search
@@ -548,3 +548,93 @@ describe("processEmail — feed icon", () => {
|
||||
).toMatchObject({ contentType: "image/png" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("processEmail — unsubscribe capture", () => {
|
||||
let env: ReturnType<typeof createMockEnv>;
|
||||
|
||||
beforeEach(async () => {
|
||||
env = createMockEnv();
|
||||
await env.EMAIL_STORAGE.put(
|
||||
`feed:${VALID_FEED_ID}:config`,
|
||||
JSON.stringify({}),
|
||||
);
|
||||
});
|
||||
|
||||
it("stores the one-click unsubscribe URL on the feed metadata, keyed by sender", async () => {
|
||||
await processEmail(
|
||||
makeInput({
|
||||
senders: ["[email protected]"],
|
||||
headers: {
|
||||
"list-unsubscribe": "<https://example.com/u?t=abc>",
|
||||
"list-unsubscribe-post": "List-Unsubscribe=One-Click",
|
||||
},
|
||||
}),
|
||||
env as any,
|
||||
);
|
||||
|
||||
const metadata = (await env.EMAIL_STORAGE.get(
|
||||
`feed:${VALID_FEED_ID}:metadata`,
|
||||
"json",
|
||||
)) as { unsubscribe?: Record<string, string> };
|
||||
expect(metadata.unsubscribe).toEqual({
|
||||
"[email protected]": "https://example.com/u?t=abc",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps one entry per sender and overwrites with the latest URL", async () => {
|
||||
await processEmail(
|
||||
makeInput({
|
||||
senders: ["[email protected]"],
|
||||
headers: {
|
||||
"list-unsubscribe": "<https://one.com/u/1>",
|
||||
"list-unsubscribe-post": "List-Unsubscribe=One-Click",
|
||||
},
|
||||
}),
|
||||
env as any,
|
||||
);
|
||||
await processEmail(
|
||||
makeInput({
|
||||
senders: ["[email protected]"],
|
||||
headers: {
|
||||
"list-unsubscribe": "<https://two.com/u/1>",
|
||||
"list-unsubscribe-post": "List-Unsubscribe=One-Click",
|
||||
},
|
||||
}),
|
||||
env as any,
|
||||
);
|
||||
await processEmail(
|
||||
makeInput({
|
||||
senders: ["[email protected]"],
|
||||
headers: {
|
||||
"list-unsubscribe": "<https://one.com/u/2>",
|
||||
"list-unsubscribe-post": "List-Unsubscribe=One-Click",
|
||||
},
|
||||
}),
|
||||
env as any,
|
||||
);
|
||||
|
||||
const metadata = (await env.EMAIL_STORAGE.get(
|
||||
`feed:${VALID_FEED_ID}:metadata`,
|
||||
"json",
|
||||
)) as { unsubscribe?: Record<string, string> };
|
||||
expect(metadata.unsubscribe).toEqual({
|
||||
"[email protected]": "https://one.com/u/2",
|
||||
"[email protected]": "https://two.com/u/1",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not store anything without the one-click Post header", async () => {
|
||||
await processEmail(
|
||||
makeInput({
|
||||
headers: { "list-unsubscribe": "<https://example.com/u/1>" },
|
||||
}),
|
||||
env as any,
|
||||
);
|
||||
|
||||
const metadata = (await env.EMAIL_STORAGE.get(
|
||||
`feed:${VALID_FEED_ID}:metadata`,
|
||||
"json",
|
||||
)) as { unsubscribe?: Record<string, string> };
|
||||
expect(metadata.unsubscribe).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user