// Extraktion von Mail-Inhalten über die Thunderbird messages.* API. const Mail = { /** Rohe RFC822-Nachricht als .eml-File. */ async getEmlFile(messageId, subject) { const raw = await browser.messages.getRaw(messageId); // String (RFC822) const blob = new Blob([raw], { type: "message/rfc822" }); const name = `${this._safeName(subject) || "email"}.eml`; return new File([blob], name, { type: "message/rfc822" }); }, /** Kopf + Body als strukturierte Metadaten zum Vorbefüllen. */ async getMeta(messageId) { const header = await browser.messages.get(messageId); const full = await browser.messages.getFull(messageId); const bodyText = this._extractText(full); const sender = this._parseAddress(header.author); const account = await this._accountForMessage(messageId); return { rfcMessageId: this._header(full, "message-id"), // RFC822 Message-ID (Dedup-Schlüssel) subject: header.subject || "", senderEmail: sender.email, senderName: sender.name, to: (header.recipients || []).join(", "), cc: (header.ccList || []).join(", "), bcc: (header.bccList || []).join(", "), date: header.date instanceof Date ? header.date : new Date(header.date), bodyText, account, direction: this._direction(header), sizeBytes: header.size || 0, }; }, /** Liste der Anhänge: [{partName, name, contentType, size}]. */ async listAttachments(messageId) { const atts = await browser.messages.listAttachments(messageId); return (atts || []) .filter((a) => a.partName && a.name) // echte Datei-Anhänge .map((a) => ({ partName: a.partName, name: a.name, contentType: a.contentType, size: a.size || 0, })); }, /** Anhang als File holen. */ async getAttachmentFile(messageId, partName) { return browser.messages.getAttachmentFile(messageId, partName); // File }, // --- Helfer --------------------------------------------------------------- /** Erster Wert eines Mail-Headers (getFull liefert Header als Arrays, klein geschrieben). */ _header(full, name) { const h = full && full.headers && full.headers[name]; return Array.isArray(h) && h.length ? String(h[0]).trim() : ""; }, _extractText(fullPart) { // Rekursiv den ersten text/plain-Teil suchen, sonst text/html entkernen. let plain = ""; let html = ""; const walk = (part) => { if (!part) return; if (part.contentType === "text/plain" && part.body) plain += part.body; else if (part.contentType === "text/html" && part.body) html += part.body; (part.parts || []).forEach(walk); }; walk(fullPart); if (plain.trim()) return plain; 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*$/); if (m) return { name: m[1].trim(), email: m[2].trim() }; return { name: "", email: str.trim() }; }, async _accountForMessage(messageId) { try { const msg = await browser.messages.get(messageId); const folder = msg.folder; if (folder && folder.accountId) { const acc = await browser.accounts.get(folder.accountId); return acc ? acc.name : ""; } } catch (_) {} return ""; }, _direction(header) { // Heuristik über den Ordnertyp; robustere Erkennung via Identitäten käme später. const folderType = header.folder && header.folder.type; if (folderType === "sent" || folderType === "outbox") return "Ausgang"; return "Eingang"; }, _safeName(s) { return (s || "").replace(/[\\/:*?"<>|]+/g, "_").slice(0, 80).trim(); }, }; if (typeof module !== "undefined") module.exports = { Mail };