diff --git a/lib/mail.js b/lib/mail.js index 000d111..7629c4d 100644 --- a/lib/mail.js +++ b/lib/mail.js @@ -72,10 +72,55 @@ const Mail = { }; walk(fullPart); if (plain.trim()) return plain; - if (html.trim()) return html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim(); + if (html.trim()) return this._htmlToText(html); return ""; }, + /** + * Macht aus HTML lesbaren Klartext: entfernt style/script/head-INHALTE komplett, + * leitet aus Block-Elementen Zeilenumbrüche ab, dekodiert Entities und glättet + * Whitespace – ohne Absatzstruktur platt zu walzen. + */ + _htmlToText(html) { + let s = String(html); + s = s.replace(//g, " "); + // style/script/head/title inkl. Inhalt raus (sonst landet CSS/JS im Text). + s = s.replace(/<(style|script|head|title)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, " "); + // Block-Grenzen -> Zeilenumbruch; Listenpunkte -> "- ". + s = s.replace(/<\s*br\s*\/?>/gi, "\n"); + s = s.replace(/<\s*li\b[^>]*>/gi, "\n- "); + s = s.replace(/<\/\s*(p|div|li|tr|h[1-6]|ul|ol|table|blockquote)\s*>/gi, "\n"); + s = s.replace(/<\/\s*td\s*>/gi, " "); + // Restliche Tags entfernen, Entities dekodieren. + s = s.replace(/<[^>]+>/g, " "); + s = this._decodeEntities(s); + // Whitespace glätten: Spaces/Tabs/NBSP zusammenfassen, Leerzeilen begrenzen. + s = s.replace(/[ \t ]+/g, " "); + s = s.replace(/ *\n */g, "\n"); + s = s.replace(/\n{3,}/g, "\n\n"); + return s.trim(); + }, + + _decodeEntities(s) { + const named = { + amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", + auml: "ä", ouml: "ö", uuml: "ü", Auml: "Ä", Ouml: "Ö", Uuml: "Ü", szlig: "ß", + euro: "€", copy: "©", reg: "®", trade: "™", hellip: "…", middot: "·", bull: "•", + ndash: "–", mdash: "—", laquo: "«", raquo: "»", deg: "°", eacute: "é", agrave: "à", + ldquo: "“", rdquo: "”", lsquo: "‘", rsquo: "’", + bdquo: "„", sbquo: "‚", + }; + return String(s) + .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => this._fromCp(parseInt(h, 16))) + .replace(/&#(\d+);/g, (_, d) => this._fromCp(parseInt(d, 10))) + .replace(/&([a-zA-Z][a-zA-Z0-9]*);/g, (m, n) => (named[n] != null ? named[n] : m)); + }, + + _fromCp(cp) { + try { return Number.isFinite(cp) ? String.fromCodePoint(cp) : ""; } + catch (_) { return ""; } + }, + _parseAddress(str) { if (!str) return { name: "", email: "" }; const m = str.match(/^\s*"?([^"<]*)"?\s*<([^>]+)>\s*$/); diff --git a/lib/pdf.js b/lib/pdf.js index 2283858..019a0f8 100644 --- a/lib/pdf.js +++ b/lib/pdf.js @@ -129,9 +129,27 @@ const Pdf = { return this._toLatin1(s).replace(/[\\()]/g, "\\$&"); }, + // Unicode-Sonderzeichen, die NICHT in Latin-1, aber in WinAnsiEncoding liegen + // (0x80–0x9F). Da die Schrift /WinAnsiEncoding nutzt und Bytes per charCodeAt&0xFF + // geschrieben werden, rendern diese so als echte Typografie-Zeichen statt als '?'. + _WINANSI: { + "€": 0x80, "‚": 0x82, "ƒ": 0x83, "„": 0x84, "…": 0x85, + "†": 0x86, "‡": 0x87, "ˆ": 0x88, "‰": 0x89, "Š": 0x8A, + "‹": 0x8B, "Œ": 0x8C, "Ž": 0x8E, "‘": 0x91, "’": 0x92, + "“": 0x93, "”": 0x94, "•": 0x95, "–": 0x96, "—": 0x97, + "˜": 0x98, "™": 0x99, "š": 0x9A, "›": 0x9B, "œ": 0x9C, + "ž": 0x9E, "Ÿ": 0x9F, + }, + _toLatin1(s) { - // Zeichen außerhalb Latin-1 durch '?' ersetzen. - return (s || "").replace(/[^\x00-\xFF]/g, "?"); + let out = ""; + for (const ch of (s || "")) { + const cp = ch.codePointAt(0); + if (cp <= 0xff) out += ch; // Latin-1 direkt + else if (this._WINANSI[ch] != null) out += String.fromCharCode(this._WINANSI[ch]); + else out += "?"; // nicht darstellbar + } + return out; }, _latin1Bytes(str) {