E-Mails als HTML anzeigen; Antworten zitieren Vornachricht (auch KI)
- HTML-Mails werden in einem sandboxed iframe (ohne allow-scripts) gerendert statt als Quelltext angezeigt; Höhe an Inhalt angepasst, Skripte/Tracking laufen nicht, Layout bleibt isoliert. Reine Text-Mails weiterhin als pre-wrap. Gilt für Postfach und Bewerbung. - Antwortformular ist wie im Mailclient mit der zitierten Vornachricht vorbelegt (Attribution + "> "-Zeilen), Cursor darüber. - KI-Antwort hängt das Zitat unter den generierten Text und erhält den bereinigten Klartext (statt HTML) als Eingabe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,58 @@ if (!fs.existsSync(emailAnhaengeDir)) {
|
||||
fs.mkdirSync(emailAnhaengeDir, { recursive: true });
|
||||
}
|
||||
|
||||
// --- E-mail display + reply helpers --------------------------------------
|
||||
// A stored message is rendered as HTML when we have an HTML body (or the plain
|
||||
// body is actually HTML markup — some senders put HTML in the text/plain part).
|
||||
// Otherwise it is shown as plain text. HTML is displayed inside a sandboxed
|
||||
// iframe (see the views), so scripts never run.
|
||||
function looksLikeHtml(s) {
|
||||
return /<(?:!doctype|html|body|div|table|p|br|span|a|img|ul|ol|h[1-6])\b|<\/[a-z]/i.test(String(s || ''));
|
||||
}
|
||||
function emailDisplayHtml(e) {
|
||||
if (e.body_html && e.body_html.trim()) return e.body_html;
|
||||
if (e.body_text && looksLikeHtml(e.body_text)) return e.body_text;
|
||||
return null;
|
||||
}
|
||||
// Wrap raw e-mail HTML in a minimal document for the sandboxed iframe: a white
|
||||
// background, readable defaults, images constrained to the width and links that
|
||||
// open in a new tab. No scripts are enabled by the iframe sandbox.
|
||||
function buildEmailSrcdoc(html) {
|
||||
return '<!doctype html><html><head><meta charset="utf-8">' +
|
||||
'<meta name="referrer" content="no-referrer"><base target="_blank">' +
|
||||
'<style>html,body{margin:0;padding:10px;background:#fff;color:#111;' +
|
||||
'font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:1.5;' +
|
||||
'word-break:break-word;overflow-wrap:anywhere}' +
|
||||
'img{max-width:100%!important;height:auto}table{max-width:100%!important}' +
|
||||
'a{color:#2563eb}*{max-width:100%}</style></head><body>' +
|
||||
String(html || '') + '</body></html>';
|
||||
}
|
||||
// Plain-text version of a message body, used as the source for reply quoting.
|
||||
function emailPlainText(e) {
|
||||
const html = emailDisplayHtml(e);
|
||||
if (html && (!e.body_text || looksLikeHtml(e.body_text))) return mailer.htmlToText(html);
|
||||
return String(e.body_text || '');
|
||||
}
|
||||
// Build a mail-client style quote of a received message: an attribution line
|
||||
// followed by the original body with every line prefixed by "> ".
|
||||
function buildReplyQuote(e) {
|
||||
const d = e.email_date ? new Date(e.email_date) : null;
|
||||
const when = d && !isNaN(d.getTime()) ? d.toLocaleString('de-DE') : '';
|
||||
const who = String(e.from_addr || '').trim();
|
||||
const src = emailPlainText(e).replace(/\r\n/g, '\n').replace(/\s+$/, '');
|
||||
const quoted = src.split('\n').map((l) => '> ' + l).join('\n');
|
||||
const attribution = who ? `Am ${when} schrieb ${who}:` : (when ? `Am ${when}:` : '');
|
||||
return (attribution ? attribution + '\n\n' : '') + quoted;
|
||||
}
|
||||
// Attach display fields (and, for received mail, a reply quote) to each row.
|
||||
function decorateEmails(emails) {
|
||||
emails.forEach((e) => {
|
||||
const html = emailDisplayHtml(e);
|
||||
e.display_srcdoc = html ? buildEmailSrcdoc(html) : null;
|
||||
if (e.direction !== 'out') e.reply_quote = buildReplyQuote(e);
|
||||
});
|
||||
}
|
||||
|
||||
// Directory for static extra attachments (e.g. Zeugnisse) the user uploads once
|
||||
// and that are sent along with every generated application.
|
||||
const basisAnhaengeDir = path.join(dataDir, 'basis_anhaenge');
|
||||
@@ -1187,6 +1239,8 @@ initializeDatabase().then(() => {
|
||||
const m = String(e.from_addr || '').match(/<([^>]+)>/);
|
||||
e.from_addr_clean = m ? m[1] : String(e.from_addr || '').trim();
|
||||
});
|
||||
// HTML rendering (sandboxed iframe) + a quote of each received message.
|
||||
decorateEmails(emails);
|
||||
// Mark received messages as read now that they are shown.
|
||||
await dbRun("UPDATE emails SET seen = 1 WHERE bewerbung_id = ? AND direction = 'in' AND seen = 0", [id]);
|
||||
}
|
||||
@@ -1334,12 +1388,13 @@ initializeDatabase().then(() => {
|
||||
const settings = await dbGet('SELECT * FROM settings WHERE id = 1');
|
||||
|
||||
const draft = await generateEmailReply({
|
||||
incoming: { from: orig.from_addr, subject: orig.subject, text: orig.body_text },
|
||||
incoming: { from: orig.from_addr, subject: orig.subject, text: emailPlainText(orig) },
|
||||
job: { firma: bewerbung.firma, stelle: bewerbung.stelle },
|
||||
settings,
|
||||
hinweise: String(req.body.hinweise || ''),
|
||||
});
|
||||
res.json(draft);
|
||||
// Include the quoted original so the reply reads like a mail-client thread.
|
||||
res.json({ ...draft, quote: buildReplyQuote(orig) });
|
||||
} catch (error) {
|
||||
console.error('Error drafting AI reply:', error);
|
||||
res.status(500).json({ error: error.message || 'Serverfehler' });
|
||||
@@ -1390,6 +1445,7 @@ initializeDatabase().then(() => {
|
||||
const byEmail = {};
|
||||
atts.forEach((a) => { (byEmail[a.email_id] = byEmail[a.email_id] || []).push(a); });
|
||||
emails.forEach((e) => { e.anhaenge = byEmail[e.id] || []; });
|
||||
decorateEmails(emails);
|
||||
}
|
||||
// Applications the user can assign an e-mail to (newest first).
|
||||
const bewerbungen = await dbAll(
|
||||
|
||||
Reference in New Issue
Block a user