ultisuite-client/lib/mail-message-participants.ts
2026-05-25 13:52:40 +02:00

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"
}