diff --git a/backend/src/services/emailProvider/pleskProvider.ts b/backend/src/services/emailProvider/pleskProvider.ts index 55644202..60bcd72e 100644 --- a/backend/src/services/emailProvider/pleskProvider.ts +++ b/backend/src/services/emailProvider/pleskProvider.ts @@ -180,17 +180,56 @@ export class PleskEmailProvider implements IEmailProvider { // Mailbox-Status aus stdout parsen (Format: "Mailbox: true" oder "Mailbox: false") let hasMailbox: boolean | undefined; + let mailgroupActive: boolean | undefined; + let mailgroupMembers: string[] | undefined; + let forwardingActive: boolean | undefined; + let forwardingTargets: string[] | undefined; if (exists && result.stdout) { const mailboxMatch = result.stdout.match(/Mailbox:\s*(true|false)/i); if (mailboxMatch) { hasMailbox = mailboxMatch[1].toLowerCase() === 'true'; } + + // Mailgroup-Status + Mitglieder. Plesk listet sie auf einer + // Zeile, Adressen sind durch Whitespace getrennt. + const mailgroupMatch = result.stdout.match(/Mailgroup:\s*(true|false)/i); + if (mailgroupMatch) { + mailgroupActive = mailgroupMatch[1].toLowerCase() === 'true'; + } + const groupMembersMatch = result.stdout.match(/Group member\(s\):\s*([^\n]*)/i); + if (groupMembersMatch) { + mailgroupMembers = groupMembersMatch[1] + .trim() + .split(/\s+/) + .filter((m) => m.includes('@')); + } + + // Forwarding-Status + Ziele. Plesk druckt "Forward request: ". + // Auf manchen Plesk-Versionen heißt das Feld auch "Forwarding". + const forwardActiveMatch = result.stdout.match(/Forwarding:\s*(true|false)/i); + if (forwardActiveMatch) { + forwardingActive = forwardActiveMatch[1].toLowerCase() === 'true'; + } + const forwardTargetsMatch = result.stdout.match(/Forward(?:ing)?(?: request)?:\s*([^\n]*)/i); + if (forwardTargetsMatch) { + forwardingTargets = forwardTargetsMatch[1] + .trim() + .split(/\s+/) + .filter((m) => m.includes('@')); + if (forwardingActive === undefined) { + forwardingActive = (forwardingTargets?.length ?? 0) > 0; + } + } } return { exists, email: exists ? email : undefined, hasMailbox, + mailgroupActive, + mailgroupMembers, + forwardingActive, + forwardingTargets, }; } catch (error) { // HTTP-Fehler oder Netzwerkfehler @@ -458,11 +497,20 @@ export class PleskEmailProvider implements IEmailProvider { }; } - // Plesk CLI API: Weiterleitungsziele aktualisieren - // Format für -forwarding-addresses: "set:email1,email2" ersetzt alle Adressen + // Plesk CLI API: Weiterleitungsziele aktualisieren. + // Format für -forwarding-addresses: "set:email1,email2" ersetzt alle Adressen. + // + // WICHTIG: Mailgroup parallel deaktivieren. Plesk hat zwei + // unabhängige Verteil-Mechanismen (Mailgroup vs. Forwarding). + // Alt-Anlagen liefen oft via Mailgroup – unser `set:`-Befehl auf + // forwarding-addresses ändert dann eine ungenutzte Tabelle und + // Mails landen weiterhin bei den Mailgroup-Members. + // Der Service-Layer importiert vorher die Mailgroup-Members in die + // `targets`-Liste, damit beim Umschalten nichts verloren geht. await this.request('POST', '/api/v2/cli/mail/call', { params: [ '--update', email, + '-mailgroup', 'false', '-forwarding', 'true', '-forwarding-addresses', `set:${targets.join(',')}`, ], diff --git a/backend/src/services/emailProvider/types.ts b/backend/src/services/emailProvider/types.ts index 5f78446e..d267bda6 100644 --- a/backend/src/services/emailProvider/types.ts +++ b/backend/src/services/emailProvider/types.ts @@ -42,6 +42,14 @@ export interface EmailExistsResult { exists: boolean; email?: string; hasMailbox?: boolean; // true wenn echte Mailbox vorhanden + // Plesk hat zwei unabhängige Verteil-Mechanismen, beide können parallel + // aktiv sein. Manuelle/Legacy-Anlagen nutzen oft "Mailgroup" statt + // "Forwarding" – unser Sync muss alte Mitglieder dort einsammeln, + // sonst gehen sie beim Umschalten auf Forwarding verloren. + mailgroupActive?: boolean; + mailgroupMembers?: string[]; + forwardingActive?: boolean; + forwardingTargets?: string[]; } export interface EmailOperationResult { diff --git a/backend/src/services/stressfreiEmail.service.ts b/backend/src/services/stressfreiEmail.service.ts index 420f6dab..bcde2ffe 100644 --- a/backend/src/services/stressfreiEmail.service.ts +++ b/backend/src/services/stressfreiEmail.service.ts @@ -558,7 +558,50 @@ export async function syncForwardingForEmail( const localPart = stressfreiEmail.email.split('@')[0]; - // 1) Forwards neu setzen. + // 0) Auto-Migration: Plesk hat zwei Verteil-Mechanismen (Mailgroup + + // Forwarding). Alt-Anlagen liefen oft via Mailgroup – unser Sync + // schreibt aber nur in die Forwarding-Liste, daher landeten neue + // Adressen nirgendwo. Hier holen wir die aktuellen Mailgroup-Members + // ab und ziehen alle, die wir nicht schon kennen, in unsere + // additionalForwardingEmails-Liste rein. Der nachfolgende Plesk-Call + // deaktiviert dann die Mailgroup und schreibt die volle Liste als + // Forwarding. Verlustfrei – kein Empfänger fällt raus. + try { + const pleskState = await checkEmailExists(localPart); + const existingMembers = [ + ...(pleskState.mailgroupMembers ?? []), + ...(pleskState.forwardingTargets ?? []), + ]; + const newImports: string[] = []; + for (const member of existingMembers) { + const key = canonicalEmailKey(member); + if (!seenKeys.has(key)) { + seenKeys.add(key); + forwardTargets.push(member); + newImports.push(member); + } + } + if (newImports.length > 0) { + const mergedAdditional = [ + ...parseAdditionalForwards(stressfreiEmail.additionalForwardingEmails), + ...newImports, + ]; + await prisma.stressfreiEmail.update({ + where: { id }, + data: { additionalForwardingEmails: serializeAdditionalForwards(mergedAdditional) }, + }); + console.log( + `[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); + } + + // 1) Forwards neu setzen (deaktiviert intern Mailgroup). const forwardResult = await setEmailForwardTargets(localPart, forwardTargets); if (!forwardResult.success) { // Wenn Plesk meldet „nicht gefunden", liefern wir eine sprechende Meldung diff --git a/docs/todo.md b/docs/todo.md index b012e8ff..05d74003 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,30 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🐞 Plesk-Sync: Legacy-Mailgroup-Adressen synchronisierten nicht** + - Prod-Bug: User trägt zusätzliche Weiterleitung ein, Toast meldet + Erfolg, aber Plesk übernimmt nichts. Ursache: Plesk hat zwei + Verteil-Mechanismen, **Mailgroup** (alte CLI-Anlagen, + `Group member(s):`) und **Forwarding** (`Forward request:`). Unser + Sync schrieb nur in Forwarding, die Adresse lief aber via Mailgroup + → unsere `set:`-Befehle landeten in einer ungenutzten Tabelle. + Stage funktionierte, weil dort die Adressen frisch vom CRM angelegt + wurden (Forwarding-Modus von Anfang an). + - `EmailExistsResult` um `mailgroupActive` + `mailgroupMembers` + + `forwardingActive` + `forwardingTargets` erweitert. + - `pleskProvider.emailExists` parst alle vier Felder aus dem + `--info`-stdout (`Mailgroup: true|false`, `Group member(s): ...`, + `Forward request: ...`). + - `pleskProvider.updateForwardTargets` setzt jetzt zusätzlich + `-mailgroup false`, damit der Legacy-Mechanismus deaktiviert wird + und nur noch Forwarding aktiv ist. + - `syncForwardingForEmail`: vor dem Plesk-Update werden bestehende + Mailgroup-Members + Forwarding-Targets abgeholt und in unsere + `additionalForwardingEmails`-Liste **importiert** (canonical-Key- + Dedup). Verlustfrei – kein bestehender Empfänger fällt beim + Umschalten auf Forwarding raus. Import-Fehler werden geloggt, + aber der eigentliche Sync läuft trotzdem. + - [x] **🔒 Pentest 81.1 (MEDIUM): Self-Forward erzeugte Mail-Loop am Provider** - Bug: User konnte die Stressfrei-Adresse selbst (z.B. `max.mustermann@stressfrei-wechseln.net`) als zusätzliches