From dfe2a4b24160720c1729851acbb57cb874d24dd0 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 18 Jun 2026 18:24:44 +0200 Subject: [PATCH] Plesk-Sync: Auto-Import bei User-Remove deaktivieren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folge-Bug zu 194c864: User löscht Adresse im Modal → DB-Liste wird kürzer → Plesk-Sync läuft → Auto-Import sieht "c ist in Plesk aber nicht in DB" → schreibt c zurück in additionalForwardingEmails → Diff sagt nichts zu entfernen. Ursache: Auto-Import (Pentest 83.x) lief für alle Sync-Pfade. Beim Sync-Button ist Plesk→DB-Übernahme gewollt (Bestands- Migration). Beim User-Add/Remove ist die DB-Liste die explizite Intent – Auto-Import macht das User-Delete kaputt. syncForwardingForEmail(id, opts?: { autoImportPleskMembers? }) mit Default true (Sync-Button-Verhalten). setAdditionalForwards ruft mit false – entfernte Adressen verschwinden jetzt sauber auch beim Provider. --- .../src/services/stressfreiEmail.service.ts | 111 ++++++++++-------- docs/todo.md | 15 +++ 2 files changed, 77 insertions(+), 49 deletions(-) diff --git a/backend/src/services/stressfreiEmail.service.ts b/backend/src/services/stressfreiEmail.service.ts index 10e5a7bf..5678fbe5 100644 --- a/backend/src/services/stressfreiEmail.service.ts +++ b/backend/src/services/stressfreiEmail.service.ts @@ -350,8 +350,10 @@ export async function setAdditionalForwards( }); // Provider unmittelbar nachziehen, sonst läuft das Plesk-Mail-Konto - // mit der alten Liste weiter. - const syncResult = await syncForwardingForEmail(id); + // mit der alten Liste weiter. autoImport=false, weil unsere DB-Liste + // hier die explizite User-Intent ist – kein Plesk-Member-Auto-Pull, + // sonst landen gerade entfernte Adressen zurück in der Liste. + const syncResult = await syncForwardingForEmail(id, { autoImportPleskMembers: false }); // 71.4: Rollback wenn Plesk den Sync abgelehnt hat. DB darf nicht // den optimistischen Stand zeigen, wenn der Provider noch auf dem @@ -506,6 +508,7 @@ export async function getDecryptedPassword(id: number): Promise { // Einträge, bei denen das Flag nie gesetzt wurde, werden so geheilt). export async function syncForwardingForEmail( id: number, + options: { autoImportPleskMembers?: boolean } = {}, ): Promise<{ success: boolean; forwardTargets?: string[]; @@ -513,6 +516,14 @@ export async function syncForwardingForEmail( passwordReset?: boolean; error?: string; }> { + // Auto-Import übernimmt unbekannte Plesk-Members in unsere DB. Macht + // beim Sync-Button Sinn (Bestands-Migration), aber NICHT beim + // User-getriggerten Add/Remove – dort ist die DB-Liste die Wahrheit. + // Sonst kreisen entfernte Adressen zurück in die Liste: + // 1. User entfernt c → DB=[a,b], Plesk=[a,b,c] + // 2. Auto-Import: "c ist in Plesk aber nicht in DB → in DB schreiben" + // 3. → DB=[a,b,c], Diff sagt nichts zu löschen, Plesk bleibt [a,b,c]. + const autoImport = options.autoImportPleskMembers ?? true; const stressfreiEmail = await prisma.stressfreiEmail.findUnique({ where: { id }, select: { @@ -572,55 +583,57 @@ export async function syncForwardingForEmail( // als Forwarding-Target auf sich selbst (Mail-Loop). seenKeys.add(canonicalEmailKey(stressfreiEmail.email)); - try { - const pleskState = await checkEmailExists(localPart); - const existingMembers = [ - ...(pleskState.mailgroupMembers ?? []), - ...(pleskState.forwardingTargets ?? []), - ]; - const newImports: string[] = []; - for (const member of existingMembers) { - // Pentest 83.1: importierte Adressen aus Plesk müssen denselben - // Filter passieren wie User-Eingaben (TLD-Blocklist, Format). - // Sonst rutschen reservierte TLDs wie `.internal` ohne Check - // in unsere DB, falls ein Plesk-Admin sie dort manuell gepflegt - // hat. Ungültige werden silent gedroppt – Log informiert. - let validated: string; - try { - validated = assertValidForwardingEmail(member); - } catch (validationErr) { - const reason = validationErr instanceof Error ? validationErr.message : 'unbekannt'; - console.debug( - `[syncForwardingForEmail] Plesk-Member "${member}" verworfen: ${reason}`, - ); - continue; - } - const key = canonicalEmailKey(validated); - if (!seenKeys.has(key)) { - seenKeys.add(key); - forwardTargets.push(validated); - newImports.push(validated); - } - } - if (newImports.length > 0) { - const mergedAdditional = [ - ...parseAdditionalForwards(stressfreiEmail.additionalForwardingEmails), - ...newImports, + if (autoImport) { + try { + const pleskState = await checkEmailExists(localPart); + const existingMembers = [ + ...(pleskState.mailgroupMembers ?? []), + ...(pleskState.forwardingTargets ?? []), ]; - await prisma.stressfreiEmail.update({ - where: { id }, - data: { additionalForwardingEmails: serializeAdditionalForwards(mergedAdditional) }, - }); - // Pentest 83.3: PII-Logs auf debug-Level statt log-Level. - console.debug( - `[syncForwardingForEmail] Importiert aus Plesk-Mailgroup für ${stressfreiEmail.email}:`, - newImports, - ); + const newImports: string[] = []; + for (const member of existingMembers) { + // Pentest 83.1: importierte Adressen aus Plesk müssen denselben + // Filter passieren wie User-Eingaben (TLD-Blocklist, Format). + // Sonst rutschen reservierte TLDs wie `.internal` ohne Check + // in unsere DB, falls ein Plesk-Admin sie dort manuell gepflegt + // hat. Ungültige werden silent gedroppt – Log informiert. + let validated: string; + try { + validated = assertValidForwardingEmail(member); + } catch (validationErr) { + const reason = validationErr instanceof Error ? validationErr.message : 'unbekannt'; + console.debug( + `[syncForwardingForEmail] Plesk-Member "${member}" verworfen: ${reason}`, + ); + continue; + } + const key = canonicalEmailKey(validated); + if (!seenKeys.has(key)) { + seenKeys.add(key); + forwardTargets.push(validated); + newImports.push(validated); + } + } + if (newImports.length > 0) { + const mergedAdditional = [ + ...parseAdditionalForwards(stressfreiEmail.additionalForwardingEmails), + ...newImports, + ]; + await prisma.stressfreiEmail.update({ + where: { id }, + data: { additionalForwardingEmails: serializeAdditionalForwards(mergedAdditional) }, + }); + // Pentest 83.3: PII-Logs auf debug-Level statt log-Level. + console.debug( + `[syncForwardingForEmail] Importiert aus Plesk-Mailgroup für ${stressfreiEmail.email}:`, + newImports, + ); + } + } catch (importErr) { + // Nicht hart fehlschlagen – im schlimmsten Fall fehlen ein paar + // alte Empfänger, aber der eigentliche Sync soll trotzdem laufen. + console.error('[syncForwardingForEmail] Mailgroup-Import fehlgeschlagen:', importErr); } - } catch (importErr) { - // Nicht hart fehlschlagen – im schlimmsten Fall fehlen ein paar - // alte Empfänger, aber der eigentliche Sync soll trotzdem laufen. - console.error('[syncForwardingForEmail] Mailgroup-Import fehlgeschlagen:', importErr); } // 1) Forwards neu setzen (deaktiviert intern Mailgroup). diff --git a/docs/todo.md b/docs/todo.md index a33ccf1a..62631156 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,21 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🐞 Entfernte Weiterleitungen kamen via Auto-Import zurück** + - Folge-Bug: User löscht Adresse im Modal → DB-Liste wird kürzer → + Plesk-Sync läuft → Auto-Import (`Pentest 83.x`) sieht „c ist in + Plesk aber nicht in DB" → schreibt `c` zurück in + `additionalForwardingEmails` → Diff sagt nichts zu entfernen. + - Ursache: Auto-Import war für **alle** Sync-Aufrufe aktiv. Beim + Sync-Button-Klick will der User Plesk-Bestand übernehmen (Import + sinnvoll), beim Add/Remove im Modal ist die DB-Liste die + explizite Intent (Import schädlich). + - Fix: `syncForwardingForEmail(id, { autoImportPleskMembers? })` + mit Default `true`. `setAdditionalForwards` ruft mit + `false` auf → entfernte Adressen verschwinden jetzt sauber bei + Plesk. Sync-Button-Pfad bleibt unverändert (importiert weiterhin + alte Bestands-Members). + - [x] **🐞 Plesk-Sync: `-forwarding-addresses set:` existiert gar nicht** - Folge-Bug nach `a83358b`/`24e152b`: Sync verändert Plesk weiterhin nicht. `plesk bin mail --help` zeigt: `-forwarding-addresses`