diff --git a/package-lock.json b/package-lock.json index eb0d18b..2949949 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1030,7 +1030,7 @@ "node_modules/@insignia/iios-messaging-ui": { "version": "0.1.0", "resolved": "file:../iios/packages/iios-messaging-ui/insignia-iios-messaging-ui-0.1.0.tgz", - "integrity": "sha512-RoXEI/SgzYM2Zj+XvwNkc3rrDQ5kDa/oqsCo2wkZxwRs3bVHJuRoQR+c713wgaWK+MmqIg0cetXQ//rfJvpBbg==", + "integrity": "sha512-qJc7+34VwPcsuaucXcUe7LbW7CAvVcGBUnUR6ktuHHISpGVWzXEni9rjUiBI+Baw3UxvUFbtmIuqT1DTXIeegw==", "peerDependencies": { "@insignia/iios-kernel-client": "*", "react": ">=18" diff --git a/src/lib/crm-inbox-adapter.ts b/src/lib/crm-inbox-adapter.ts index 2bc3041..f5cca15 100644 --- a/src/lib/crm-inbox-adapter.ts +++ b/src/lib/crm-inbox-adapter.ts @@ -6,11 +6,24 @@ 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; } @@ -63,8 +76,21 @@ export class CrmInboxAdapter implements InboxAdapter { })); } - async mailReply(threadId: string, content: string): Promise { - await this.sdk.command("crm.mail.reply", { threadId, content }); + 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 directory(): Promise { @@ -72,11 +98,16 @@ export class CrmInboxAdapter implements InboxAdapter { return rows.map((d) => ({ id: d.id, name: d.displayName, kind: d.kind })); } - async composeInternal(recipientUserId: string, subject: string, text: string): Promise { - await this.sdk.command("crm.mail.internal", { recipientUserId, subject, text, html: `

${escapeHtml(text)}

` }); + 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): Promise { - await this.sdk.command("crm.mail.send", { target, subject, text, html: `

${escapeHtml(text)}

` }); + 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 })) }; +}