feat: add daily digest email notification module
Build and Push Multi-Platform Images / build-and-push (push) Successful in 50s

- New DailyDigestModule with scheduled summary email for open dashboard items
- Extract StatsService from StatsController for reuse in digest
- Add DailyDigestEnabled, UserEmail, UserPreferredUsername to UserSettings entity
- Sync email/username from OIDC token on each get/update call
- Add dailyDigestEnabled to UserSettingsDto and update API
- Notifications tab in UserSettingsPage with enable toggle and "Jetzt senden" button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 15:57:10 +02:00
parent 029d5b351f
commit 52438ee11f
12 changed files with 366 additions and 86 deletions
@@ -19,6 +19,7 @@ export interface UserSettingsDto {
mailSignatureHtml: string | null;
defaultLabelTemplateId: number | null;
emailRecipientHistory: string[] | null;
dailyDigestEnabled: boolean;
}
@Injectable()
@@ -64,8 +65,16 @@ export class UserSettingsService {
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
}
async getSettings(userId: string): Promise<UserSettingsDto> {
const entity = await this.repo.findOne({ where: { UserId: userId } });
async getSettings(userId: string, email?: string, preferredUsername?: string): Promise<UserSettingsDto> {
let entity = await this.repo.findOne({ where: { UserId: userId } });
if (email || preferredUsername) {
if (!entity) {
entity = this.repo.create({ UserId: userId });
}
if (email) entity.UserEmail = email;
if (preferredUsername) entity.UserPreferredUsername = preferredUsername;
await this.repo.save(entity);
}
return this.toDto(entity);
}
@@ -82,7 +91,10 @@ export class UserSettingsService {
mailSignatureHtml?: string | null;
defaultLabelTemplateId?: number | null;
emailRecipientHistory?: string[] | null;
dailyDigestEnabled?: boolean;
},
email?: string,
preferredUsername?: string,
): Promise<UserSettingsDto> {
let entity = await this.repo.findOne({ where: { UserId: userId } });
if (!entity) {
@@ -101,6 +113,9 @@ export class UserSettingsService {
if (data.mailSignatureHtml !== undefined) entity.MailSignatureHtml = data.mailSignatureHtml;
if (data.defaultLabelTemplateId !== undefined) entity.DefaultLabelTemplateId = data.defaultLabelTemplateId;
if (data.emailRecipientHistory !== undefined) entity.EmailRecipientHistory = data.emailRecipientHistory;
if (data.dailyDigestEnabled !== undefined) entity.DailyDigestEnabled = data.dailyDigestEnabled;
if (email) entity.UserEmail = email;
if (preferredUsername) entity.UserPreferredUsername = preferredUsername;
await this.repo.save(entity);
return this.toDto(entity);
@@ -144,6 +159,12 @@ export class UserSettingsService {
};
}
async findAllDigestSubscribers(): Promise<UserSettings[]> {
return this.repo.find({
where: { DailyDigestEnabled: true },
}).then(rows => rows.filter(r => !!r.UserEmail));
}
async getAvailableSenders(userId: string): Promise<{ id: string; label: string }[]> {
const defaultEmail = this.configService.get<string>('SMTP_FROM', 'paperless@localhost');
const defaultName = this.configService.get<string>('SMTP_FROM_NAME', '');
@@ -172,6 +193,7 @@ export class UserSettingsService {
mailSignatureHtml: entity?.MailSignatureHtml ?? null,
defaultLabelTemplateId: entity?.DefaultLabelTemplateId ?? null,
emailRecipientHistory: entity?.EmailRecipientHistory ?? null,
dailyDigestEnabled: entity?.DailyDigestEnabled ?? false,
};
}
}