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:
2026-07-04 23:06:59 +02:00
co-authored by Claude Opus 4.8
parent 7ceff39364
commit b1531663bb
4 changed files with 128 additions and 11 deletions
+41 -5
View File
@@ -336,7 +336,13 @@
<span class="text-xs text-gray-500 dark:text-gray-400 shrink-0"><%= e.email_date ? new Date(e.email_date).toLocaleString('de-DE') : '' %></span>
</div>
<p class="text-sm font-medium text-gray-800 dark:text-gray-100 mb-1"><%= e.subject || '(kein Betreff)' %></p>
<p class="whitespace-pre-wrap break-words text-sm leading-relaxed text-gray-700 dark:text-gray-300"><%= e.body_text || '' %></p>
<% if (e.display_srcdoc) { %>
<iframe class="email-html-frame w-full border border-gray-200 dark:border-gray-600 rounded bg-white" style="height:6rem"
sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox" referrerpolicy="no-referrer"
srcdoc="<%= e.display_srcdoc %>"></iframe>
<% } else { %>
<p class="whitespace-pre-wrap break-words text-sm leading-relaxed text-gray-700 dark:text-gray-300"><%= e.body_text || '' %></p>
<% } %>
<% if (e.anhaenge && e.anhaenge.length) { %>
<div class="mt-2 flex flex-wrap gap-2">
@@ -369,8 +375,8 @@
<span class="ai-reply-label">KI-Antwort generieren</span>
</button>
</div>
<textarea name="body" rows="8" required
class="reply-body w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white text-sm leading-relaxed"></textarea>
<textarea name="body" rows="12" required
class="reply-body w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white text-sm leading-relaxed"><%= e.reply_quote ? '\n\n' + e.reply_quote : '' %></textarea>
<div class="flex justify-end mt-2">
<button type="submit" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">Antwort senden</button>
</div>
@@ -688,11 +694,36 @@
} catch (e) { /* clipboard blocked — ignore */ }
});
// Size a sandboxed e-mail iframe to its content height. The sandbox
// enables allow-same-origin (but not allow-scripts), so scripts in the
// mail never run, yet we can still measure the rendered document.
function fitFrame(f) {
try {
const doc = f.contentDocument || (f.contentWindow && f.contentWindow.document);
if (!doc || !doc.body) return;
const h = Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight);
if (h > 0) f.style.height = (h + 6) + 'px';
} catch (e) { /* cross-origin (shouldn't happen) — leave default height */ }
}
document.querySelectorAll('.email-html-frame').forEach((f) => {
f.addEventListener('load', () => fitFrame(f));
fitFrame(f);
// Late reflow once embedded images have loaded.
setTimeout(() => fitFrame(f), 400);
setTimeout(() => fitFrame(f), 1500);
});
// Toggle inline reply forms under received e-mails.
document.querySelectorAll('.reply-toggle').forEach((btn) => {
btn.addEventListener('click', () => {
const form = document.getElementById(btn.dataset.target);
if (form) form.classList.toggle('hidden');
if (!form) return;
form.classList.toggle('hidden');
// On open, put the cursor above the quoted text.
if (!form.classList.contains('hidden')) {
const ta = form.querySelector('.reply-body');
if (ta) { ta.focus(); try { ta.setSelectionRange(0, 0); } catch (e) {} ta.scrollTop = 0; }
}
});
});
@@ -715,7 +746,12 @@
if (!res.ok) throw new Error(data.error || 'Fehler');
const body = form.querySelector('.reply-body');
const subject = form.querySelector('.reply-subject');
if (body && data.text) body.value = data.text;
if (body && data.text) {
// Generated reply on top, quoted original beneath — like a mail client.
body.value = data.text + (data.quote ? '\n\n' + data.quote : '');
body.focus();
try { body.setSelectionRange(data.text.length, data.text.length); } catch (e) {}
}
if (subject && data.betreff) subject.value = data.betreff;
} catch (e) {
alert('KI-Antwort fehlgeschlagen: ' + e.message);
+28 -4
View File
@@ -60,10 +60,16 @@
<span class="text-xs text-gray-500 dark:text-gray-400 shrink-0"><%= e.email_date ? new Date(e.email_date).toLocaleString('de-DE') : '' %></span>
</div>
<p class="text-sm font-medium text-gray-800 dark:text-gray-100 mb-1"><%= e.subject || '(kein Betreff)' %></p>
<p class="email-body whitespace-pre-wrap break-words text-sm leading-relaxed text-gray-700 dark:text-gray-300 max-h-40 overflow-hidden"><%= e.body_text || '' %></p>
<% if (e.body_text && e.body_text.length > 600) { %>
<button type="button" class="email-toggle text-xs text-blue-600 hover:text-blue-800 dark:text-blue-400 hover:underline mt-1"
data-expanded="false">Vollständig anzeigen</button>
<% if (e.display_srcdoc) { %>
<iframe class="email-html-frame w-full border border-gray-200 dark:border-gray-600 rounded bg-white" style="height:6rem"
sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox" referrerpolicy="no-referrer"
srcdoc="<%= e.display_srcdoc %>"></iframe>
<% } else { %>
<p class="email-body whitespace-pre-wrap break-words text-sm leading-relaxed text-gray-700 dark:text-gray-300 max-h-40 overflow-hidden"><%= e.body_text || '' %></p>
<% if (e.body_text && e.body_text.length > 600) { %>
<button type="button" class="email-toggle text-xs text-blue-600 hover:text-blue-800 dark:text-blue-400 hover:underline mt-1"
data-expanded="false">Vollständig anzeigen</button>
<% } %>
<% } %>
<% if (e.anhaenge && e.anhaenge.length) { %>
@@ -146,6 +152,24 @@
const cy = document.getElementById('currentYear');
if (cy) cy.textContent = new Date().getFullYear();
// Size each sandboxed e-mail iframe to its rendered content. The
// sandbox omits allow-scripts, so mail scripts never run; allow-same-
// origin lets us measure the document height.
function fitFrame(f) {
try {
var doc = f.contentDocument || (f.contentWindow && f.contentWindow.document);
if (!doc || !doc.body) return;
var h = Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight);
if (h > 0) f.style.height = (h + 6) + 'px';
} catch (e) { /* leave default height */ }
}
document.querySelectorAll('.email-html-frame').forEach(function (f) {
f.addEventListener('load', function () { fitFrame(f); });
fitFrame(f);
setTimeout(function () { fitFrame(f); }, 400);
setTimeout(function () { fitFrame(f); }, 1500);
});
// Expand/collapse the full e-mail body for each card.
document.querySelectorAll('.email-toggle').forEach(function (btn) {
btn.addEventListener('click', function () {