216 lines
6.4 KiB
TypeScript
216 lines
6.4 KiB
TypeScript
import type { Recipient } from "@/lib/api/types"
|
|
import { cleanSenderName } from "@/lib/sender-display"
|
|
|
|
export function normalizeMailAddress(addr: string): string {
|
|
return addr.trim().toLowerCase().replace(/^<|>$/g, "")
|
|
}
|
|
|
|
function parseAngleAddr(value: string): { name: string; email: string } {
|
|
const trimmed = value.trim()
|
|
const match = trimmed.match(/^(.*?)\s*<([^>]+)>$/)
|
|
if (match) {
|
|
return {
|
|
name: match[1]?.trim() ?? "",
|
|
email: match[2]?.trim() ?? "",
|
|
}
|
|
}
|
|
if (trimmed.includes("@") && !trimmed.includes(" ")) {
|
|
return { name: "", email: trimmed.replace(/^<|>$/g, "") }
|
|
}
|
|
return { name: trimmed, email: "" }
|
|
}
|
|
|
|
/** Normalizes API from[] when IMAP/sync left name or address empty or combined. */
|
|
export function resolveMessageFrom(
|
|
from: Recipient[] | undefined,
|
|
options?: {
|
|
selfEmails?: Iterable<string>
|
|
selfDisplayName?: string
|
|
}
|
|
): { name: string; email: string } {
|
|
const first = from?.[0]
|
|
let name = first?.name?.trim() ?? ""
|
|
let email = first?.address?.trim() ?? ""
|
|
|
|
if (!email && name) {
|
|
const parsed = parseAngleAddr(name)
|
|
name = parsed.name
|
|
email = parsed.email
|
|
}
|
|
if (!name && email) {
|
|
const parsed = parseAngleAddr(email)
|
|
if (parsed.email) {
|
|
email = parsed.email
|
|
name = parsed.name
|
|
}
|
|
}
|
|
|
|
email = email.replace(/^<|>$/g, "")
|
|
const selfSet = new Set(
|
|
options?.selfEmails
|
|
? [...options.selfEmails].map(normalizeMailAddress).filter(Boolean)
|
|
: []
|
|
)
|
|
|
|
if (email && selfSet.has(normalizeMailAddress(email)) && options?.selfDisplayName) {
|
|
name = options.selfDisplayName
|
|
} else if (email && !name) {
|
|
const local = email.split("@")[0] ?? email
|
|
name = local.replace(/[._-]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
|
|
}
|
|
|
|
return {
|
|
name: cleanSenderName(name || email),
|
|
email,
|
|
}
|
|
}
|
|
|
|
export function collectSelfMailEmails(
|
|
accounts: { email: string }[] | undefined,
|
|
identities: { email: string }[] | undefined,
|
|
platformEmail?: string | null
|
|
): string[] {
|
|
const out = new Set<string>()
|
|
for (const a of accounts ?? []) {
|
|
if (a.email) out.add(normalizeMailAddress(a.email))
|
|
}
|
|
for (const i of identities ?? []) {
|
|
if (i.email) out.add(normalizeMailAddress(i.email))
|
|
}
|
|
if (platformEmail) out.add(normalizeMailAddress(platformEmail))
|
|
return [...out]
|
|
}
|
|
|
|
export function isMessageFromSelf(
|
|
from: Recipient[] | undefined,
|
|
selfEmails: Iterable<string>
|
|
): boolean {
|
|
const { email } = resolveMessageFrom(from)
|
|
if (!email) return false
|
|
const norm = normalizeMailAddress(email)
|
|
for (const s of selfEmails) {
|
|
if (norm === normalizeMailAddress(s)) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
function recipientDisplayName(r: Recipient): string {
|
|
const name = r.name?.trim()
|
|
if (name) return cleanSenderName(name)
|
|
const addr = r.address?.trim().replace(/^<|>$/g, "")
|
|
if (!addr) return ""
|
|
const local = addr.split("@")[0] ?? addr
|
|
return cleanSenderName(
|
|
local.replace(/[._-]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
|
|
)
|
|
}
|
|
|
|
const RECIPIENT_SUMMARY_NAMES = 3
|
|
|
|
function selfAddressSet(selfEmails: Iterable<string>): Set<string> {
|
|
return new Set([...selfEmails].map(normalizeMailAddress).filter(Boolean))
|
|
}
|
|
|
|
/** To/Cc rows that match one of the user's mail addresses. */
|
|
export function selfRecipientsInMessage(
|
|
to: Recipient[] | undefined,
|
|
cc: Recipient[] | undefined,
|
|
selfEmails: Iterable<string>
|
|
): Recipient[] {
|
|
const self = selfAddressSet(selfEmails)
|
|
const seen = new Set<string>()
|
|
const out: Recipient[] = []
|
|
for (const r of [...(to ?? []), ...(cc ?? [])]) {
|
|
if (!r.address) continue
|
|
const norm = normalizeMailAddress(r.address)
|
|
if (!self.has(norm) || seen.has(norm)) continue
|
|
seen.add(norm)
|
|
out.push(r)
|
|
}
|
|
return out
|
|
}
|
|
|
|
/** External recipients (excludes self addresses). */
|
|
export function externalRecipients(
|
|
to: Recipient[] | undefined,
|
|
cc: Recipient[] | undefined,
|
|
selfEmails: Iterable<string>
|
|
): Recipient[] {
|
|
const self = selfAddressSet(selfEmails)
|
|
return [...(to ?? []), ...(cc ?? [])].filter(
|
|
(r) => r.address && !self.has(normalizeMailAddress(r.address))
|
|
)
|
|
}
|
|
|
|
/** "moi" with the mailbox that received/sent (Gmail-style precision). */
|
|
export function formatSelfMoiLabel(r: Recipient): string {
|
|
const email = r.address?.trim().replace(/^<|>$/g, "")
|
|
return email ? `moi <${email}>` : "moi"
|
|
}
|
|
|
|
/** Gmail-style one-line: à moi | à Name | à A, B, C + N autres */
|
|
export function formatMessageToSummary(
|
|
to: Recipient[] | undefined,
|
|
cc: Recipient[] | undefined,
|
|
selfEmails: Iterable<string>
|
|
): string {
|
|
const recipients = externalRecipients(to, cc, selfEmails)
|
|
const selfOnly = selfRecipientsInMessage(to, cc, selfEmails)
|
|
|
|
if (recipients.length === 0) {
|
|
if (selfOnly.length === 1) {
|
|
return `à ${formatSelfMoiLabel(selfOnly[0]!)}`
|
|
}
|
|
if (selfOnly.length > 1) {
|
|
return `à ${selfOnly.map(formatSelfMoiLabel).join(", ")}`
|
|
}
|
|
return "à moi"
|
|
}
|
|
if (recipients.length === 1) {
|
|
const label = recipientDisplayName(recipients[0]!)
|
|
return label ? `à ${label}` : "à moi"
|
|
}
|
|
if (recipients.length <= RECIPIENT_SUMMARY_NAMES) {
|
|
return `à ${recipients.map((r) => recipientDisplayName(r)).join(", ")}`
|
|
}
|
|
const shown = recipients
|
|
.slice(0, RECIPIENT_SUMMARY_NAMES)
|
|
.map((r) => recipientDisplayName(r))
|
|
.join(", ")
|
|
const rest = recipients.length - RECIPIENT_SUMMARY_NAMES
|
|
return `à ${shown} + ${rest} autre${rest > 1 ? "s" : ""}`
|
|
}
|
|
|
|
export function formatRecipientMailbox(r: Recipient): string {
|
|
const { name, email } = resolveMessageFrom([r])
|
|
if (name && email) return `${name} <${email}>`
|
|
return email || name
|
|
}
|
|
|
|
/** All To/Cc including self, for the details popover. */
|
|
export function formatAllRecipientsLine(
|
|
to: Recipient[] | undefined,
|
|
cc: Recipient[] | undefined,
|
|
selfEmails: Iterable<string>
|
|
): string {
|
|
const self = new Set([...selfEmails].map(normalizeMailAddress).filter(Boolean))
|
|
const parts: string[] = []
|
|
for (const r of to ?? []) {
|
|
if (!r.address) continue
|
|
if (self.has(normalizeMailAddress(r.address))) {
|
|
parts.push(formatSelfMoiLabel(r))
|
|
} else {
|
|
parts.push(formatRecipientMailbox(r))
|
|
}
|
|
}
|
|
for (const r of cc ?? []) {
|
|
if (!r.address) continue
|
|
const box = formatRecipientMailbox(r)
|
|
parts.push(parts.length ? `Cc: ${box}` : box)
|
|
}
|
|
if (parts.length > 0) return parts.join(", ")
|
|
const selfOnly = selfRecipientsInMessage(to, cc, selfEmails)
|
|
if (selfOnly[0]) return formatSelfMoiLabel(selfOnly[0])
|
|
return "moi"
|
|
}
|