From 1cbec5b32dc7b390547783b1765fde5a311c3715 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 15:24:59 +0000 Subject: [PATCH] Kontrolle: Betreff-Zusaetze Erinnerung und Kopie, Mail-Protokolle ohne Meldung und Adresse Die Erinnerung beginnt mit 'Erinnerung: ', die Teamkopie mit 'Kopie: ' (der Mailer nimmt dafuer ein optionales subjectPrefix). Die Protokollzeilen in mails.js nennen nur Nummer und Fehlerkennung. Co-Authored-By: Claude Sonnet 5.5 --- server/services/auftraege/mails.js | 18 ++++++++++++------ server/services/mailer.js | 5 +++-- test/auftraegeMails.test.js | 28 +++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/server/services/auftraege/mails.js b/server/services/auftraege/mails.js index c0247bf..c062765 100644 --- a/server/services/auftraege/mails.js +++ b/server/services/auftraege/mails.js @@ -32,6 +32,10 @@ export function agbPdfPfad(fassung = AGB_FASSUNG) { return path.join(agbVerzeichnis, `agb-${fassung}.pdf`) } +// Betreff-Zusaetze der K31 (die Vorlage selbst bleibt fuer alle Faelle gleich) +const ERINNERUNG_PRAEFIX = 'Erinnerung: ' +const KOPIE_PRAEFIX = 'Kopie: ' + const zeitFormat = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }) function zeitpunkt(iso) { @@ -126,18 +130,20 @@ export async function sendeAuftragMail(auftrag, { pdf, token, erinnerung = false try { agbPdf = await readFile(agbPdfPfad(fassung)) } catch (err) { - throw new Error(`AGB-PDF ${fassung} nicht lesbar: ${err.code || err.message}`) + throw new Error(`AGB-PDF ${fassung} nicht lesbar: ${err.code || err.name}`) } await sendCustomerMail(adresse, { ...k31Mail(auftrag, { token, erinnerung }), + subjectPrefix: erinnerung ? ERINNERUNG_PRAEFIX : '', attachments: [ { filename: `${auftrag.nummer}.pdf`, content: abPdf, contentType: 'application/pdf' }, { filename: `WEBklar-AGB-${fassung}.pdf`, content: agbPdf, contentType: 'application/pdf' }, ], bccTeam: false, }) - const kopie = await sendAdminNotice(k31Mail(auftrag, { erinnerung, teamkopie: true })) - if (!kopie.sent) console.error(`[auftraege] Teamkopie der Auftragsbestätigung ${auftrag.nummer} nicht gesendet: ${kopie.reason}`) + // Der Grund steht nicht im Protokoll: Meldungen des Mailservers nennen oft die Empfaengeradresse + const kopie = await sendAdminNotice({ ...k31Mail(auftrag, { erinnerung, teamkopie: true }), subjectPrefix: `${KOPIE_PRAEFIX}${erinnerung ? ERINNERUNG_PRAEFIX : ''}` }) + if (!kopie.sent) console.error(`[auftraege] Teamkopie der Auftragsbestätigung ${auftrag.nummer} nicht gesendet`) } /** K32 an den Kunden (Kopie ans Team wie üblich: die Mail trägt keinen Link), danach T22 ans Team. Wirft nie. */ @@ -149,13 +155,13 @@ export async function sendeBestaetigtMails(auftrag) { try { await sendCustomerMail(adresse, k32Mail(auftrag)) } catch (err) { - console.error(`[auftraege] K32 für ${auftrag.nummer} fehlgeschlagen: ${err.message}`) + console.error(`[auftraege] K32 für ${auftrag.nummer} fehlgeschlagen: ${err?.code || err?.name || 'Fehler'}`) } } try { const t22 = await sendAdminNotice(t22Mail(auftrag)) - if (!t22.sent) console.error(`[auftraege] T22 für ${auftrag.nummer} nicht gesendet: ${t22.reason}`) + if (!t22.sent) console.error(`[auftraege] T22 für ${auftrag.nummer} nicht gesendet`) } catch (err) { - console.error(`[auftraege] T22 für ${auftrag.nummer} fehlgeschlagen: ${err.message}`) + console.error(`[auftraege] T22 für ${auftrag.nummer} fehlgeschlagen: ${err?.code || err?.name || 'Fehler'}`) } } diff --git a/server/services/mailer.js b/server/services/mailer.js index 5a5781d..4b40fcb 100644 --- a/server/services/mailer.js +++ b/server/services/mailer.js @@ -35,10 +35,11 @@ function getTransporter() { } /** Vorlage rendern (server/mail-templates); Unstimmigkeiten landen im Log, die Mail geht trotzdem raus. */ -function render({ template, values = {}, blocks = {} }) { +function render({ template, values = {}, blocks = {}, subjectPrefix = '' }) { const mail = renderMail(template, values, blocks) for (const problem of mail.problems) console.warn(`[mailer] Vorlage ${template}: ${problem}`) - return mail + // optional: Text vor dem Betreff der Vorlage (z. B. "Kopie: " bei einer Teamkopie) + return subjectPrefix ? { ...mail, subject: `${subjectPrefix}${mail.subject}` } : mail } // Throttle: pro Kunde maximal eine Benachrichtigung je Zeitfenster, diff --git a/test/auftraegeMails.test.js b/test/auftraegeMails.test.js index d1131ed..f78300f 100644 --- a/test/auftraegeMails.test.js +++ b/test/auftraegeMails.test.js @@ -132,7 +132,7 @@ test('sendeAuftragMail: K31 an den Kunden ohne BCC (Link im Fragment, AB-PDF und assert.ok(Buffer.from(kundenmail.attachments[0].content, 'base64').equals(AB_PDF)) assert.ok(Buffer.from(kundenmail.attachments[1].content, 'base64').equals(AGB_PDF)) assert.deepEqual(adressen(teamkopie.to), config.chatNotify.emails) - assert.equal(teamkopie.subject, K31_BETREFF) + assert.equal(teamkopie.subject, `Kopie: ${K31_BETREFF}`) assert.ok(teamkopie.text.split('\n').includes('Kopie für das Team – der Bestätigungslink steht nur in der Mail an den Kunden.')) assert.ok(!teamkopie.attachments || teamkopie.attachments.length === 0, 'die AB-PDF liegt im Ticketsystem') const geheim = JSON.stringify(teamkopie) @@ -144,6 +144,8 @@ test('sendeAuftragMail: die Erinnerung geht auch als Teamkopie raus; ohne pdf wi await sendeAuftragMail(abDokument({ ...AUFWAND, pdfDatei }), { token: TOKEN, erinnerung: true }) assert.equal(sink.sent.length, 2) for (const mail of sink.sent) assert.ok(mail.text.includes('Wir haben Ihnen diese Auftragsbestätigung am 06.10.2026 schon einmal geschickt')) + const betreff = 'Ihre Auftragsbestätigung AB-2026-0001 – Website für Musterbäckerei' + assert.deepEqual(sink.sent.map((m) => m.subject), [`Erinnerung: ${betreff}`, `Kopie: Erinnerung: ${betreff}`]) assert.ok(Buffer.from(sink.sent[0].attachments[0].content, 'base64').equals(AB_PDF)) assert.ok(!JSON.stringify(sink.sent[1]).includes(TOKEN)) }) @@ -202,3 +204,27 @@ test('sendeBestaetigtMails wirft nie: scheitert K32, geht T22 trotzdem raus; ohn await sendeBestaetigtMails(BESTAETIGT({ gesendetAn: '', kunde: kunde('privat', { email: '' }) })) assert.deepEqual(sink.sent.map((m) => m.subject), [T22_BETREFF]) }) + +test('Protokoll der Mail-Wege: nur Nummer und Fehlerkennung, nie die Meldung des Transports und nie eine Adresse', async () => { + const meldung = 'Recipient address rejected: ' + // nur die Zeilen von mails.js; das Protokoll des Mailers (shared) ist nicht Gegenstand + const protokolliert = () => protokoll.mock.calls.map((c) => c.arguments.join(' ')).filter((zeile) => zeile.startsWith('[auftraege]')).join('\n') + const pruefe = (mitKennung) => { + const text = protokolliert() + assert.ok(text.includes('AB-2026-0001'), 'die Nummer steht im Protokoll') + for (const verboten of ['k@example.invalid', 'Recipient', 'rejected']) assert.ok(!text.includes(verboten), `das Protokoll enthält ${verboten}`) + if (mitKennung) assert.ok(text.includes('EENVELOPE'), 'die Fehlerkennung steht im Protokoll') + protokoll.mock.resetCalls() + } + const fehler = () => Object.assign(new Error(meldung), { code: 'EENVELOPE' }) + // Teamkopie scheitert (zweiter Aufruf) + let aufrufe = 0 + setMailTransportForTests({ async sendMail(mail) { aufrufe += 1; if (aufrufe === 2) throw fehler(); return sink.sendMail(mail) } }) + await sendeAuftragMail(AB, { pdf: AB_PDF, token: TOKEN }) + pruefe(false) + // K32 und T22 scheitern beide + setMailTransportForTests({ async sendMail() { throw fehler() } }) + await sendeBestaetigtMails(BESTAETIGT()) + assert.ok(protokolliert().includes('K32') && protokolliert().includes('T22')) + pruefe(true) +})