diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index 3a185bd2..1ab56931 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -4143,310 +4143,6 @@ export function EmailViewer({ - {/* Expandable Details */} - {showFullHeaders && (() => { - const translateAuthResult = (result?: string) => { - const r = (result || '').toLowerCase(); - switch (r) { - case 'pass': return t('authentication.result.pass'); - case 'fail': return t('authentication.result.fail'); - case 'softfail': return t('authentication.result.softfail'); - case 'neutral': return t('authentication.result.neutral'); - case 'permerror': return t('authentication.result.permerror'); - case 'temperror': return t('authentication.result.temperror'); - case 'none': return t('authentication.result.none'); - default: return result || ''; - } - }; - const replyToDifferent = !!email.replyTo?.length && - (!email.from || email.replyTo[0].email !== email.from[0]?.email); - const deliveryDeltaMs = email.sentAt && email.receivedAt - ? Math.abs(new Date(email.receivedAt).getTime() - new Date(email.sentAt).getTime()) - : 0; - const formatDelta = (diff: number) => { - const minutes = Math.floor(diff / 60000); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - const dayUnit = days > 1 ? t('time.days') : t('time.day'); - const hourUnit = (hours % 24) > 1 ? t('time.hours') : t('time.hour'); - const minuteUnit = (minutes % 60) > 1 ? t('time.minutes') : t('time.minute'); - const minuteUnitSingle = minutes > 1 ? t('time.minutes') : t('time.minute'); - if (days > 0) return `${days} ${dayUnit} ${hours % 24} ${hourUnit}`; - if (hours > 0) return `${hours} ${hourUnit} ${minutes % 60} ${minuteUnit}`; - return `${minutes} ${minuteUnitSingle}`; - }; - const fullDate = (iso?: string) => iso - ? formatDateTime(iso, timeFormat, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', second: '2-digit', timeZoneName: 'short' }) - : '-'; - const auth = email.authenticationResults; - const totalAttachmentSize = effectiveAttachments.reduce((s, a) => s + (a.size || 0), 0); - const topMimeType = email.bodyStructure?.type; - const SectionHeader = ({ children }: { children: React.ReactNode }) => ( -
- {children} -
- ); - const Row = ({ label, children, mono }: { label: string; children: React.ReactNode; mono?: boolean }) => ( - <> -
{label}
-
{children}
- - ); - const AuthChip = ({ name, result, extra, tooltip }: { name: string; result?: string; extra?: React.ReactNode; tooltip?: string }) => { - if (!result) return null; - const status = getSecurityStatus(result); - const Icon = status.icon === 'check' ? Check - : status.icon === 'x' ? X - : status.icon === 'alert' ? AlertTriangle - : Minus; - return ( - - - {name} - - {translateAuthResult(result)} - - {extra && ( - <> - · - {extra} - - )} - - ); - }; - - const hasIdentifiers = !!(email.messageId || email.inReplyTo?.length || email.references?.length || email.threadId); - const hasListInfo = !!(listHeaders?.listId || listHeaders?.listUnsubscribe || listHeaders?.listHelp || listHeaders?.listPost); - const hasAuthSection = !!(auth?.spf || auth?.dkim || auth?.dmarc || auth?.iprev || email.spamScore !== undefined || email.spamLLM); - - return ( -
-
- {t('details.recipients_routing')} -
- -
- ` : undefined} - onViewContact={handleViewContactSidebar} - className="text-sm text-left" - /> -
-
- {replyToDifferent && ( - -
- {email.replyTo!.map((r, i) => ( - - ))} -
-
- )} - {email.to && email.to.length > 0 && ( - -
- {renderClickableRecipients(email.to, currentUserEmail, t, handleViewContactSidebar, 100)} -
-
- )} - {email.cc && email.cc.length > 0 && ( - -
- {renderClickableRecipients(email.cc, currentUserEmail, t, handleViewContactSidebar, 100)} -
-
- )} - {email.bcc && email.bcc.length > 0 && ( - -
- {renderClickableRecipients(email.bcc, currentUserEmail, t, handleViewContactSidebar, 100)} -
-
- )} - {email.sentAt && ( - {fullDate(email.sentAt)} - )} - - {fullDate(email.receivedAt)} - {deliveryDeltaMs > 60000 && ( - · {formatDelta(deliveryDeltaMs)} {t('details.delivery_time').toLowerCase()} - )} - -
-
- - {hasAuthSection && ( -
- {t('details.authentication_security')} -
- {auth?.spf && ( - - )} - {auth?.dkim && ( - - )} - {auth?.dmarc && ( - - )} - {auth?.iprev && ( - - )} - {email.spamScore !== undefined && ( - 5 ? "bg-red-500/[0.07] border-red-500/30" : - email.spamScore > 2 ? "bg-amber-500/[0.07] border-amber-500/30" : - "bg-green-500/[0.07] border-green-500/30", - )}> - 5 ? "text-red-700 dark:text-red-400" : - email.spamScore > 2 ? "text-amber-700 dark:text-amber-400" : - "text-green-700 dark:text-green-400", - )} /> - {t('authentication.spam_score')} - 5 ? "text-red-700 dark:text-red-400" : - email.spamScore > 2 ? "text-amber-700 dark:text-amber-400" : - "text-green-700 dark:text-green-400", - )}> - {email.spamScore.toFixed(1)} - - {email.spamStatus && ( - <> - · - {email.spamStatus} - - )} - - )} -
- {email.spamLLM && ( -
- {email.spamLLM.verdict === 'LEGITIMATE' ? : - email.spamLLM.verdict === 'SPAM' ? : - } -
- - {email.spamLLM.verdict} - - · {email.spamLLM.explanation} -
-
- )} -
- )} - - {hasIdentifiers && ( -
- {t('details.identifiers_threading')} -
- {email.messageId && ( - {email.messageId} - )} - {email.inReplyTo && email.inReplyTo.length > 0 && ( - -
- {email.inReplyTo.map((id, i) =>
{id}
)} -
-
- )} - {email.references && email.references.length > 0 && ( - -
- - - {t(email.references.length === 1 ? 'previous_messages' : 'previous_messages_plural', { count: email.references.length })} - -
- {email.references.map((id, i) =>
{id}
)} -
-
-
- )} - {email.threadId && ( - {email.threadId} - )} -
-
- )} - -
- {t('details.message_properties')} -
- {email.subject !== undefined && ( - {email.subject || {t('details.no_subject')}} - )} - - {formatFileSize(email.size)} - {topMimeType && ( - · {topMimeType} - )} - - {effectiveAttachments.length > 0 && ( - - {t('details.attachments_summary', { - count: effectiveAttachments.length, - size: formatFileSize(totalAttachmentSize), - })} - - )} - {email.accountLabel && ( - {email.accountLabel} - )} -
-
- - {hasListInfo && ( -
- {t('details.mailing_list')} -
- {listHeaders?.listId && ( - {listHeaders.listId} - )} - {listHeaders?.listUnsubscribe?.preferred && ( - - - {listHeaders.listUnsubscribe.preferred === 'http' - ? listHeaders.listUnsubscribe.http - : listHeaders.listUnsubscribe.mailto} - - - )} - {listHeaders?.listHelp && ( - {listHeaders.listHelp} - )} - {listHeaders?.listPost && ( - {listHeaders.listPost} - )} -
-
- )} -
- ); - })()} {/* Attachments on the right (beside-sender mode) */} @@ -4648,6 +4344,22 @@ export function EmailViewer({ )} )} + {/* Date/time + size on the right (mobile) */} @@ -4664,6 +4376,313 @@ export function EmailViewer({ + {/* Expandable Details (shared across mobile/tablet/desktop) */} + {showFullHeaders && (() => { + const translateAuthResult = (result?: string) => { + const r = (result || '').toLowerCase(); + switch (r) { + case 'pass': return t('authentication.result.pass'); + case 'fail': return t('authentication.result.fail'); + case 'softfail': return t('authentication.result.softfail'); + case 'neutral': return t('authentication.result.neutral'); + case 'permerror': return t('authentication.result.permerror'); + case 'temperror': return t('authentication.result.temperror'); + case 'none': return t('authentication.result.none'); + default: return result || ''; + } + }; + const replyToDifferent = !!email.replyTo?.length && + (!email.from || email.replyTo[0].email !== email.from[0]?.email); + const deliveryDeltaMs = email.sentAt && email.receivedAt + ? Math.abs(new Date(email.receivedAt).getTime() - new Date(email.sentAt).getTime()) + : 0; + const formatDelta = (diff: number) => { + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + const dayUnit = days > 1 ? t('time.days') : t('time.day'); + const hourUnit = (hours % 24) > 1 ? t('time.hours') : t('time.hour'); + const minuteUnit = (minutes % 60) > 1 ? t('time.minutes') : t('time.minute'); + const minuteUnitSingle = minutes > 1 ? t('time.minutes') : t('time.minute'); + if (days > 0) return `${days} ${dayUnit} ${hours % 24} ${hourUnit}`; + if (hours > 0) return `${hours} ${hourUnit} ${minutes % 60} ${minuteUnit}`; + return `${minutes} ${minuteUnitSingle}`; + }; + const fullDate = (iso?: string) => iso + ? formatDateTime(iso, timeFormat, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', second: '2-digit', timeZoneName: 'short' }) + : '-'; + const auth = email.authenticationResults; + const totalAttachmentSize = effectiveAttachments.reduce((s, a) => s + (a.size || 0), 0); + const topMimeType = email.bodyStructure?.type; + const SectionHeader = ({ children }: { children: React.ReactNode }) => ( +
+ {children} +
+ ); + const Row = ({ label, children, mono }: { label: string; children: React.ReactNode; mono?: boolean }) => ( + <> +
{label}
+
{children}
+ + ); + const AuthChip = ({ name, result, extra, tooltip }: { name: string; result?: string; extra?: React.ReactNode; tooltip?: string }) => { + if (!result) return null; + const status = getSecurityStatus(result); + const Icon = status.icon === 'check' ? Check + : status.icon === 'x' ? X + : status.icon === 'alert' ? AlertTriangle + : Minus; + return ( + + + {name} + + {translateAuthResult(result)} + + {extra && ( + <> + · + {extra} + + )} + + ); + }; + + const hasIdentifiers = !!(email.messageId || email.inReplyTo?.length || email.references?.length || email.threadId); + const hasListInfo = !!(listHeaders?.listId || listHeaders?.listUnsubscribe || listHeaders?.listHelp || listHeaders?.listPost); + const hasAuthSection = !!(auth?.spf || auth?.dkim || auth?.dmarc || auth?.iprev || email.spamScore !== undefined || email.spamLLM); + + return ( +
+
+
+ {t('details.recipients_routing')} +
+ +
+ ` : undefined} + onViewContact={handleViewContactSidebar} + className="text-sm text-left" + /> +
+
+ {replyToDifferent && ( + +
+ {email.replyTo!.map((r, i) => ( + + ))} +
+
+ )} + {email.to && email.to.length > 0 && ( + +
+ {renderClickableRecipients(email.to, currentUserEmail, t, handleViewContactSidebar, 100)} +
+
+ )} + {email.cc && email.cc.length > 0 && ( + +
+ {renderClickableRecipients(email.cc, currentUserEmail, t, handleViewContactSidebar, 100)} +
+
+ )} + {email.bcc && email.bcc.length > 0 && ( + +
+ {renderClickableRecipients(email.bcc, currentUserEmail, t, handleViewContactSidebar, 100)} +
+
+ )} + {email.sentAt && ( + {fullDate(email.sentAt)} + )} + + {fullDate(email.receivedAt)} + {deliveryDeltaMs > 60000 && ( + · {formatDelta(deliveryDeltaMs)} {t('details.delivery_time').toLowerCase()} + )} + +
+
+ + {hasAuthSection && ( +
+ {t('details.authentication_security')} +
+ {auth?.spf && ( + + )} + {auth?.dkim && ( + + )} + {auth?.dmarc && ( + + )} + {auth?.iprev && ( + + )} + {email.spamScore !== undefined && ( + 5 ? "bg-red-500/[0.07] border-red-500/30" : + email.spamScore > 2 ? "bg-amber-500/[0.07] border-amber-500/30" : + "bg-green-500/[0.07] border-green-500/30", + )}> + 5 ? "text-red-700 dark:text-red-400" : + email.spamScore > 2 ? "text-amber-700 dark:text-amber-400" : + "text-green-700 dark:text-green-400", + )} /> + {t('authentication.spam_score')} + 5 ? "text-red-700 dark:text-red-400" : + email.spamScore > 2 ? "text-amber-700 dark:text-amber-400" : + "text-green-700 dark:text-green-400", + )}> + {email.spamScore.toFixed(1)} + + {email.spamStatus && ( + <> + · + {email.spamStatus} + + )} + + )} +
+ {email.spamLLM && ( +
+ {email.spamLLM.verdict === 'LEGITIMATE' ? : + email.spamLLM.verdict === 'SPAM' ? : + } +
+ + {email.spamLLM.verdict} + + · {email.spamLLM.explanation} +
+
+ )} +
+ )} + + {hasIdentifiers && ( +
+ {t('details.identifiers_threading')} +
+ {email.messageId && ( + {email.messageId} + )} + {email.inReplyTo && email.inReplyTo.length > 0 && ( + +
+ {email.inReplyTo.map((id, i) =>
{id}
)} +
+
+ )} + {email.references && email.references.length > 0 && ( + +
+ + + {t(email.references.length === 1 ? 'previous_messages' : 'previous_messages_plural', { count: email.references.length })} + +
+ {email.references.map((id, i) =>
{id}
)} +
+
+
+ )} + {email.threadId && ( + {email.threadId} + )} +
+
+ )} + +
+ {t('details.message_properties')} +
+ {email.subject !== undefined && ( + {email.subject || {t('details.no_subject')}} + )} + + {formatFileSize(email.size)} + {topMimeType && ( + · {topMimeType} + )} + + {effectiveAttachments.length > 0 && ( + + {t('details.attachments_summary', { + count: effectiveAttachments.length, + size: formatFileSize(totalAttachmentSize), + })} + + )} + {email.accountLabel && ( + {email.accountLabel} + )} +
+
+ + {hasListInfo && ( +
+ {t('details.mailing_list')} +
+ {listHeaders?.listId && ( + {listHeaders.listId} + )} + {listHeaders?.listUnsubscribe?.preferred && ( + + + {listHeaders.listUnsubscribe.preferred === 'http' + ? listHeaders.listUnsubscribe.http + : listHeaders.listUnsubscribe.mailto} + + + )} + {listHeaders?.listHelp && ( + {listHeaders.listHelp} + )} + {listHeaders?.listPost && ( + {listHeaders.listPost} + )} +
+
+ )} +
+
+ ); + })()} + {/* S/MIME Status Banner */} {smimeStatus && (