thunderbird2docuware/lib/mail.js
sylyx 589c0018a9 Fixes: XSS-Schutz, Datums-Zeitzone, Multi-Retry, Settings-Race, http-Warnung
- dialog.js: Anhang-Liste per DOM-API statt innerHTML (kein HTML-Inject über
  angreiferkontrollierte Anhang-Namen); Feldname-Selektoren via CSS.escape,
  datalist-id bereinigt; Datums-Eingabe lokal statt UTC (kein Off-by-one);
  bereits abgelegte Mails bei Retry überspringen (keine Dubletten)
- docuware.js: Datumsfelder auf lokalen Kalendertag normalisieren (UTC-Mitternacht)
- store.js: Settings.set serialisiert (kein read-modify-write Race)
- options.js: Warnung bei unverschlüsseltem http://-Server
- mail.js: ungenutzten Parameter aus _direction entfernt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 13:43:59 +02:00

156 lines
5.7 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),
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) {
// 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 };