Separate the two feed identities so the public read URL never reveals the inbound address and vice-versa: - FeedId becomes an opaque high-entropy token (read id + KV key); MailboxId (noun.noun.NN) owns the inbound address and the untrusted-input boundary via MailboxId.parse. They map only through the inbound:<mailbox> secondary index, resolved solely at reception. - inbound index lifecycle is owned by FeedRepository: written by save/saveConfig, dropped by removeFromList(Bulk) — symmetric, never mirrored by hand (removes the manual delete in feed-service + the cron loop, and a silent empty-catch). - Feed.mailboxId exposes a MailboxId VO (symmetry with Feed.id); the mailbox@domain shape lives on MailboxId.emailAddress(domain). - Distinguish mailbox_unknown (no feed claims the address) from feed_not_found (dangling index) for observability; both forwardable, both 404. - Drop the redundant EmailParser.extractMailbox pass-through so MailboxId.parse is the single parse boundary. Docs (README/INSTALL/CLAUDE.md/landing) and tests updated; 439 tests green, tsc clean, build dry-run OK. Co-Authored-By: Claude Opus 4.7 <[email protected]>
39 lines
1.2 KiB
TypeScript
39 lines
1.2 KiB
TypeScript
import { Env, FeedConfig, EmailData } from "../types";
|
|
import { MAX_FEED_ITEMS } from "../config/constants";
|
|
import { FeedRepository } from "../infrastructure/feed-repository";
|
|
import { FeedId } from "../domain/value-objects/feed-id";
|
|
|
|
export interface FeedData {
|
|
feedConfig: FeedConfig;
|
|
emails: EmailData[];
|
|
}
|
|
|
|
export async function fetchFeedData(
|
|
feedId: FeedId,
|
|
env: Env,
|
|
): Promise<FeedData | null> {
|
|
const repo = FeedRepository.from(env);
|
|
|
|
const feedMetadata = await repo.getMetadata(feedId);
|
|
if (!feedMetadata) return null;
|
|
|
|
const feedConfig = (await repo.getConfig(feedId)) ?? {
|
|
title: `Newsletter Feed ${feedId.value}`,
|
|
description: "Converted email newsletter",
|
|
language: "en",
|
|
// Read-model fallback only: the RSS/Atom/JSON path never builds the inbound
|
|
// address, so an empty mailbox is inert here (the real one lives on config).
|
|
mailbox_id: "",
|
|
created_at: Date.now(),
|
|
};
|
|
|
|
const emailRefs = feedMetadata.emails.slice(0, MAX_FEED_ITEMS);
|
|
const emails: EmailData[] = [];
|
|
for (const ref of emailRefs) {
|
|
const data = await repo.getEmail(ref.key);
|
|
if (data) emails.push(data);
|
|
}
|
|
|
|
return { feedConfig, emails };
|
|
}
|