Im Schreiben-Editor gibt es jetzt den Umschalter Brief/E-Mail. Die E-Mail-Fassung wird aus dem - womoeglich bearbeiteten - Brieftext abgeleitet: ohne Absenderblock, Anschrift und Datumszeile, aber mit dem Bestellungsabsatz. Betreff und Text einzeln kopierbar. Die offene Entscheidung aus dem Issue (mailto oder .eml) ist nach Rueckfrage mit BEIDES beantwortet: - mailto fuer kurze Texte. Der Knopf verschwindet, sobald der kodierte Link 1800 Zeichen uebersteigt - mailto uebergibt den Text in der Adresszeile, deren Laenge nirgends zugesichert ist, und ein halbierter Brief faellt erst beim Empfaenger auf. Die Grenze wird NACH der URL-Kodierung gerechnet; 900 Umlaute sind roh harmlos und kodiert weit drueber. - .eml immer: RFC 5322 mit X-Unsent: 1, quoted-printable fuer den Rumpf, RFC-2047-Kodierung fuer Umlaute im Betreff. Bewusst ohne From-Kopf - ein erfundener Absender wuerde je nach Programm uebernommen. Beide Knoepfe erscheinen nur mit E-Mail-Adresse (aus dem Kontakt uebernommen oder von Hand). Ohne Adresse bleibt das Kopieren. Die Ablage kennt den Unterschied: Eine E-Mail heisst in der Akte "E-Mail: ...", und abgelegt wird die E-Mail-Fassung samt Empfaengeradresse im Kopf der PDF - in der Akte soll stehen, was tatsaechlich hinausging. Sicherung in emailFassung(): Findet sich keine Anrede (frei umgebauter Text), wird NICHTS entfernt. Lieber ein Briefkopf zu viel in der Mail als stillschweigend verlorener Inhalt. Geprueft: 20 Einheitstests, vier End-to-End-Tests auf beiden Geraeteprofilen. 590 Unit-Tests, 81 e2e, null Typfehler. Refs #5
96 lines
3.1 KiB
TypeScript
96 lines
3.1 KiB
TypeScript
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')
|
|
}
|