Plesk-Sync: Legacy-Mailgroup-Adressen synchronisierten nicht

Prod-Bug: zusätzliche Weiterleitung eintragen → Toast meldet
Erfolg, Plesk übernimmt nichts. Plesk hat zwei unabhängige
Verteil-Mechanismen, Mailgroup (alte CLI-Anlagen) und Forwarding
(neue). Unser Sync schrieb nur in Forwarding, die alte Adresse
lief aber via Mailgroup → set:-Befehle landeten in ungenutzter
Tabelle. Stage funktionierte, weil dort frisch im Forwarding-
Modus angelegt.

- EmailExistsResult um mailgroupActive/Members + forwardingActive/
  Targets erweitert.
- pleskProvider.emailExists parst alle vier Felder aus --info-
  stdout (Mailgroup: true|false, Group member(s): ..., Forward
  request: ...).
- pleskProvider.updateForwardTargets setzt -mailgroup false dazu –
  deaktiviert den Legacy-Mechanismus.
- syncForwardingForEmail holt vorm Plesk-Update die bestehenden
  Mailgroup-Members und Forwarding-Targets ab und importiert sie
  in unsere additionalForwardingEmails-Liste (canonical-Key-Dedup).
  Verlustfrei – kein Empfänger fällt beim Umschalten raus.

Smoke-Test mit echtem Plesk-stdout (User-Log): 3 Group-Members
sauber geparst, leeres "Forward request" als [] erkannt.
This commit is contained in:
2026-06-18 17:22:08 +02:00
parent 5bb048c534
commit a83358bbe6
4 changed files with 126 additions and 3 deletions
@@ -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: <addrs>".
// 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(',')}`,
],
@@ -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 {
@@ -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
+24
View File
@@ -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