thunderbird2docuware/lib/mail.js
sylyx 7e351a818a Feld-Zuordnung: Dateiname, Datum+Uhrzeit, Benutzer und weitere Werte
MAIL_TOKENS wächst von 11 auf 22 auswählbare Werte:
- Dokument: fileName (je Dokument), attachmentCount, rfcMessageId
- Datum/Zeit: dateTime, time, archiveDate, archiveDateTime
- Wer/Woher: dwUser, dwOrganization, accountEmail, folder

fileName kann nicht wie die übrigen Tokens einmal je Mail aufgelöst werden: die
.eml/PDF trägt den Mailnamen, jeder Anhang seinen eigenen. Darum tragen TOKEN_FN
jetzt ein zweites Argument ctx {fileName, attachmentCount}, und Felder, die auf
ein Token aus PER_FILE_TOKENS gemappt sind, werden über applyPerFileFields() vor
JEDEM Upload neu gesetzt. Im Formular steht nur eine Vorschau (Tooltip erklärt
das), die beim Umschalten .eml <-> PDF nachzieht.

Fix DateTime-Felder: _field prüfte t.includes("date") zuerst, und "DateTime"
enthält "date" — jedes DocuWare-DateTime-Feld wurde damit auf UTC-Mitternacht
normalisiert und verlor die Uhrzeit. Das betraf auch bestehende Zuordnungen.
Jetzt wird datetime zuerst geprüft; der Dialog rendert solche Felder als
datetime-local und liest sie lokal zurück. Date-Felder bleiben unverändert auf
dem lokalen Kalendertag als UTC-Mitternacht.

Offen und im Code vermerkt: ob DocuWare bei DateTime das "Z" auswertet oder die
ISO-Zeichenkette als lokale Wanduhrzeit liest, ist nicht gegen einen Server
verifiziert.

Mail.getMeta liefert zusätzlich folder und accountEmail; _accountForMessage
wurde zu _accountInfo (Name + Identitäts-Adresse).
DEFAULT_FIELD_MAP: DOC_FILE_NAME -> fileName.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:55:32 +02:00

165 lines
6.1 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 acc = await this._accountInfo(messageId);
return {
folder: (header.folder && (header.folder.name || header.folder.path)) || "",
accountEmail: acc.email,
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: acc.name,
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() };
},
/**
* Konto-Name und eigene E-Mail-Adresse (erste Identität) zur Nachricht.
* @returns {Promise<{name:string, email:string}>}
*/
async _accountInfo(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);
if (acc) {
const ident = (acc.identities || [])[0];
return { name: acc.name || "", email: (ident && ident.email) || "" };
}
}
} catch (_) {}
return { name: "", email: "" };
},
_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 };