2026-06-03 11:15:59 +00:00
|
|
|
|
// 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 {
|
2026-06-05 07:03:48 +00:00
|
|
|
|
rfcMessageId: this._header(full, "message-id"), // RFC822 Message-ID (Dedup-Schlüssel)
|
2026-06-03 11:15:59 +00:00
|
|
|
|
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,
|
2026-06-17 11:43:59 +00:00
|
|
|
|
direction: this._direction(header),
|
2026-06-03 11:15:59 +00:00
|
|
|
|
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 ---------------------------------------------------------------
|
|
|
|
|
|
|
2026-06-05 07:03:48 +00:00
|
|
|
|
/** 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() : "";
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-03 11:15:59 +00:00
|
|
|
|
_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;
|
2026-06-16 08:50:31 +00:00
|
|
|
|
if (html.trim()) return this._htmlToText(html);
|
2026-06-03 11:15:59 +00:00
|
|
|
|
return "";
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-16 08:50:31 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 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 ""; }
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-03 11:15:59 +00:00
|
|
|
|
_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 "";
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-17 11:43:59 +00:00
|
|
|
|
_direction(header) {
|
|
|
|
|
|
// Heuristik über den Ordnertyp; robustere Erkennung via Identitäten käme später.
|
2026-06-03 11:15:59 +00:00
|
|
|
|
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 };
|