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 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-07 15:24:59 +00:00
1 parent b6c1d81342
commit 1cbec5b32d
3 files changed
+42 -9

No files matched your search

+12 -6
View File
@@ -32,6 +32,10 @@ export function agbPdfPfad(fassung = AGB_FASSUNG) {
return path.join(agbVerzeichnis, `agb-${fassung}.pdf`) 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' }) const zeitFormat = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
function zeitpunkt(iso) { function zeitpunkt(iso) {
@@ -126,18 +130,20 @@ export async function sendeAuftragMail(auftrag, { pdf, token, erinnerung = false
try { try {
agbPdf = await readFile(agbPdfPfad(fassung)) agbPdf = await readFile(agbPdfPfad(fassung))
} catch (err) { } 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, { await sendCustomerMail(adresse, {
...k31Mail(auftrag, { token, erinnerung }), ...k31Mail(auftrag, { token, erinnerung }),
subjectPrefix: erinnerung ? ERINNERUNG_PRAEFIX : '',
attachments: [ attachments: [
{ filename: `${auftrag.nummer}.pdf`, content: abPdf, contentType: 'application/pdf' }, { filename: `${auftrag.nummer}.pdf`, content: abPdf, contentType: 'application/pdf' },
{ filename: `WEBklar-AGB-${fassung}.pdf`, content: agbPdf, contentType: 'application/pdf' }, { filename: `WEBklar-AGB-${fassung}.pdf`, content: agbPdf, contentType: 'application/pdf' },
], ],
bccTeam: false, bccTeam: false,
}) })
const kopie = await sendAdminNotice(k31Mail(auftrag, { erinnerung, teamkopie: true })) // Der Grund steht nicht im Protokoll: Meldungen des Mailservers nennen oft die Empfaengeradresse
if (!kopie.sent) console.error(`[auftraege] Teamkopie der Auftragsbestätigung ${auftrag.nummer} nicht gesendet: ${kopie.reason}`) 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. */ /** 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 { try {
await sendCustomerMail(adresse, k32Mail(auftrag)) await sendCustomerMail(adresse, k32Mail(auftrag))
} catch (err) { } 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 { try {
const t22 = await sendAdminNotice(t22Mail(auftrag)) 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) { } 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'}`)
} }
} }
+3 -2
View File
@@ -35,10 +35,11 @@ function getTransporter() {
} }
/** Vorlage rendern (server/mail-templates); Unstimmigkeiten landen im Log, die Mail geht trotzdem raus. */ /** 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) const mail = renderMail(template, values, blocks)
for (const problem of mail.problems) console.warn(`[mailer] Vorlage ${template}: ${problem}`) 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, // Throttle: pro Kunde maximal eine Benachrichtigung je Zeitfenster,
+27 -1
View File
@@ -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[0].content, 'base64').equals(AB_PDF))
assert.ok(Buffer.from(kundenmail.attachments[1].content, 'base64').equals(AGB_PDF)) assert.ok(Buffer.from(kundenmail.attachments[1].content, 'base64').equals(AGB_PDF))
assert.deepEqual(adressen(teamkopie.to), config.chatNotify.emails) 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.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') assert.ok(!teamkopie.attachments || teamkopie.attachments.length === 0, 'die AB-PDF liegt im Ticketsystem')
const geheim = JSON.stringify(teamkopie) 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 }) await sendeAuftragMail(abDokument({ ...AUFWAND, pdfDatei }), { token: TOKEN, erinnerung: true })
assert.equal(sink.sent.length, 2) 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')) 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(Buffer.from(sink.sent[0].attachments[0].content, 'base64').equals(AB_PDF))
assert.ok(!JSON.stringify(sink.sent[1]).includes(TOKEN)) 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: '' }) })) await sendeBestaetigtMails(BESTAETIGT({ gesendetAn: '', kunde: kunde('privat', { email: '' }) }))
assert.deepEqual(sink.sent.map((m) => m.subject), [T22_BETREFF]) 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: <k@example.invalid>'
// 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)
})