import 'server-only' /** * Eine .eml-Datei bauen - der E-Mail-Entwurf zum Oeffnen im Mailprogramm. * * Warum .eml zusaetzlich zu mailto? mailto uebergibt den Text in der * Adresszeile, und deren Laenge ist nirgends zugesichert. Die Briefe hier * sind lang (Bestellungsabsatz, sechs Auskunftspunkte) - eine .eml-Datei * traegt den vollen Text garantiert, egal wie lang. * * Das Format ist RFC 5322: Kopfzeilen, Leerzeile, Rumpf. Die Kopfzeile * "X-Unsent: 1" bittet das Mailprogramm, die Datei als ENTWURF zu oeffnen * statt als empfangene Nachricht - Outlook und Thunderbird respektieren * das. Ein Programm, das sie ignoriert, zeigt die Mail eben als Nachricht * mit Antwort-Moeglichkeit; auch damit laesst sich arbeiten. * * Ohne Absenderadresse ("From"): Die traegt das Mailprogramm beim Senden * selbst ein. Eine erfundene Adresse hier wuerde je nach Programm sogar * uebernommen - und dann ginge die Mail mit falschem Absender hinaus. */ /** * Kodiert Text als quoted-printable (RFC 2045). * * Noetig fuer die Umlaute: .eml ist ein Textformat mit ASCII-Erwartung. * Base64 ginge auch, aber quoted-printable bleibt lesbar, wenn jemand die * Datei im Editor oeffnet - und bei einem Schreiben in einer * Betreuungssache ist Nachvollziehbarkeit ein Wert fuer sich. */ export function quotedPrintable(text: string): string { const bytes = Buffer.from(text.replace(/\r?\n/g, '\r\n'), 'utf8') let ergebnis = '' let zeilenlaenge = 0 for (const byte of bytes) { let stueck: string if (byte === 0x0d || byte === 0x0a) { // Zeilenumbruch unkodiert - er strukturiert die Mail. ergebnis += String.fromCharCode(byte) zeilenlaenge = 0 continue } const istDruckbar = (byte >= 33 && byte <= 126 && byte !== 61) || byte === 32 || byte === 9 if (istDruckbar) { stueck = String.fromCharCode(byte) } else { stueck = `=${byte.toString(16).toUpperCase().padStart(2, '0')}` } // RFC 2045: hoechstens 76 Zeichen je Zeile, weiche Umbrueche mit "=". if (zeilenlaenge + stueck.length > 75) { ergebnis += '=\r\n' zeilenlaenge = 0 } ergebnis += stueck zeilenlaenge += stueck.length } return ergebnis } /** * Kodiert eine Kopfzeile mit Umlauten (RFC 2047, "encoded word"). * * "Betreff: Rückkaufswert" wuerde als rohe Kopfzeile von manchen * Programmen verstuemmelt. ASCII-Betreffs bleiben unkodiert - lesbarer. */ export function kopfzeileKodieren(text: string): string { // eslint-disable-next-line no-control-regex if (!/[^\x20-\x7E]/.test(text)) return text return `=?UTF-8?B?${Buffer.from(text, 'utf8').toString('base64')}?=` } export type EmlEingabe = { an: string betreff: string text: string } /** Baut die .eml-Datei als Buffer. */ export function emlErzeugen(eingabe: EmlEingabe): Buffer { const kopf = [ `To: ${kopfzeileKodieren(eingabe.an)}`, `Subject: ${kopfzeileKodieren(eingabe.betreff)}`, 'X-Unsent: 1', 'MIME-Version: 1.0', 'Content-Type: text/plain; charset=UTF-8', 'Content-Transfer-Encoding: quoted-printable', ].join('\r\n') return Buffer.from(`${kopf}\r\n\r\n${quotedPrintable(eingabe.text)}\r\n`, 'utf8') }