import type { Email, ConversationMessage } from "@/lib/email-data"
import type {
Contact,
ComposeOpenPreset,
ThreadComposeKind,
} from "@/lib/compose-context"
import { readCoarsePointerMatches } from "@/hooks/use-touch-nav"
import { readXsMatches } from "@/hooks/use-xs"
import { getComposeIdentities } from "@/lib/stores/compose-identities-store"
import { buildBodyWithSignature, myEmailsFromIdentities } from "@/lib/compose/identity-map"
import { resolveComposeIdentity } from "@/lib/compose/resolve-compose-identity"
import { formatMailDetailDate } from "@/lib/mail-date"
import { cleanSenderName } from "@/lib/sender-display"
function appendDefaultSignature(html: string): string {
return buildBodyWithSignature(html, resolveComposeIdentity())
}
/** Signature juste après la zone de saisie, avant le bloc « message transféré ». */
function insertSignatureBeforeForwardedBlock(html: string): string {
const identity = resolveComposeIdentity()
const sigHtml = identity.signatureHtml
if (!sigHtml?.trim()) return html
const sigBlock = `
`
const lead = ""
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, """)
}
function myEmailSet(): Set {
return myEmailsFromIdentities(getComposeIdentities())
}
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()
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(``)
}
ids.push(``)
return ids
}
function inReplyToFor(email: Email): string {
const last = lastMessage(email)
if (last) return ``
return ``
}
function formatQuoteDate(
dateIso: string,
senderEmail: string,
senderName: string
): string {
const who =
senderName && senderName !== senderEmail
? `${escapeHtml(senderName)} <${escapeHtml(senderEmail)}>`
: escapeHtml(senderEmail)
return `Le ${escapeHtml(formatMailDetailDate(dateIso))}, ${who} a écrit :`
}
function quotedBlock(html: string): string {
return `${html}
`
}
function replyQuotedHtml(email: Email): string {
const last = lastMessage(email)
if (last) {
const line = formatQuoteDate(last.date, last.senderEmail, cleanSenderName(last.sender))
return `
${line}
${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 ?? `${escapeHtml(email.preview)}
`
return `
${line}
${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(
`` +
`
De : ${escapeHtml(cleanSenderName(m.sender))} <${escapeHtml(m.senderEmail)}>
` +
`Date : ${escapeHtml(formatMailDetailDate(m.date))}
${m.body}
`
)
}
const mainAddr =
email.senderEmail ??
`${cleanSenderName(email.sender).toLowerCase().replace(/\s+/g, ".")}@example.com`
blocks.push(
`` +
`
De : ${escapeHtml(cleanSenderName(email.sender))} <${escapeHtml(mainAddr)}>
` +
`Date : ${escapeHtml(formatMailDetailDate(email.date))}
` +
`${email.body ?? `
${escapeHtml(email.preview)}
`}
`
)
return blocks.join("")
}
function forwardBodyHtml(email: Email): string {
const mainAddr =
email.senderEmail ??
`${cleanSenderName(email.sender).toLowerCase().replace(/\s+/g, ".")}@example.com`
const header =
`---------- Forwarded message ---------
` +
`` +
`De : ${escapeHtml(cleanSenderName(email.sender))} <${escapeHtml(mainAddr)}>
` +
`Date : ${escapeHtml(formatMailDetailDate(email.date))}
` +
`Objet : ${escapeHtml(email.subject)}
` +
`${escapeHtml(forwardParticipantsLine(email))}
`
return `${header}${forwardConversationHtml(email)}`
}
/** Tablette / tactile : composition plein écran (dock maximisé) au lieu d’inline sous le fil. */
export function withTouchFullscreenComposePreset(
preset: ComposeOpenPreset
): ComposeOpenPreset {
if (typeof window === "undefined" || !readCoarsePointerMatches()) {
return preset
}
return {
...preset,
placement: "dock",
maximized: true,
}
}
/**
* 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 defaultIdentity = resolveComposeIdentity()
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 {
from: defaultIdentity,
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 {
from: defaultIdentity,
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 {
from: defaultIdentity,
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,
}
}