From f4d34477a663b6c62902e7e974fc46c65ec31e0d Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sun, 15 Mar 2026 18:52:01 +0100
Subject: [PATCH] feat(email-viewer): improve message details and contact
sidebar
- Show sender name and email in message details From field
- Add "Add to contacts" button in contact sidebar for unknown senders
- Add info tooltips explaining SPF, DKIM, DMARC, and Spam Score
- Use portal-based tooltips to prevent overflow clipping
- Move desktop sender info into scrollable area to fix scroll when details expanded
---
components/email/email-viewer.tsx | 109 +++++++++++++++++++++++++++---
1 file changed, 99 insertions(+), 10 deletions(-)
diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx
index e154c07a..6e9001a3 100644
--- a/components/email/email-viewer.tsx
+++ b/components/email/email-viewer.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useMemo, useRef, useCallback } from "react";
+import ReactDOM from "react-dom";
import DOMPurify from "dompurify";
import { Email, ContactCard, Mailbox } from "@/lib/jmap/types";
import { EMAIL_SANITIZE_CONFIG, collapseBlockedImageContainers } from "@/lib/email-sanitization";
@@ -59,6 +60,7 @@ import {
Folder,
Sun,
Moon,
+ HelpCircle,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
@@ -209,13 +211,17 @@ function renderClickableRecipients(
function ContactSidebarPanel({
email,
contact,
+ senderName,
onClose,
+ onAddToContacts,
}: {
email: string;
contact: ContactCard | null;
+ senderName?: string;
onClose: () => void;
+ onAddToContacts?: (email: string, name?: string) => void;
}) {
- const name = contact ? getContactDisplayName(contact) : null;
+ const name = contact ? getContactDisplayName(contact) : senderName || null;
const primaryEmail = contact ? getContactPrimaryEmail(contact) : email;
const emails = contact?.emails ? Object.values(contact.emails) : [];
const phones = contact?.phones ? Object.values(contact.phones) : [];
@@ -374,10 +380,19 @@ function ContactSidebarPanel({
{/* No contact found message */}
{!contact && (
-
+
Not in your contacts
+ {onAddToContacts && (
+
+ )}
)}
@@ -397,6 +412,46 @@ function SidebarSection({ icon: Icon, title, children }: { icon: React.Component
);
}
+function InfoTooltip({ text }: { text: string }) {
+ const [open, setOpen] = useState(false);
+ const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
+ const btnRef = useRef(null);
+
+ const show = () => {
+ if (btnRef.current) {
+ const rect = btnRef.current.getBoundingClientRect();
+ setPos({ top: rect.top - 8, left: rect.left + rect.width / 2 });
+ }
+ setOpen(true);
+ };
+ const hide = () => setOpen(false);
+
+ return (
+
+
+ {open && pos && ReactDOM.createPortal(
+
+ {text}
+ ,
+ document.body
+ )}
+
+ );
+}
+
export function EmailViewer({
email,
isLoading = false,
@@ -2274,6 +2329,7 @@ export function EmailViewer({
` : undefined}
onViewContact={handleViewContactSidebar}
className="text-sm"
/>
@@ -2372,8 +2428,11 @@ export function EmailViewer({
}
{getSecurityStatus(email.authenticationResults.spf.result).icon === 'minus' &&
}
-
-
SPF
+
+
+ SPF
+
+
{email.authenticationResults.spf.result}
@@ -2403,8 +2462,11 @@ export function EmailViewer({
}
{getSecurityStatus(email.authenticationResults.dkim.result).icon === 'minus' &&
}
-
-
DKIM
+
+
+ DKIM
+
+
{email.authenticationResults.dkim.result}
@@ -2434,8 +2496,11 @@ export function EmailViewer({
}
{getSecurityStatus(email.authenticationResults.dmarc.result).icon === 'minus' &&
}
-
-
DMARC
+
+
+ DMARC
+
+
{email.authenticationResults.dmarc.result}
@@ -2464,8 +2529,11 @@ export function EmailViewer({
email.spamScore > 2 ? "text-amber-700 dark:text-amber-400" :
"text-green-700 dark:text-green-400"
)} />
-
-
Spam Score
+
+
+ Spam Score
+
+
5 ? "text-red-700 dark:text-red-400" :
@@ -3010,7 +3078,28 @@ export function EmailViewer({
{
+ const allRecipients = [...(email?.from || []), ...(email?.to || []), ...(email?.cc || []), ...(email?.bcc || []), ...(email?.replyTo || [])];
+ return allRecipients.find(r => r.email.toLowerCase() === contactSidebarEmail.toLowerCase())?.name;
+ })()}
onClose={() => setContactSidebarEmail(null)}
+ onAddToContacts={(addr, name) => {
+ const { createContact, addLocalContact, supportsSync } = useContactStore.getState();
+ const client = useAuthStore.getState().client;
+ const contactData: Partial = {
+ emails: { email: { address: addr } },
+ ...(name ? { name: { components: name.includes(' ')
+ ? [{ kind: 'given' as const, value: name.split(' ')[0] }, { kind: 'surname' as const, value: name.split(' ').slice(1).join(' ') }]
+ : [{ kind: 'given' as const, value: name }]
+ }} : {}),
+ };
+ if (client && supportsSync) {
+ createContact(client, contactData).then(() => toast.success('Contact added'));
+ } else {
+ addLocalContact({ id: `local-${crypto.randomUUID()}`, addressBookIds: {}, ...contactData } as ContactCard);
+ toast.success('Contact added');
+ }
+ }}
/>
)}