feat(feed): optional per-feed sender-in-title toggle

Add a per-feed senderInTitle flag (domain FeedState.senderInTitle ↔
FeedConfig.sender_in_title). When set, the feed generator prefixes each
entry title with [Sender] (display name, falling back to the address).
Exposed as an admin edit-form checkbox and across the REST API
create/update/response schemas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-25 15:48:31 +02:00
parent 7086526670
commit e86beeeb8a
14 changed files with 234 additions and 2 deletions
+31
View File
@@ -147,6 +147,37 @@ describe("generateRssFeed", () => {
expect(result).not.toContain("<item>");
});
it("leaves the item title unprefixed by default", () => {
const result = generateRssFeed(
mockFeedConfig,
mockEmails,
BASE_URL,
FEED_ID,
);
expect(result).toContain("Hello World");
expect(result).not.toContain("[Alice]");
});
it("prefixes the item title with the sender when sender_in_title is set", () => {
const result = generateRssFeed(
{ ...mockFeedConfig, sender_in_title: true },
mockEmails,
BASE_URL,
FEED_ID,
);
expect(result).toContain("[Alice] Hello World");
});
it("falls back to the email address when the sender has no display name", () => {
const result = generateRssFeed(
{ ...mockFeedConfig, sender_in_title: true },
[{ ...mockEmails[0], from: "bob@example.com" }],
BASE_URL,
FEED_ID,
);
expect(result).toContain("[bob@example.com] Hello World");
});
it("feed link points to the public read URL, never an admin path", () => {
const result = generateRssFeed(
mockFeedConfig,
+5 -1
View File
@@ -74,8 +74,12 @@ function buildFeed(
baseUrl,
EmailAddress.parse(email.from)?.siteBaseUrl() ?? "",
);
const subject = htmlToText(email.subject);
const title = feedConfig.sender_in_title
? `[${parseFromAddress(email.from).name ?? email.from}] ${subject}`
: subject;
feed.addItem({
title: htmlToText(email.subject),
title,
id: entryUrl,
link: entryUrl,
description: bodyContent,
+2
View File
@@ -18,6 +18,7 @@ export function fromConfigDTO(dto: FeedConfig): FeedState {
language: dto.language,
mailboxId: dto.mailbox_id,
author: dto.author,
senderInTitle: dto.sender_in_title,
allowedSenders: dto.allowed_senders ?? [],
blockedSenders: dto.blocked_senders ?? [],
createdAt: dto.created_at,
@@ -34,6 +35,7 @@ export function toConfigDTO(state: FeedState): FeedConfig {
language: state.language,
mailbox_id: state.mailboxId,
author: state.author,
sender_in_title: state.senderInTitle,
allowed_senders: state.allowedSenders,
blocked_senders: state.blockedSenders,
created_at: state.createdAt,