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:
Julien HerrandClaude Opus 4.7 committed 2026-05-23 14:35:05 +02:00
1 parent eb12f21894
commit 3ad0188bc0
14 files changed
+558 -4

No files matched your search

+14
View File
@@ -12,6 +12,7 @@ import {
cacheFaviconForDomain,
extractEmailDomain,
} from "../utils/favicon-fetcher";
import { parseOneClickUnsubscribe } from "../utils/unsubscribe";
import { logger } from "./logger";
import { FEED_MAX_BYTES } from "../config/constants";
@@ -223,6 +224,19 @@ export async function storeEmail(
feedMetadata.iconDomain = iconDomain;
}
// Capture the sender's RFC 8058 one-click unsubscribe link so we can stop the
// newsletter when the feed is deleted. Keyed by sender: each newsletter on the
// feed keeps its own entry, and a repeat send overwrites with the latest URL.
const unsubUrl = parseOneClickUnsubscribe(input.headers ?? {});
if (unsubUrl) {
const senderKey =
input.senders[0] || extractEmailDomain(input.from) || input.from;
feedMetadata.unsubscribe = {
...(feedMetadata.unsubscribe ?? {}),
[senderKey]: unsubUrl,
};
}
let totalSize = feedMetadata.emails.reduce(
(sum, e) => sum + (e.size ?? 0),
0,