263 lines
8.4 KiB
TypeScript
263 lines
8.4 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 { 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, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
}
|
|
|
|
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(date: string, senderEmail: string, senderName: string): string {
|
|
const who =
|
|
senderName && senderName !== senderEmail
|
|
? `${escapeHtml(senderName)} <${escapeHtml(senderEmail)}>`
|
|
: escapeHtml(senderEmail)
|
|
return `Le ${escapeHtml(date)}, ${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))} <${escapeHtml(m.senderEmail)}><br/>` +
|
|
`<strong>Date :</strong> ${escapeHtml(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))} <${escapeHtml(mainAddr)}><br/>` +
|
|
`<strong>Date :</strong> ${escapeHtml(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))} <${escapeHtml(mainAddr)}><br/>` +
|
|
`<strong>Date :</strong> ${escapeHtml(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,
|
|
}
|
|
}
|