diff --git a/components/email/__tests__/viewer-details-per-message.test.tsx b/components/email/__tests__/viewer-details-per-message.test.tsx
new file mode 100644
index 000000000..953d760bc
--- /dev/null
+++ b/components/email/__tests__/viewer-details-per-message.test.tsx
@@ -0,0 +1,66 @@
+import { render, screen, fireEvent, act } from '@testing-library/react';
+import { describe, it, expect, vi } from 'vitest';
+import type { Email, Mailbox } from '@/lib/jmap/types';
+
+// mail-7: "Show details" stayed open for every message opened after it.
+// mail-29: a mail received before it was sent showed a delivery time.
+
+vi.mock('@/hooks/use-media-query', () => ({
+ useDeviceDetection: () => ({ isMobile: false, isTablet: false, isDesktop: true }),
+ useMediaQuery: () => false,
+}));
+vi.mock('@/components/tour/tour-provider', () => ({
+ useTour: () => ({ activeTour: null, startTour: vi.fn(), endTour: vi.fn(), registerStep: vi.fn() }),
+}));
+
+class NoopResizeObserver {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+}
+vi.stubGlobal('ResizeObserver', NoopResizeObserver);
+
+const { EmailViewer } = await import('../email-viewer');
+
+const mailboxes = [
+ { id: 'inbox', name: 'Inbox', role: 'inbox', parentId: null, myRights: { mayAddItems: true } },
+] as unknown as Mailbox[];
+
+const make = (id: string, sentAt: string, receivedAt: string) => ({
+ id, blobId: `b-${id}`, threadId: `t-${id}`, mailboxIds: { inbox: true }, keywords: { $seen: true },
+ from: [{ name: 'Sam', email: 'sam@example.com' }], to: [{ name: 'Bob', email: 'bob@example.com' }],
+ subject: `Message ${id}`, sentAt, receivedAt, preview: 'Hello',
+ hasAttachment: false, size: 100, textBody: [], htmlBody: [], attachments: [], bodyValues: {},
+}) as unknown as Email;
+
+const viewer = (email: Email) => (
+
+);
+
+describe('message details panel', () => {
+ it('closes when another message opens', () => {
+ const first = make('e1', '2026-11-03T10:00:00Z', '2026-11-03T10:00:05Z');
+ const second = make('e2', '2026-11-03T11:00:00Z', '2026-11-03T11:00:05Z');
+ const { rerender } = render(viewer(first));
+ act(() => { fireEvent.click(screen.getAllByText('show_details')[0]); });
+ expect(screen.queryByText('details.recipients_routing')).not.toBeNull();
+ rerender(viewer(second));
+ expect(screen.queryByText('details.recipients_routing')).toBeNull();
+ // And back on the first one it is closed too: it was opened for that view.
+ rerender(viewer(first));
+ expect(screen.queryByText('details.recipients_routing')).toBeNull();
+ });
+
+ it('shows no delivery time for a mail received before it was sent', () => {
+ render(viewer(make('e3', '2026-11-03T21:40:00Z', '2026-11-03T20:40:00Z')));
+ act(() => { fireEvent.click(screen.getAllByText('show_details')[0]); });
+ expect(screen.queryByText(/details\.delivery_time/i)).toBeNull();
+ expect(document.body.textContent).not.toMatch(/delivery_time/);
+ });
+
+ it('still shows a real delivery time', () => {
+ render(viewer(make('e4', '2026-11-03T20:40:00Z', '2026-11-03T21:40:00Z')));
+ act(() => { fireEvent.click(screen.getAllByText('show_details')[0]); });
+ expect(document.body.textContent).toMatch(/delivery_time/);
+ });
+});
diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx
index b18c7c983..5d6ece619 100644
--- a/components/email/email-viewer.tsx
+++ b/components/email/email-viewer.tsx
@@ -1670,7 +1670,13 @@ export function EmailViewer({
}, [onRescheduleScheduled, t, tComposer, scheduledSendAt, timeFormat]);
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
const { startTour } = useTour();
- const [showFullHeaders, setShowFullHeaders] = useState(false);
+ // The details panel belongs to the message it was opened on: the next
+ // message opens without it (mail-7).
+ const shownEmailId = email?.id ?? null;
+ const [details, setDetails] = useState<{ emailId: string | null; open: boolean }>({ emailId: shownEmailId, open: false });
+ if (details.emailId !== shownEmailId) setDetails({ emailId: shownEmailId, open: false });
+ const showFullHeaders = details.open && details.emailId === shownEmailId;
+ const setShowFullHeaders = (open: boolean) => setDetails({ emailId: shownEmailId, open });
const [isDownloadingAll, setIsDownloadingAll] = useState(false);
const [imageThumbUrls, setImageThumbUrls] = useState>({});
const [allowExternalContent, setAllowExternalContent] = useState(false);
@@ -5516,8 +5522,10 @@ ${printWatermark ? printWatermark.html : ''}
};
const replyToDifferent = !!email.replyTo?.length &&
(!email.from || email.replyTo[0].email !== email.from[0]?.email);
+ // Received before it was sent means a wrong clock somewhere, not a
+ // delivery time (mail-29).
const deliveryDeltaMs = email.sentAt && email.receivedAt
- ? Math.abs(new Date(email.receivedAt).getTime() - new Date(email.sentAt).getTime())
+ ? new Date(email.receivedAt).getTime() - new Date(email.sentAt).getTime()
: 0;
const formatDelta = (diff: number) => {
const minutes = Math.floor(diff / 60000);