diff --git a/lib/mailer.js b/lib/mailer.js
index d5e12df..2838781 100644
--- a/lib/mailer.js
+++ b/lib/mailer.js
@@ -175,5 +175,6 @@ module.exports = {
fromField,
fromAddress,
textToHtml,
+ htmlToText,
config: cfg,
};
diff --git a/server.js b/server.js
index eba322a..d8114df 100644
--- a/server.js
+++ b/server.js
@@ -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 '
' +
+ '' +
+ '' +
+ String(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(
diff --git a/views/bewerbung.ejs b/views/bewerbung.ejs
index 29aef44..a69e5e3 100644
--- a/views/bewerbung.ejs
+++ b/views/bewerbung.ejs
@@ -336,7 +336,13 @@
<%= e.email_date ? new Date(e.email_date).toLocaleString('de-DE') : '' %>