feat(email): Weiterleiten + Erneut senden im Detail-Pane

Zwei Aktionen, die der existierende Reply-Pfad bisher nicht abdeckte:

1. Weiterleiten (Compose-Modal-Forward-Modus):
   - Neuer Button im EmailDetail, neben "Antworten"
   - ComposeEmailModal akzeptiert jetzt einen `forwardOf` prop und
     füllt das Formular im Forward-Stil vor:
     * To leer (User trägt selbst ein)
     * Subject mit "Fwd:"-Prefix
     * Body mit zitierten Headern (Von, An, Datum, Betreff) +
       Original-Text
   - Titel des Modals reagiert ("Antworten" / "Weiterleiten" /
     "Neue E-Mail")

2. Erneut senden (One-Click-Resend):
   - Neuer Button im EmailDetail; schickt die Mail nochmal an die
     ursprüngliche toAddresses (= die Stressfrei-Adresse selbst).
     Plesk routet dann gemäß der HEUTE hinterlegten Forwards –
     Use-Case: die Stressfrei-Forward-Adresse wurde nach Empfang
     umgestellt, der Empfang soll beim neuen Forward-Empfänger
     landen.
   - Confirm-Dialog erklärt den Vorgang und warnt explizit, dass
     Anhänge nicht erneut mit gesendet werden (Anhänge wären
     IMAP-Refetch, dafür "Weiterleiten" nutzen).
   - Toast-Feedback für Erfolg/Fehler.
   - Im TRASH-Folder wird der Resend-Button bewusst nicht
     eingeblendet (kein sinnvoller Use-Case dort).

Backend braucht keine neuen Endpoints – beide Aktionen nutzen die
bestehenden `stressfreiEmailApi.sendEmail` + `cachedEmailApi.getById`
(letztere für den Body, der ohnehin schon im Detail-View geladen ist).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 14:46:44 +02:00
parent 185b38dc55
commit f6df97226d
4 changed files with 135 additions and 3 deletions
@@ -26,6 +26,7 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
const [showCompose, setShowCompose] = useState(false);
const [showAssign, setShowAssign] = useState(false);
const [replyToEmail, setReplyToEmail] = useState<CachedEmail | null>(null);
const [forwardEmail, setForwardEmail] = useState<CachedEmail | null>(null);
// Such- und Filterzustand. Alle Filter sind AND-verknüpft im Backend.
const [searchQuery, setSearchQuery] = useState('');
@@ -193,14 +194,74 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
const handleReply = () => {
setReplyToEmail(emailDetail || null);
setForwardEmail(null);
setShowCompose(true);
};
const handleForward = () => {
setForwardEmail(emailDetail || null);
setReplyToEmail(null);
setShowCompose(true);
};
const handleNewEmail = () => {
setReplyToEmail(null);
setForwardEmail(null);
setShowCompose(true);
};
// "Erneut senden": die E-Mail an die ursprüngliche Empfänger-Adresse
// (= die Stressfrei-Adresse selbst) noch einmal schicken. Use-Case:
// wenn die Forwards der Stressfrei-Adresse zwischenzeitlich auf eine
// andere Kunden-E-Mail umgestellt wurden, kommt die alte Mail dort nicht
// an durch erneutes Senden ans Postfach läuft sie durch die jetzt
// aktuellen Forwards und landet beim neuen Empfänger.
const handleResend = async () => {
if (!emailDetail || !selectedAccount) return;
let toAddresses: string[] = [];
try {
const parsed = JSON.parse(emailDetail.toAddresses || '[]');
if (Array.isArray(parsed)) toAddresses = parsed;
} catch {
// Fallback: bekannte Mailbox-Adresse
if (selectedAccount.email) toAddresses = [selectedAccount.email];
}
if (toAddresses.length === 0 && selectedAccount.email) {
toAddresses = [selectedAccount.email];
}
const hasAttachments = emailDetail.hasAttachments;
const lines = [
`Diese E-Mail erneut an ${toAddresses.join(', ')} senden?`,
'',
'Die Mail wird via SMTP wieder ans Postfach zugestellt und nimmt den',
'Weg durch die AKTUELL hinterlegten Forwards damit landet sie bei',
'dem heute hinterlegten Empfänger (auch wenn er sich seit dem',
'Original-Empfang geändert hat).',
];
if (hasAttachments) {
lines.push('', '⚠ Hinweis: Anhänge werden NICHT erneut versendet. Wenn du Anhänge brauchst, nutze stattdessen "Weiterleiten".');
}
if (!confirm(lines.join('\n'))) return;
try {
const subj = emailDetail.subject || '(Kein Betreff)';
await stressfreiEmailApi.sendEmail(selectedAccount.id, {
to: toAddresses,
subject: subj,
text: emailDetail.textBody || undefined,
html: emailDetail.htmlBody || undefined,
});
toast.success('E-Mail wurde erneut an ' + toAddresses.join(', ') + ' gesendet.');
// Gesendet-Ordner-Counts aktualisieren
queryClient.invalidateQueries({ queryKey: ['folder-counts', selectedAccountId] });
queryClient.invalidateQueries({ queryKey: ['emails'] });
} catch (err: any) {
toast.error(err?.response?.data?.error || err?.message || 'Fehler beim erneuten Senden');
}
};
const handleAssignContract = () => {
setShowAssign(true);
};
@@ -551,6 +612,8 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
<EmailDetail
email={emailDetail}
onReply={handleReply}
onForward={handleForward}
onResend={selectedFolder !== 'TRASH' ? handleResend : undefined}
onAssignContract={handleAssignContract}
onDeleted={() => {
setSelectedEmail(null);
@@ -582,9 +645,11 @@ export default function EmailClientTab({ customerId }: EmailClientTabProps) {
onClose={() => {
setShowCompose(false);
setReplyToEmail(null);
setForwardEmail(null);
}}
account={selectedAccount}
replyTo={replyToEmail || undefined}
forwardOf={forwardEmail || undefined}
onSuccess={() => {
// Gesendete E-Mails aktualisieren
queryClient.invalidateQueries({ queryKey: ['emails', 'customer', customerId, selectedAccountId, 'SENT'] });