From 0a0cbe0e530bc4c2c67aff1091ed8f6ede8186b0 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Mon, 27 Jul 2026 16:26:21 +0200 Subject: [PATCH] =?UTF-8?q?Spam-Tab:=20Anh=C3=A4nge=20aus=20dem=20echten?= =?UTF-8?q?=20Junk-Ordner=20holen=20(Pentest=20R124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beim Spam-Feature wurden moveEmailToTrash/restoreEmailFromTrash auf den echten Junk-Pfad umgestellt, vier Attachment-Funktionen im Controller aber nicht: 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: bestenfalls 404, schlimmstenfalls (UID-Kollision INBOX vs. Junk) der FALSCHE Anhang aus INBOX – der dann z.B. als Rechnung/Vertragsdokument abgelegt würde. Kein Cross-Customer-Leak (gleicher Kunde/Postfach), aber Datenintegritätsproblem. Fix: an allen vier Stellen dieselbe Junk-Pfad-Logik wie in moveEmailToTrash (email.folder === 'SPAM' → findJunkFolderPath). findJunkFolderPath war in dem Controller noch nicht importiert. Vom Pentester (R124) gefunden – beim ursprünglichen Feature übersehen. Co-Authored-By: Claude Opus 4.7 --- .../src/controllers/cachedEmail.controller.ts | 58 +++++++++++++++++-- docs/todo.md | 15 +++++ 2 files changed, 68 insertions(+), 5 deletions(-) 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