thunderbird2docuware/lib/mail.js

157 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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, account),
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(/<!--[\s\S]*?-->/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, accountName) {
// Heuristik: ist der Account-Name/eine Identität Absender -> Ausgang.
// Robustere Erkennung erfolgt später über Identitäten; v1 simpel:
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 };