import { describe, expect, it } from 'vitest' import { MAILTO_HOECHSTLAENGE, emailFassung, mailtoLink, siehtAusWieEmail, } from '@/lib/schreiben/email' import { emlErzeugen, kopfzeileKodieren, quotedPrintable } from '@/lib/schreiben/eml' /** * Schreiben als E-Mail. * * Zwei Gefahren stehen im Mittelpunkt: dass beim Umbau vom Brief zur Mail * stillschweigend Inhalt verloren geht (vor allem der Bestellungsabsatz), * und dass ein mailto-Link einen halben Brief übergibt, ohne dass es * jemand merkt. */ const BRIEF = [ 'Florian Wilde', 'Musterstraße 1', '96450 Coburg', '', 'Allianz Lebensversicherungs-AG', '[Straße und Hausnummer]', '[PLZ und Ort]', '', '23.08.2026', '', 'Betreff: Auskunft über Rückkaufswert — Franziska Wilde, geb. 23.09.1963', '', 'Sehr geehrte Damen und Herren,', '', 'ich bin durch Beschluss des Amtsgerichts Lichtenfels (Az. XVII 355/25) ' + 'zum rechtlichen Betreuer bestellt.', '', 'für ein laufendes Verfahren benötige ich folgende Auskünfte.', '', 'Mit freundlichen Grüßen', 'Florian Wilde', ].join('\n') describe('E-Mail-Fassung aus dem Brief', () => { it('entfernt Absender, Anschrift, Datum und Betreffzeile', () => { const fassung = emailFassung(BRIEF) expect(fassung.text).not.toContain('Musterstraße') expect(fassung.text).not.toContain('[Straße und Hausnummer]') expect(fassung.text).not.toContain('23.08.2026') expect(fassung.text).not.toContain('Betreff:') }) it('beginnt mit der Anrede und behält den Bestellungsabsatz', () => { // Der Absatz ist der Grund, warum überhaupt geantwortet wird. const fassung = emailFassung(BRIEF) expect(fassung.text.startsWith('Sehr geehrte Damen und Herren,')).toBe(true) expect(fassung.text).toContain('Amtsgerichts Lichtenfels') expect(fassung.text).toContain('Mit freundlichen Grüßen') }) it('zieht den Betreff aus der Betreffzeile', () => { const fassung = emailFassung(BRIEF) expect(fassung.betreff).toContain('Auskunft über Rückkaufswert') expect(fassung.betreff).toContain('Franziska Wilde') }) it('lässt einen frei umgebauten Text unangetastet', () => { // Ohne erkennbare Anrede wird nichts entfernt: Lieber ein Briefkopf zu // viel in der Mail als stillschweigend verlorener Inhalt. const frei = 'Hallo Frau Maier,\nhier die gewünschten Unterlagen.\nViele Grüße' const fassung = emailFassung(frei, 'Unterlagen') expect(fassung.text).toContain('Hallo Frau Maier,') expect(fassung.text).toContain('Viele Grüße') expect(fassung.betreff).toBe('Unterlagen') }) it('faltet mehrfache Leerzeilen zusammen', () => { const fassung = emailFassung(BRIEF) expect(fassung.text).not.toContain('\n\n\n') }) }) describe('mailto-Link', () => { const fassung = { betreff: 'Kurzer Betreff', text: 'Kurzer Text.' } it('baut den Link mit Adresse, Betreff und Text', () => { const link = mailtoLink('service@allianz.de', fassung) expect(link).not.toBeNull() expect(link).toContain('mailto:service%40allianz.de') expect(link).toContain('subject=Kurzer%20Betreff') expect(link).toContain('body=Kurzer%20Text.') }) it('verweigert den Link bei zu langem Text, statt abzuschneiden', () => { // Der Kern der Beides-Entscheidung: Ein Knopf, der einen halben Brief // übergibt, wäre schlimmer als keiner. const lang = { betreff: 'B', text: 'ä'.repeat(MAILTO_HOECHSTLAENGE) } expect(mailtoLink('a@b.de', lang)).toBeNull() }) it('verweigert den Link ohne Adresse', () => { expect(mailtoLink('', fassung)).toBeNull() expect(mailtoLink(' ', fassung)).toBeNull() }) it('rechnet die Grenze nach der Kodierung, nicht davor', () => { // 900 Umlaute sind roh unter der Grenze, kodiert (je 3 Byte -> %C3%A4, // 6 Zeichen) weit darüber. const umlaute = { betreff: 'B', text: 'ä'.repeat(900) } expect(mailtoLink('a@b.de', umlaute)).toBeNull() }) }) describe('Adressprüfung', () => { it('erkennt gewöhnliche Adressen', () => { expect(siehtAusWieEmail('service@allianz.de')).toBe(true) expect(siehtAusWieEmail('info@stadt-coburg.bayern.de')).toBe(true) }) it('weist Offensichtliches ab', () => { expect(siehtAusWieEmail('')).toBe(false) expect(siehtAusWieEmail(null)).toBe(false) expect(siehtAusWieEmail('kein-at-zeichen.de')).toBe(false) expect(siehtAusWieEmail('zwei @ leerzeichen@x.de')).toBe(false) }) }) describe('.eml-Datei', () => { it('trägt Empfänger, Betreff und den vollen Text', () => { const eml = emlErzeugen({ an: 'service@allianz.de', betreff: 'Testbetreff', text: 'Zeile eins.\nZeile zwei.', }).toString('utf8') expect(eml).toContain('To: service@allianz.de') expect(eml).toContain('Subject: Testbetreff') expect(eml).toContain('Zeile eins.') expect(eml).toContain('Zeile zwei.') }) it('bittet das Mailprogramm um einen Entwurf', () => { const eml = emlErzeugen({ an: 'a@b.de', betreff: 'B', text: 'T' }).toString('utf8') expect(eml).toContain('X-Unsent: 1') }) it('erfindet keinen Absender', () => { // Ein erfundener From-Kopf würde je nach Programm übernommen - und // dann ginge die Mail mit falschem Absender hinaus. const eml = emlErzeugen({ an: 'a@b.de', betreff: 'B', text: 'T' }).toString('utf8') expect(eml).not.toMatch(/^From:/m) }) it('kodiert Umlaute im Betreff nach RFC 2047', () => { const eml = emlErzeugen({ an: 'a@b.de', betreff: 'Rückkaufswert', text: 'T' }).toString('utf8') expect(eml).toContain('Subject: =?UTF-8?B?') // Und zurückgerechnet stimmt der Inhalt. const treffer = eml.match(/Subject: =\?UTF-8\?B\?([A-Za-z0-9+/=]+)\?=/) expect(Buffer.from(treffer![1]!, 'base64').toString('utf8')).toBe('Rückkaufswert') }) it('lässt einen ASCII-Betreff lesbar', () => { const eml = emlErzeugen({ an: 'a@b.de', betreff: 'Anfrage', text: 'T' }).toString('utf8') expect(eml).toContain('Subject: Anfrage') }) }) describe('quoted-printable', () => { it('kodiert Umlaute und lässt ASCII lesbar', () => { const kodiert = quotedPrintable('Grüße') expect(kodiert).toContain('Gr') expect(kodiert).toContain('=C3=BC') expect(kodiert).toContain('=C3=9F') }) it('kodiert das Gleichheitszeichen', () => { expect(quotedPrintable('a=b')).toBe('a=3Db') }) it('hält die 76-Zeichen-Grenze je Zeile ein', () => { const kodiert = quotedPrintable('ä'.repeat(200)) for (const zeile of kodiert.split('\r\n')) { expect(zeile.length).toBeLessThanOrEqual(76) } }) it('lässt Kopfzeilen ohne Sonderzeichen unkodiert', () => { expect(kopfzeileKodieren('Anfrage 2026')).toBe('Anfrage 2026') }) })