/** * Which timestamp to show for a message. * * JMAP `receivedAt` is the server's internal date - the moment the message * landed in the store. That is fine for freshly delivered mail, but it is not * the mail's own date: an import, migration or backup restore that does not * preserve internal dates stamps every message with the import time, so a whole * mailbox suddenly "arrived" today (#891). `sentAt` is the RFC 5322 `Date` * header, which survives such moves and is what Thunderbird, Outlook and Apple * Mail display. * * Prefer `sentAt`; fall back to `receivedAt` when the header is missing, * unparsable, or implausibly far in the future relative to the receive time * (spam forges future dates to float to the top of date-sorted lists; genuine * clock skew is minutes, not days). * * List *ordering* is unaffected - queries still sort by `receivedAt`, which is * monotonic and cannot be forged by the sender. */ /** How far ahead of `receivedAt` a `Date` header may be before it is ignored. */ export const MAX_FUTURE_SENT_AT_MS = 24 * 60 * 60 * 1000; interface DatedEmail { sentAt?: string | null; receivedAt?: string | null; } /** `string` when the input is guaranteed a `receivedAt` (a full `Email`), else possibly undefined. */ type DisplayDate = T extends { receivedAt: string } ? string : string | undefined; export function emailDisplayDate(email: T): DisplayDate { const { sentAt, receivedAt } = email; const fallback = (receivedAt ?? undefined) as DisplayDate; if (!sentAt) return fallback; const sent = Date.parse(sentAt); if (Number.isNaN(sent)) return fallback; if (receivedAt) { const received = Date.parse(receivedAt); if (!Number.isNaN(received) && sent - received > MAX_FUTURE_SENT_AT_MS) return fallback; } return sentAt as DisplayDate; }