ultisuite-client/lib/thread-compose-preset.ts
2026-05-16 20:30:50 +02:00

268 lines
8.6 KiB
TypeScript

import type { Email, ConversationMessage } from "@/lib/email-data"
import type {
Contact,
ComposeOpenPreset,
ThreadComposeKind,
} from "@/lib/compose-context"
import { DEFAULT_IDENTITIES, SIGNATURES } from "@/lib/compose-context"
import { formatMailDetailDate } from "@/lib/mail-date"
import { cleanSenderName } from "@/lib/sender-display"
function appendDefaultSignature(html: string): string {
const identity = DEFAULT_IDENTITIES[0]
const sigId = identity.defaultSignatureId
const sig = sigId ? SIGNATURES.find((s) => s.id === sigId) : null
if (!sig) return html
return `${html}<div id="ultimail-signature"><p>--</p>${sig.html}</div>`
}
/** Signature juste après la zone de saisie, avant le bloc « message transféré ». */
function insertSignatureBeforeForwardedBlock(html: string): string {
const identity = DEFAULT_IDENTITIES[0]
const sigId = identity.defaultSignatureId
const sig = sigId ? SIGNATURES.find((s) => s.id === sigId) : null
if (!sig) return html
const sigBlock = `<div id="ultimail-signature"><p>--</p>${sig.html}</div>`
const lead = "<p></p>"
if (html.startsWith(lead)) {
return `${lead}${sigBlock}${html.slice(lead.length)}`
}
return `${lead}${sigBlock}${html}`
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
}
function myEmailSet(): Set<string> {
return new Set(DEFAULT_IDENTITIES.map((i) => i.email.toLowerCase()))
}
function contactKey(email: string): string {
return email.trim().toLowerCase()
}
/** Tous les expéditeurs des messages du fil (hors « moi »), ordre chronologique conservé. */
export function collectThreadParticipants(email: Email): Contact[] {
const my = myEmailSet()
const seen = new Set<string>()
const out: Contact[] = []
const push = (name: string, addrRaw: string) => {
const addr = addrRaw.trim()
if (!addr) return
const k = contactKey(addr)
if (my.has(k) || seen.has(k)) return
seen.add(k)
out.push({
name: cleanSenderName(name) || addr,
email: addr,
})
}
for (const m of email.conversation ?? []) {
push(m.sender, m.senderEmail)
}
const mainAddr =
email.senderEmail ??
`${cleanSenderName(email.sender).toLowerCase().replace(/\s+/g, ".")}@example.com`
push(email.sender, mainAddr)
return out
}
function lastMessage(email: Email): ConversationMessage | null {
const conv = email.conversation ?? []
if (conv.length === 0) return null
return conv[conv.length - 1]!
}
function replySubject(subject: string): string {
const s = subject.trim()
if (/^re\s*:/i.test(s)) return s
return `Re: ${s}`
}
function forwardSubject(subject: string): string {
const s = subject.trim()
if (/^(fwd|fw|tr)\s*:/i.test(s)) return s
return `Fwd: ${s}`
}
function buildReferences(email: Email): string[] {
const ids: string[] = []
for (const m of email.conversation ?? []) {
ids.push(`<thread-msg-${m.id}@local>`)
}
ids.push(`<thread-msg-${email.id}@local>`)
return ids
}
function inReplyToFor(email: Email): string {
const last = lastMessage(email)
if (last) return `<thread-msg-${last.id}@local>`
return `<thread-msg-${email.id}@local>`
}
function formatQuoteDate(
dateIso: string,
senderEmail: string,
senderName: string
): string {
const who =
senderName && senderName !== senderEmail
? `${escapeHtml(senderName)} &lt;${escapeHtml(senderEmail)}&gt;`
: escapeHtml(senderEmail)
return `Le ${escapeHtml(formatMailDetailDate(dateIso))}, ${who} a écrit :`
}
function quotedBlock(html: string): string {
return `<blockquote type="cite" style="margin:0 0 0 0.8ex;border-left:1px solid #ccc;padding-left:1ex;color:#500">${html}</blockquote>`
}
function replyQuotedHtml(email: Email): string {
const last = lastMessage(email)
if (last) {
const line = formatQuoteDate(last.date, last.senderEmail, cleanSenderName(last.sender))
return `<p><br></p><div style="color:#666;font-size:13px">${line}</div>${quotedBlock(last.body)}`
}
const addr =
email.senderEmail ??
`${cleanSenderName(email.sender).toLowerCase().replace(/\s+/g, ".")}@example.com`
const line = formatQuoteDate(email.date, addr, cleanSenderName(email.sender))
const body = email.body ?? `<p style="color:#5f6368">${escapeHtml(email.preview)}</p>`
return `<p><br></p><div style="color:#666;font-size:13px">${line}</div>${quotedBlock(body)}`
}
function forwardParticipantsLine(email: Email): string {
const parts = collectThreadParticipants(email)
if (parts.length === 0) return "To: (inconnu)"
return `To: ${parts.map((p) => `${p.name} <${p.email}>`).join(", ")}`
}
/** Corps HTML du fil (messages précédents + message principal), sans en-tête « Forwarded ». */
function forwardConversationHtml(email: Email): string {
const blocks: string[] = []
for (const m of email.conversation ?? []) {
blocks.push(
`<div style="margin-top:12px;padding-top:12px;border-top:1px solid #e0e0e0">` +
`<div style="color:#666;font-size:12px;margin-bottom:8px"><strong>De :</strong> ${escapeHtml(cleanSenderName(m.sender))} &lt;${escapeHtml(m.senderEmail)}&gt;<br/>` +
`<strong>Date :</strong> ${escapeHtml(formatMailDetailDate(m.date))}</div>${m.body}</div>`
)
}
const mainAddr =
email.senderEmail ??
`${cleanSenderName(email.sender).toLowerCase().replace(/\s+/g, ".")}@example.com`
blocks.push(
`<div style="margin-top:12px;padding-top:12px;border-top:1px solid #e0e0e0">` +
`<div style="color:#666;font-size:12px;margin-bottom:8px"><strong>De :</strong> ${escapeHtml(cleanSenderName(email.sender))} &lt;${escapeHtml(mainAddr)}&gt;<br/>` +
`<strong>Date :</strong> ${escapeHtml(formatMailDetailDate(email.date))}</div>` +
`${email.body ?? `<p style="color:#5f6368">${escapeHtml(email.preview)}</p>`}</div>`
)
return blocks.join("")
}
function forwardBodyHtml(email: Email): string {
const mainAddr =
email.senderEmail ??
`${cleanSenderName(email.sender).toLowerCase().replace(/\s+/g, ".")}@example.com`
const header =
`<p>---------- Forwarded message ---------</p>` +
`<p style="color:#222;font-size:13px;line-height:1.5">` +
`<strong>De :</strong> ${escapeHtml(cleanSenderName(email.sender))} &lt;${escapeHtml(mainAddr)}&gt;<br/>` +
`<strong>Date :</strong> ${escapeHtml(formatMailDetailDate(email.date))}<br/>` +
`<strong>Objet :</strong> ${escapeHtml(email.subject)}<br/>` +
`<strong>${escapeHtml(forwardParticipantsLine(email))}</strong></p>`
return `<p></p>${header}${forwardConversationHtml(email)}`
}
/**
* Preset pour une fenêtre de composition inline (réponse / transfert) rattachée au fil `email`.
*/
export function buildThreadComposePreset(
email: Email,
kind: ThreadComposeKind
): ComposeOpenPreset {
const participants = collectThreadParticipants(email)
const last = lastMessage(email)
const lastContact: Contact | null = last
? { name: cleanSenderName(last.sender), email: last.senderEmail }
: participants.length > 0
? participants[participants.length - 1]!
: null
const threading = {
inReplyTo: inReplyToFor(email),
references: buildReferences(email),
}
if (kind === "forward") {
const baseBody = insertSignatureBeforeForwardedBlock(forwardBodyHtml(email))
return {
to: [],
cc: [],
bcc: [],
subject: forwardSubject(email.subject),
bodyHtml: baseBody,
placement: "inline",
threadEmailId: email.id,
threadKind: "forward",
focusToOnMount: true,
focusBodyOnMount: false,
threading,
}
}
if (kind === "reply") {
const to = lastContact ? [lastContact] : []
return {
to,
cc: [],
bcc: [],
subject: replySubject(email.subject),
bodyHtml: appendDefaultSignature(replyQuotedHtml(email)),
placement: "inline",
threadEmailId: email.id,
threadKind: "reply",
focusToOnMount: false,
focusBodyOnMount: true,
threading,
}
}
/* replyAll */
const to: Contact[] = []
const cc: Contact[] = []
if (lastContact) {
to.push(lastContact)
for (const p of participants) {
if (p.email === lastContact.email) continue
cc.push(p)
}
} else if (participants.length > 0) {
to.push(participants[0]!)
for (let i = 1; i < participants.length; i++) {
cc.push(participants[i]!)
}
}
return {
to,
cc,
bcc: [],
subject: replySubject(email.subject),
bodyHtml: appendDefaultSignature(replyQuotedHtml(email)),
placement: "inline",
threadEmailId: email.id,
threadKind: "replyAll",
focusToOnMount: false,
focusBodyOnMount: true,
threading,
showCc: cc.length > 0,
}
}