diff --git a/backend/src/controllers/cachedEmail.controller.ts b/backend/src/controllers/cachedEmail.controller.ts index 6024c307..85b20e2d 100644 --- a/backend/src/controllers/cachedEmail.controller.ts +++ b/backend/src/controllers/cachedEmail.controller.ts @@ -5,7 +5,7 @@ import * as cachedEmailService from '../services/cachedEmail.service.js'; import * as stressfreiEmailService from '../services/stressfreiEmail.service.js'; import * as invoiceService from '../services/invoice.service.js'; import { sendEmail, SmtpCredentials, SendEmailParams, EmailAttachment } from '../services/smtpService.js'; -import { fetchAttachment, appendToSent, ImapCredentials, humanizeImapError } from '../services/imapService.js'; +import { fetchAttachment, appendToSent, ImapCredentials, humanizeImapError, findJunkFolderPath } from '../services/imapService.js'; import { getImapSmtpSettings } from '../services/emailProvider/emailProviderService.js'; import { decrypt } from '../utils/encryption.js'; import { sanitizeNotes, stripHtml, validateContractDocumentType, validateOptionalIsoDate, assertSafePdf } from '../utils/sanitize.js'; @@ -740,7 +740,19 @@ export async function downloadAttachment(req: AuthRequest, res: Response): Promi }; // Ordner basierend auf E-Mail-Typ bestimmen (INBOX oder Sent) - const imapFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX'; + // SPAM-Mails liegen im Junk-Ordner (Name variiert je Server) – IMAP-UIDs + // sind pro Ordner vergeben, daher MUSS der echte Junk-Pfad ermittelt + // werden, sonst wird der Anhang im falschen Ordner gesucht (404 oder, + // bei UID-Kollision, der falsche Anhang aus INBOX). Gleiches Muster wie + // cachedEmail.service.ts::moveEmailToTrash. + let imapFolder: string; + if (email.folder === 'SENT') { + imapFolder = 'Sent'; + } else if (email.folder === 'SPAM') { + imapFolder = (await findJunkFolderPath(credentials)) || 'Junk'; + } else { + imapFolder = 'INBOX'; + } // Anhang per IMAP abrufen const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder); @@ -1405,7 +1417,19 @@ export async function saveAttachmentTo(req: AuthRequest, res: Response): Promise }; // Ordner basierend auf E-Mail-Typ bestimmen (INBOX oder Sent) - const imapFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX'; + // SPAM-Mails liegen im Junk-Ordner (Name variiert je Server) – IMAP-UIDs + // sind pro Ordner vergeben, daher MUSS der echte Junk-Pfad ermittelt + // werden, sonst wird der Anhang im falschen Ordner gesucht (404 oder, + // bei UID-Kollision, der falsche Anhang aus INBOX). Gleiches Muster wie + // cachedEmail.service.ts::moveEmailToTrash. + let imapFolder: string; + if (email.folder === 'SENT') { + imapFolder = 'Sent'; + } else if (email.folder === 'SPAM') { + imapFolder = (await findJunkFolderPath(credentials)) || 'Junk'; + } else { + imapFolder = 'INBOX'; + } // Anhang per IMAP abrufen const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder); @@ -2211,7 +2235,19 @@ export async function saveAttachmentAsInvoice(req: AuthRequest, res: Response): }; // IMAP-Ordner bestimmen - const imapFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX'; + // SPAM-Mails liegen im Junk-Ordner (Name variiert je Server) – IMAP-UIDs + // sind pro Ordner vergeben, daher MUSS der echte Junk-Pfad ermittelt + // werden, sonst wird der Anhang im falschen Ordner gesucht (404 oder, + // bei UID-Kollision, der falsche Anhang aus INBOX). Gleiches Muster wie + // cachedEmail.service.ts::moveEmailToTrash. + let imapFolder: string; + if (email.folder === 'SENT') { + imapFolder = 'Sent'; + } else if (email.folder === 'SPAM') { + imapFolder = (await findJunkFolderPath(credentials)) || 'Junk'; + } else { + imapFolder = 'INBOX'; + } // Anhang vom IMAP-Server laden const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder); @@ -2370,7 +2406,19 @@ export async function saveAttachmentAsContractDocument(req: AuthRequest, res: Re allowSelfSignedCerts: settings.allowSelfSignedCerts, }; - const imapFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX'; + // SPAM-Mails liegen im Junk-Ordner (Name variiert je Server) – IMAP-UIDs + // sind pro Ordner vergeben, daher MUSS der echte Junk-Pfad ermittelt + // werden, sonst wird der Anhang im falschen Ordner gesucht (404 oder, + // bei UID-Kollision, der falsche Anhang aus INBOX). Gleiches Muster wie + // cachedEmail.service.ts::moveEmailToTrash. + let imapFolder: string; + if (email.folder === 'SENT') { + imapFolder = 'Sent'; + } else if (email.folder === 'SPAM') { + imapFolder = (await findJunkFolderPath(credentials)) || 'Junk'; + } else { + imapFolder = 'INBOX'; + } const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder); if (!attachment) { diff --git a/docs/todo.md b/docs/todo.md index 14a6ebf7..0c7c62f2 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,21 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🐞 Spam-Tab: Anhänge aus Junk-Ordner (Pentest R124-Fund)** + - Beim Spam-Feature wurden `moveEmailToTrash`/`restoreEmailFromTrash` + auf den echten Junk-Pfad umgestellt, aber vier Attachment-Funktionen + im Controller (`downloadAttachment`, `saveAttachmentTo`, + `saveAttachmentAsInvoice`, `saveAttachmentAsContractDocument`) + bestimmten den IMAP-Ordner weiter hart als + `email.folder === 'SENT' ? 'Sent' : 'INBOX'`. Für SPAM-Mails landete + das fälschlich auf INBOX. Da IMAP-UIDs pro Ordner vergeben sind: + 404 (Anhang nicht gefunden) oder – bei UID-Kollision – der falsche + Anhang aus INBOX (z.B. als Rechnung/Vertragsdokument abgelegt). + Kein Cross-Customer-Leak (bleibt im selben Postfach), aber echtes + Datenintegritätsproblem. + - Fix: an allen vier Stellen dieselbe Junk-Pfad-Logik wie in + `moveEmailToTrash` + `findJunkFolderPath` importiert. + - [x] **📧 E-Mail-Client: Spam-Ordner als eigener Tab** - Neuer Tab „Spam" (zwischen Gesendet und Papierkorb) im E-Mail-Client, zeigt den Junk-/Spam-Ordner des gewählten Postfachs – damit