// The CRM's InboxAdapter — the SDK rendered over the be-crm data door // (crm.inbox.* + crm.mail.*). Folds mail threads into the unified inbox exactly as the old // inbox-api did; the CRM keeps auth/tenancy server-side. import type { InboxAdapter, InboxItem, InboxState, MailAttachment, MailMessage, MailPerson, } from "@insignia/iios-messaging-ui"; import type { DataDoor } from "./crm-messaging-adapter"; const MAX_ATTACHMENT_BYTES = 26 * 1024 * 1024; // matches IIOS's cap // Some types (notably .md) have no OS-registered MIME, so the browser reports an empty file.type. const EXT_MIME: Record = { md: "text/markdown", markdown: "text/markdown", html: "text/html", htm: "text/html", txt: "text/plain", csv: "text/csv", }; function mimeForFile(file: File): string { if (file.type) return file.type; const ext = file.name.toLowerCase().split(".").pop() ?? ""; return EXT_MIME[ext] ?? "application/octet-stream"; } interface InboxItemDTO { id: string; kind: string; state: InboxState; title: string; summary?: string; priority: string; threadId?: string; createdAt: string; } interface MailThreadDTO { threadId: string; subject: string | null; participants: string[]; unread: number; lastMessage?: string; lastAt?: string } interface MailMessageDTO { interactionId: string; actorId: string | null; kind: string; occurredAt: string; html: string | null; text: string | null; attachment: { contentRef: string; mimeType: string; sizeBytes: number; filename: string | null } | null; } interface DirectoryDTO { id: string; displayName: string; kind: "staff" | "customer" } const escapeHtml = (s: string): string => s.replace(/&/g, "&").replace(//g, ">"); export class CrmInboxAdapter implements InboxAdapter { constructor(private readonly sdk: DataDoor) {} async listInbox(state?: InboxState): Promise { const showMail = !state || state === "OPEN"; const [items, mail] = await Promise.all([ this.sdk.query("crm.inbox.list", state ? { state } : {}), showMail ? this.sdk.query("crm.mail.list", {}) : Promise.resolve([] as MailThreadDTO[]), ]); const mailItems: InboxItem[] = mail.map((t) => ({ id: `mail:${t.threadId}`, kind: "MAIL", state: "OPEN", title: t.subject || "(no subject)", ...(t.lastMessage ? { summary: t.lastMessage } : {}), priority: t.unread > 0 ? "HIGH" : "LOW", threadId: t.threadId, createdAt: t.lastAt ?? "", })); return [...mailItems, ...items].sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? "")); } async transition(id: string, state: InboxState): Promise { await this.sdk.command("crm.inbox.transition", { id, state }); } async mailHistory(threadId: string): Promise { const rows = await this.sdk.query("crm.mail.history", { threadId }); return rows.map((m) => ({ id: m.interactionId, actorId: m.actorId, kind: m.kind, at: m.occurredAt, html: m.html, text: m.text, attachment: m.attachment, })); } async mailReply(threadId: string, content: string, attachment?: MailAttachment): Promise { await this.sdk.command("crm.mail.reply", { threadId, content, ...(attachment ? { attachment: { filename: attachment.filename ?? "attachment", contentRef: attachment.contentRef, mimeType: attachment.mimeType, sizeBytes: attachment.sizeBytes } } : {}), }); } async uploadAttachment(file: File): Promise { if (file.size > MAX_ATTACHMENT_BYTES) throw new Error("File is too large (max 25 MB)."); const mime = mimeForFile(file); const { objectKey, uploadUrl } = await this.sdk.command<{ objectKey: string; uploadUrl: string }>("crm.media.presignUpload", { mime, sizeBytes: file.size }); const res = await fetch(uploadUrl, { method: "PUT", body: file }); if (!res.ok) throw new Error(`Upload failed (${res.status}).`); return { contentRef: objectKey, mimeType: mime, sizeBytes: file.size, filename: file.name }; } async downloadAttachment(attachment: MailAttachment): Promise { const { url } = await this.sdk.command<{ url: string }>("crm.media.presignDownload", { contentRef: attachment.contentRef, ...(attachment.mimeType ? { mime: attachment.mimeType } : {}), }); return url; } async directory(): Promise { const rows = await this.sdk.query("crm.messenger.directory", { kind: "all", limit: 100 }); return rows.map((d) => ({ id: d.id, name: d.displayName, kind: d.kind })); } async composeInternal(recipientUserId: string, subject: string, text: string, attachments?: MailAttachment[]): Promise { await this.sdk.command("crm.mail.internal", { recipientUserId, subject, text, html: `

${escapeHtml(text)}

`, ...attachmentsVar(attachments) }); } async composeExternal(target: string, subject: string, text: string, attachments?: MailAttachment[]): Promise { await this.sdk.command("crm.mail.send", { target, subject, text, html: `

${escapeHtml(text)}

`, ...attachmentsVar(attachments) }); } } function attachmentsVar(attachments?: MailAttachment[]): { attachments?: Array<{ filename: string; contentRef: string; mimeType: string; sizeBytes: number }> } { if (!attachments || attachments.length === 0) return {}; return { attachments: attachments.map((a) => ({ filename: a.filename ?? "attachment", contentRef: a.contentRef, mimeType: a.mimeType, sizeBytes: a.sizeBytes })) }; }