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>
This commit is contained in:
parent
6ca1e01b46
commit
7e351a818a
140
dialog/dialog.js
140
dialog/dialog.js
@ -27,6 +27,8 @@ let STATE = {
|
||||
|
||||
// Mail-Token -> Funktion, die den Wert aus den Mail-Metadaten liefert.
|
||||
// Welches DocuWare-Feld welchen Token bekommt, steht im Mapping (s. effectiveFieldMap).
|
||||
// Zweites Argument ctx trägt Werte, die nicht an der Mail hängen, sondern am
|
||||
// konkreten Upload: { fileName, attachmentCount }.
|
||||
const TOKEN_FN = {
|
||||
senderEmail: (m) => m.senderEmail,
|
||||
senderName: (m) => m.senderName,
|
||||
@ -39,8 +41,34 @@ const TOKEN_FN = {
|
||||
direction: (m) => m.direction,
|
||||
size: (m) => m.sizeBytes,
|
||||
account: (m) => m.account,
|
||||
|
||||
// Datum/Zeit. date liefert den Kalendertag, dateTime den echten Zeitpunkt –
|
||||
// in DocuWare braucht letzteres ein Feld vom Typ DateTime, sonst schneidet
|
||||
// der Server die Uhrzeit ab.
|
||||
dateTime: (m) => m.date,
|
||||
time: (m) => fmtTime(m.date),
|
||||
// archiveDate ist bewusst auf Mitternacht gesetzt: so bleibt es „nur Datum",
|
||||
// auch wenn es auf ein DateTime-Feld gemappt wird. archiveDateTime trägt die Uhrzeit.
|
||||
archiveDate: () => { const n = new Date(); return new Date(n.getFullYear(), n.getMonth(), n.getDate()); },
|
||||
archiveDateTime: () => new Date(),
|
||||
|
||||
// Dokumentbezogen
|
||||
fileName: (m, ctx) => (ctx && ctx.fileName) || "",
|
||||
attachmentCount: (m, ctx) => (ctx && ctx.attachmentCount) || 0,
|
||||
rfcMessageId: (m) => DocuWare.normalizeMessageId(m.rfcMessageId),
|
||||
|
||||
// Wer/Woher – aus den Einstellungen bzw. dem Thunderbird-Konto.
|
||||
dwUser: () => (STATE.settings && STATE.settings.username) || "",
|
||||
dwOrganization: () => (STATE.settings && STATE.settings.organization) || "",
|
||||
accountEmail: (m) => m.accountEmail || "",
|
||||
folder: (m) => m.folder || "",
|
||||
};
|
||||
|
||||
// Tokens, deren Wert vom einzelnen hochgeladenen Dokument abhängt. Felder, die
|
||||
// darauf gemappt sind, werden je Upload neu gesetzt – sonst trüge jeder Anhang
|
||||
// den Dateinamen der E-Mail.
|
||||
const PER_FILE_TOKENS = new Set(["fileName"]);
|
||||
|
||||
// Aktives Feld-Mapping: eigenes (aus den Einstellungen) wenn gesetzt, sonst Default.
|
||||
function effectiveFieldMap() {
|
||||
const m = STATE.settings && STATE.settings.fieldMapping;
|
||||
@ -49,16 +77,23 @@ function effectiveFieldMap() {
|
||||
|
||||
// Vorbefüllwert für ein DocuWare-Feld (oder undefined). Das Mapping kann je Feld
|
||||
// entweder einen Mail-Token (String) ODER einen festen Wert ({ const: "NEU" }) tragen.
|
||||
function prefillFor(fieldName, meta) {
|
||||
function prefillFor(fieldName, meta, ctx) {
|
||||
const m = effectiveFieldMap()[fieldName];
|
||||
if (!m) return undefined;
|
||||
if (typeof m === "object") {
|
||||
if (m.const !== undefined && m.const !== null && m.const !== "") return m.const;
|
||||
if (m.token && TOKEN_FN[m.token]) return TOKEN_FN[m.token](meta);
|
||||
if (m.token && TOKEN_FN[m.token]) return TOKEN_FN[m.token](meta, ctx);
|
||||
return undefined;
|
||||
}
|
||||
const fn = TOKEN_FN[m];
|
||||
return fn ? fn(meta) : undefined;
|
||||
return fn ? fn(meta, ctx) : undefined;
|
||||
}
|
||||
|
||||
// Auf welchen Token ist ein Feld gemappt? (String-Form oder { token }.)
|
||||
function tokenOf(fieldName) {
|
||||
const m = effectiveFieldMap()[fieldName];
|
||||
if (typeof m === "string") return m;
|
||||
return m && typeof m === "object" ? m.token : undefined;
|
||||
}
|
||||
|
||||
function status(msg, kind) {
|
||||
@ -461,6 +496,10 @@ function buildInput(f) {
|
||||
// Nur Listenwerte erlaubt -> starres Dropdown.
|
||||
el = document.createElement("select");
|
||||
el.innerHTML = '<option value=""></option>';
|
||||
} else if (t.includes("datetime")) {
|
||||
// DateTime-Feld: Tag UND Uhrzeit erfassbar (vor dem reinen date-Zweig prüfen).
|
||||
el = document.createElement("input");
|
||||
el.type = "datetime-local";
|
||||
} else if (t.includes("date")) {
|
||||
el = document.createElement("input");
|
||||
el.type = "date";
|
||||
@ -493,10 +532,18 @@ function applyFieldDefaults() {
|
||||
}
|
||||
|
||||
function prefillFields() {
|
||||
const ctx = formCtx();
|
||||
STATE.fields.forEach((f) => {
|
||||
const val = prefillFor(f.name, STATE.meta);
|
||||
const val = prefillFor(f.name, STATE.meta, ctx);
|
||||
if (val === undefined || val === null || val === "") return;
|
||||
setFieldValue(f.name, f, val);
|
||||
// Dateiname-Felder sind nur eine Vorschau: beim Ablegen trägt jeder Anhang
|
||||
// seinen eigenen Namen. Im Tooltip erklären, statt still zu überschreiben.
|
||||
if (PER_FILE_TOKENS.has(tokenOf(f.name))) {
|
||||
const wrap = document.querySelector(`.field[data-name="${cssEsc(f.name)}"]`);
|
||||
const el = wrap && wrap.querySelector("[data-role='value']");
|
||||
if (el) el.title = "Wird beim Ablegen je Dokument gesetzt (Anhänge tragen ihren eigenen Dateinamen).";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -505,9 +552,9 @@ function setFieldValue(name, f, val) {
|
||||
const wrap = document.querySelector(`.field[data-name="${cssEsc(name)}"]`);
|
||||
const el = wrap && wrap.querySelector("[data-role='value']");
|
||||
if (!el) return;
|
||||
if (el.type === "date") {
|
||||
if (el.type === "date" || el.type === "datetime-local") {
|
||||
const d = val instanceof Date ? val : new Date(val);
|
||||
if (!isNaN(d)) el.value = toDateInput(d);
|
||||
if (!isNaN(d)) el.value = el.type === "date" ? toDateInput(d) : toDateTimeInput(d);
|
||||
} else if (el.tagName === "SELECT") {
|
||||
let opt = Array.from(el.options).find((o) => o.value === String(val));
|
||||
if (!opt) {
|
||||
@ -576,8 +623,12 @@ function collectFieldValues() {
|
||||
}
|
||||
if (String(raw).trim() !== "") {
|
||||
// Date-Input als LOKALE Mitternacht parsen (raw+"T00:00:00"), sonst
|
||||
// interpretiert new Date("YYYY-MM-DD") als UTC.
|
||||
const value = el.type === "date" ? new Date(`${raw}T00:00:00`) : raw;
|
||||
// interpretiert new Date("YYYY-MM-DD") als UTC. datetime-local ist bereits
|
||||
// lokal und wird direkt geparst.
|
||||
const value =
|
||||
el.type === "date" ? new Date(`${raw}T00:00:00`)
|
||||
: el.type === "datetime-local" ? new Date(raw)
|
||||
: raw;
|
||||
result[name] = { value, type };
|
||||
}
|
||||
});
|
||||
@ -643,7 +694,8 @@ async function submit() {
|
||||
mid === STATE.messageId ? STATE.attachments : await Mail.listAttachments(mid);
|
||||
|
||||
// Indexfelder: manuelle Werte für alle gleich; gemappte Felder je Mail neu.
|
||||
const fields = STATE.isBasket ? {} : fieldsForMessage(result, meta, multi);
|
||||
const msgCtx = { attachmentCount: atts.length };
|
||||
const fields = STATE.isBasket ? {} : fieldsForMessage(result, meta, multi, msgCtx);
|
||||
// Message-ID für Dedup/„bereits abgelegt" mitschreiben (wenn Schrank EML_ID hat).
|
||||
if (!STATE.isBasket && meta.rfcMessageId && fieldNames.includes("EML_ID")) {
|
||||
fields.EML_ID = { value: DocuWare.normalizeMessageId(meta.rfcMessageId), type: "Text" };
|
||||
@ -654,14 +706,16 @@ async function submit() {
|
||||
await uploadOnce(`${mid}|eml`, async () => {
|
||||
status(`${prefix}E-Mail …`);
|
||||
const eml = await Mail.getEmlFile(mid, meta.subject);
|
||||
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, eml, eml.name, fields);
|
||||
const ef = applyPerFileFields(fields, meta, { ...msgCtx, fileName: eml.name });
|
||||
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, eml, eml.name, ef);
|
||||
});
|
||||
}
|
||||
if (wantPdf) {
|
||||
await uploadOnce(`${mid}|pdf`, async () => {
|
||||
status(`${prefix}PDF …`);
|
||||
const pdf = Pdf.fromMail(meta);
|
||||
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, pdf, pdf.name, fields);
|
||||
const pf = applyPerFileFields(fields, meta, { ...msgCtx, fileName: pdf.name });
|
||||
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, pdf, pdf.name, pf);
|
||||
});
|
||||
}
|
||||
if (wantAtt) {
|
||||
@ -673,7 +727,14 @@ async function submit() {
|
||||
await uploadOnce(`${mid}|att:${a.partName}`, async () => {
|
||||
status(`${prefix}Anhang ${a.name} …`);
|
||||
const file = await Mail.getAttachmentFile(mid, a.partName);
|
||||
const af = STATE.isBasket ? {} : { ...fields, DOC_FILE_NAME: { value: a.name, type: "Text" } };
|
||||
// Anhang trägt seinen EIGENEN Dateinamen – sowohl in gemappten
|
||||
// fileName-Feldern als auch im festen DOC_FILE_NAME.
|
||||
const af = STATE.isBasket
|
||||
? {}
|
||||
: {
|
||||
...applyPerFileFields(fields, meta, { ...msgCtx, fileName: a.name }),
|
||||
DOC_FILE_NAME: { value: a.name, type: "Text" },
|
||||
};
|
||||
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, file, a.name, af);
|
||||
});
|
||||
}
|
||||
@ -720,17 +781,32 @@ async function uploadOnce(part, doUpload) {
|
||||
|
||||
// Indexfelder für eine konkrete Nachricht: gemappte Felder (EML_*/generisch)
|
||||
// werden je Mail aus deren Metadaten neu berechnet, manuelle Werte bleiben.
|
||||
function fieldsForMessage(baseResult, meta, multi) {
|
||||
function fieldsForMessage(baseResult, meta, multi, ctx) {
|
||||
if (!multi) return baseResult; // Einzelmail: manuelle Edits respektieren
|
||||
const result = { ...baseResult };
|
||||
STATE.fields.forEach((f) => {
|
||||
const val = prefillFor(f.name, meta);
|
||||
const val = prefillFor(f.name, meta, ctx);
|
||||
if (val === undefined || val === null || val === "") return;
|
||||
result[f.name] = { value: val, type: f.type };
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Felder, die auf ein dokumentabhängiges Token (fileName) gemappt sind, für
|
||||
// GENAU dieses Dokument setzen – auch bei Einzelmail, weil ein Formularwert
|
||||
// nicht gleichzeitig für .eml und jeden Anhang stimmen kann.
|
||||
function applyPerFileFields(fields, meta, ctx) {
|
||||
if (STATE.isBasket) return fields; // Briefkorb: keine Indexfelder
|
||||
const out = { ...fields };
|
||||
STATE.fields.forEach((f) => {
|
||||
if (!PER_FILE_TOKENS.has(tokenOf(f.name))) return;
|
||||
const val = prefillFor(f.name, meta, ctx);
|
||||
if (val === undefined || val === null || val === "") return;
|
||||
out[f.name] = { value: val, type: f.type };
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// Sticky: zuletzt genutztes Format + Anhänge-Wahl in den Einstellungen merken.
|
||||
async function saveOptionPrefs(mailFormat, storeAttachments) {
|
||||
try {
|
||||
@ -780,6 +856,27 @@ const toDateInput = (d) => {
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${day}`;
|
||||
};
|
||||
// Lokal als YYYY-MM-DDTHH:MM für <input type="datetime-local">.
|
||||
const toDateTimeInput = (d) => `${toDateInput(d)}T${fmtTime(d)}`;
|
||||
// Uhrzeit HH:MM lokal (auch als eigenständiger Text-Token nutzbar).
|
||||
function fmtTime(d) {
|
||||
const dt = d instanceof Date ? d : new Date(d);
|
||||
if (isNaN(dt)) return "";
|
||||
return `${String(dt.getHours()).padStart(2, "0")}:${String(dt.getMinutes()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
// Dateiname, den die E-Mail selbst beim Upload bekäme – für die Vorschau im
|
||||
// Formular. Beim tatsächlichen Upload gilt der echte Name (siehe submit()).
|
||||
function previewFileName() {
|
||||
const subj = (STATE.meta && STATE.meta.subject) || "";
|
||||
const base = subj.replace(/[\\/:*?"<>|]+/g, "_").slice(0, 80).trim() || "email";
|
||||
return `${base}.${getMailFormat() === "pdf" ? "pdf" : "eml"}`;
|
||||
}
|
||||
|
||||
// Kontext für die Token-Auflösung im Formular (Vorschauwerte).
|
||||
function formCtx() {
|
||||
return { fileName: previewFileName(), attachmentCount: STATE.attachments.length };
|
||||
}
|
||||
// Sicheres CSS-Escaping für Attribut-Selektoren (Feldnamen aus DocuWare).
|
||||
const cssEsc = (s) =>
|
||||
window.CSS && CSS.escape ? CSS.escape(String(s)) : String(s).replace(/["\\\]]/g, "\\$&");
|
||||
@ -811,8 +908,21 @@ $("cancel").addEventListener("click", () => window.close());
|
||||
|
||||
// Sticky: Optionen sofort bei Änderung merken (nicht erst beim Ablegen).
|
||||
document.querySelectorAll('input[name="mailFormat"]').forEach((r) =>
|
||||
r.addEventListener("change", () => saveOptionPrefs(getMailFormat(), $("optAtt").checked))
|
||||
r.addEventListener("change", () => {
|
||||
refreshPerFilePreview(); // .eml ↔ .pdf: Dateiname-Vorschau nachziehen
|
||||
saveOptionPrefs(getMailFormat(), $("optAtt").checked);
|
||||
})
|
||||
);
|
||||
|
||||
// Aktualisiert die Vorschauwerte dokumentabhängiger Felder (Dateiname).
|
||||
function refreshPerFilePreview() {
|
||||
const ctx = formCtx();
|
||||
STATE.fields.forEach((f) => {
|
||||
if (!PER_FILE_TOKENS.has(tokenOf(f.name))) return;
|
||||
const val = prefillFor(f.name, STATE.meta, ctx);
|
||||
if (val !== undefined && val !== null && val !== "") setFieldValue(f.name, f, val);
|
||||
});
|
||||
}
|
||||
$("optAtt").addEventListener("change", () => {
|
||||
const show = !isMulti() && $("optAtt").checked && STATE.attachments.length > 0;
|
||||
$("attList").style.display = show ? "" : "none";
|
||||
|
||||
@ -429,6 +429,21 @@ const DocuWare = {
|
||||
|
||||
_field(name, value, type) {
|
||||
const t = (type || "Text").toLowerCase();
|
||||
// WICHTIG: "datetime" zuerst prüfen – "DateTime".includes("date") ist true und
|
||||
// würde sonst die Uhrzeit auf Mitternacht normalisieren (Datenverlust).
|
||||
if (t.includes("datetime")) {
|
||||
const d = value instanceof Date ? value : new Date(value);
|
||||
if (isNaN(d)) return { FieldName: name, Item: String(value), ItemElementName: "DateTime" };
|
||||
// Mit Uhrzeit: echter Zeitpunkt, daher unverändert als ISO (UTC) senden.
|
||||
//
|
||||
// UNGEPRÜFT gegen den Server: ob DocuWare das "Z" auswertet (dann stimmt die
|
||||
// Uhrzeit) oder die ISO-Zeichenkette als lokale Wanduhrzeit übernimmt (dann
|
||||
// erscheint sie um den UTC-Versatz verschoben, in Wien -2 h). Reine Date-Felder
|
||||
// sind davon nicht betroffen, die werden auf UTC-Mitternacht normalisiert.
|
||||
// Falls im Testarchiv verschoben: hier stattdessen die lokale Wanduhrzeit ohne
|
||||
// Zeitzone senden, also `${toLocalIso(d)}` statt d.toISOString().
|
||||
return { FieldName: name, Item: d.toISOString(), ItemElementName: "DateTime" };
|
||||
}
|
||||
if (t.includes("date")) {
|
||||
// DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform.
|
||||
const d = value instanceof Date ? value : new Date(value);
|
||||
|
||||
19
lib/mail.js
19
lib/mail.js
@ -16,9 +16,11 @@ const Mail = {
|
||||
const bodyText = this._extractText(full);
|
||||
|
||||
const sender = this._parseAddress(header.author);
|
||||
const account = await this._accountForMessage(messageId);
|
||||
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,
|
||||
@ -28,7 +30,7 @@ const Mail = {
|
||||
bcc: (header.bccList || []).join(", "),
|
||||
date: header.date instanceof Date ? header.date : new Date(header.date),
|
||||
bodyText,
|
||||
account,
|
||||
account: acc.name,
|
||||
direction: this._direction(header),
|
||||
sizeBytes: header.size || 0,
|
||||
};
|
||||
@ -128,16 +130,23 @@ const Mail = {
|
||||
return { name: "", email: str.trim() };
|
||||
},
|
||||
|
||||
async _accountForMessage(messageId) {
|
||||
/**
|
||||
* 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);
|
||||
return acc ? acc.name : "";
|
||||
if (acc) {
|
||||
const ident = (acc.identities || [])[0];
|
||||
return { name: acc.name || "", email: (ident && ident.email) || "" };
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return "";
|
||||
return { name: "", email: "" };
|
||||
},
|
||||
|
||||
_direction(header) {
|
||||
|
||||
22
lib/store.js
22
lib/store.js
@ -32,6 +32,27 @@ const MAIL_TOKENS = [
|
||||
{ key: "direction", label: "Richtung (Ein-/Ausgang)" },
|
||||
{ key: "size", label: "Größe (Bytes)" },
|
||||
{ key: "account", label: "Konto" },
|
||||
|
||||
// --- Datum/Zeit ---------------------------------------------------------
|
||||
// "date" liefert nur den Kalendertag. Für DocuWare-Felder vom Typ DateTime
|
||||
// gibt es "dateTime" (Tag + Uhrzeit); "time" nur die Uhrzeit als Text.
|
||||
{ key: "dateTime", label: "Datum + Uhrzeit der E-Mail" },
|
||||
{ key: "time", label: "Uhrzeit der E-Mail" },
|
||||
{ key: "archiveDate", label: "Ablagedatum (heute)" },
|
||||
{ key: "archiveDateTime", label: "Ablagezeitpunkt (jetzt)" },
|
||||
|
||||
// --- Dokument -----------------------------------------------------------
|
||||
// fileName wird JE HOCHGELADENEM DOKUMENT gesetzt: .eml/PDF tragen den
|
||||
// Mailnamen, jeder Anhang seinen eigenen Dateinamen.
|
||||
{ key: "fileName", label: "Dateiname (je Dokument)" },
|
||||
{ key: "attachmentCount", label: "Anzahl Anhänge" },
|
||||
{ key: "rfcMessageId", label: "Message-ID (RFC822)" },
|
||||
|
||||
// --- Wer/Woher ----------------------------------------------------------
|
||||
{ key: "dwUser", label: "DocuWare-Benutzer (Mitarbeiter)" },
|
||||
{ key: "dwOrganization", label: "DocuWare-Organisation" },
|
||||
{ key: "accountEmail", label: "Eigene E-Mail-Adresse" },
|
||||
{ key: "folder", label: "Ordner" },
|
||||
];
|
||||
|
||||
// Standard-Zuordnung DocuWare-Feldname -> Mail-Token. Greift, solange der Nutzer
|
||||
@ -53,6 +74,7 @@ const DEFAULT_FIELD_MAP = {
|
||||
EML_SIZE: "size",
|
||||
EML_ACCOUNT: "account",
|
||||
DOC_DATE: "date",
|
||||
DOC_FILE_NAME: "fileName", // .eml/PDF: Mailname, Anhang: eigener Dateiname
|
||||
SUBJECT: "subject",
|
||||
BETREFF: "subject",
|
||||
EMAIL: "senderEmail",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user