fix: render HTML-only emails and redesign external content prompt

This commit is contained in:
Linus Rath
2026-05-07 16:10:26 +02:00
parent 3b36738192
commit 9225ba0790
2 changed files with 56 additions and 32 deletions
+50 -30
View File
@@ -2296,8 +2296,12 @@ export function EmailViewer({
htmlContent = email.bodyValues[email.htmlBody[0].partId].value; htmlContent = email.bodyValues[email.htmlBody[0].partId].value;
// Prefer textBody when HTML is auto-generated minimal wrapper (no rich formatting). // Prefer textBody when HTML is auto-generated minimal wrapper (no rich formatting).
// Server-generated HTML from text/plain emails often lacks <br> tags, collapsing newlines. // Server-generated HTML from text/plain emails often lacks <br> tags, collapsing newlines.
const hasTextBody = email.textBody?.[0]?.partId && email.bodyValues[email.textBody[0].partId]; // Per RFC 8621, an HTML-only email exposes the same partId in both htmlBody and textBody —
if (hasTextBody && htmlContent) { // in that case there is no real plain-text alternative, so always render the HTML.
const textPartId = email.textBody?.[0]?.partId;
const htmlPartId = email.htmlBody[0].partId;
const hasDistinctTextBody = !!textPartId && textPartId !== htmlPartId && !!email.bodyValues[textPartId];
if (hasDistinctTextBody && htmlContent) {
useHtmlVersion = hasMeaningfulHtmlBody(htmlContent); useHtmlVersion = hasMeaningfulHtmlBody(htmlContent);
} else { } else {
useHtmlVersion = !!htmlContent; useHtmlVersion = !!htmlContent;
@@ -4562,34 +4566,50 @@ export function EmailViewer({
<div className="flex flex-col gap-3 isolate"> <div className="flex flex-col gap-3 isolate">
{/* External Content Controls */} {/* External Content Controls */}
{hasBlockedContent && !allowExternalContent && externalContentPolicy !== 'allow' && ( {hasBlockedContent && !allowExternalContent && externalContentPolicy !== 'allow' && (
<div className="flex items-center gap-3 flex-wrap md:justify-center rounded-md px-3 py-1 bg-muted/50 dark:bg-muted/30"> <div className="flex items-start gap-3 py-1">
{externalContentPolicy === 'ask' && ( <div className="w-10 h-10 rounded-full bg-info/15 text-info flex items-center justify-center flex-shrink-0 shadow-sm">
<button <Image className="w-5 h-5" />
onClick={() => setAllowExternalContent(true)} </div>
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground bg-transparent hover:bg-transparent transition-colors min-h-[44px] md:min-h-0" <div className="flex-1 min-w-0 space-y-2">
> <div>
<Image className="w-3.5 h-3.5" /> <div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
{t('load_external_content')} External Content
</button> </div>
)} <div className="text-sm font-medium text-foreground break-words">
{email.from?.[0]?.email && ( {t('external_content_warning')}
<button </div>
onClick={() => { </div>
const senderEmail = email.from?.[0]?.email; <div className="flex flex-wrap items-center gap-1.5 pt-0.5">
if (senderEmail) { {externalContentPolicy === 'ask' && (
if (trustedSendersAddressBook && client) { <button
addToTrustedSendersBook(client, senderEmail).catch(console.error); onClick={() => setAllowExternalContent(true)}
} else { className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground px-3 py-1.5 rounded-md border border-border hover:bg-muted transition-colors min-h-[36px]"
addTrustedSender(senderEmail); >
} <Image className="w-3.5 h-3.5" />
setAllowExternalContent(true); {t('load_external_content')}
} </button>
}} )}
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground bg-transparent hover:bg-transparent transition-colors min-h-[44px] md:min-h-0" {email.from?.[0]?.email && (
> <button
{t('trust_sender')} onClick={() => {
</button> const senderEmail = email.from?.[0]?.email;
)} if (senderEmail) {
if (trustedSendersAddressBook && client) {
addToTrustedSendersBook(client, senderEmail).catch(console.error);
} else {
addTrustedSender(senderEmail);
}
setAllowExternalContent(true);
}
}}
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground px-3 py-1.5 rounded-md border border-border hover:bg-muted transition-colors min-h-[36px]"
>
<ShieldCheck className="w-3.5 h-3.5" />
{t('trust_sender')}
</button>
)}
</div>
</div>
</div> </div>
)} )}
@@ -331,8 +331,12 @@ function EmailCard({
htmlContent = email.bodyValues[email.htmlBody[0].partId].value; htmlContent = email.bodyValues[email.htmlBody[0].partId].value;
// Prefer textBody when HTML is auto-generated minimal wrapper (no rich formatting). // Prefer textBody when HTML is auto-generated minimal wrapper (no rich formatting).
// Server-generated HTML from text/plain emails often lacks <br> tags, collapsing newlines. // Server-generated HTML from text/plain emails often lacks <br> tags, collapsing newlines.
const hasTextBody = email.textBody?.[0]?.partId && email.bodyValues[email.textBody[0].partId]; // Per RFC 8621, an HTML-only email exposes the same partId in both htmlBody and textBody —
if (hasTextBody && htmlContent) { // in that case there is no real plain-text alternative, so always render the HTML.
const textPartId = email.textBody?.[0]?.partId;
const htmlPartId = email.htmlBody[0].partId;
const hasDistinctTextBody = !!textPartId && textPartId !== htmlPartId && !!email.bodyValues[textPartId];
if (hasDistinctTextBody && htmlContent) {
useHtmlVersion = hasMeaningfulHtmlBody(htmlContent); useHtmlVersion = hasMeaningfulHtmlBody(htmlContent);
} else { } else {
useHtmlVersion = !!htmlContent; useHtmlVersion = !!htmlContent;