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:
1 parent
b6c1d81342
commit
1cbec5b32d
3 files changed
+42
-9
No files matched your search
@@ -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'}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user