feat(260929-if2): Erinnerung zusaetzlich per E-Mail, Doku und Aenderungsliste

- E-Mail-Planer: Anspruch vor dem Senden (genau eine Mail je Faelligkeit, hoechstens 3 Versuche), Systemlesen nur fuer die Kandidatenabfrage
- MailService.sendReminderEmail (Berliner Zeit, nur Text), GET /reminders/email-status, Haken im Formular mit Erklaerung
- Zugriffsklassifikation und Erlaubnisliste fuer forSystem nachgezogen, Aenderungsliste und Anwenderanleitung

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 14:10:06 +02:00
parent 709b41a007
commit 6879c756f2
21 changed files with 1038 additions and 21 deletions
+47
View File
@@ -316,4 +316,51 @@ export class MailService {
await this.sendViaTenantTransport(tenantId, { to: email, subject, text }, 'Welcome');
}
/**
* Erinnerungs-E-Mail (quick-260929-if2): eine Mail je faelliger Erinnerung an
* die eigene Adresse des Besitzers, ueber den SMTP-Transport seines Mandanten.
* Gibt `true` zurueck, wenn der Versand gelang, `false` bei einem
* Transportfehler — der Planer (`ReminderMailScheduler`) entscheidet daran,
* ob er den Anspruch wieder freigibt (E-04). Wirft nie.
*
* Nur Text, kein HTML (T-IF2-05, kein HTML-Einschleusen). Der Betreff hat
* Zeilenumbrueche durch Leerzeichen ersetzt (Header-Einschleusung) und ist
* auf 150 Zeichen gekuerzt. Die Zeit steht in `Europe/Berlin` (E-07): im
* Benutzer ist keine Zeitzone gespeichert, das Haus arbeitet in deutscher Zeit.
*/
async sendReminderEmail(
tenantId: string,
to: string,
reminder: { title: string; description: string; dueAt: Date },
): Promise<boolean> {
const when = `${new Intl.DateTimeFormat('de-DE', {
timeZone: 'Europe/Berlin',
dateStyle: 'full',
timeStyle: 'short',
}).format(reminder.dueAt)} Uhr`;
const subject = `Erinnerung: ${reminder.title}`.replace(/[\r\n]+/g, ' ').slice(0, 150);
const lines = [
'Guten Tag,',
'',
`Sie haben in Tessera eine Erinnerung für ${when} gesetzt:`,
'',
reminder.title,
];
if (reminder.description.trim() !== '') {
lines.push('', reminder.description);
}
lines.push('', this.appUrl);
try {
await this.deliver(tenantId, { to, subject, text: lines.join('\n') }, 'Reminder');
return true;
} catch (error) {
this.logger.error(
`Failed to send Reminder email to ${to}`,
error instanceof Error ? error.stack : String(error),
);
return false;
}
}
}