feat(inbox): wire mail attachments through CrmInboxAdapter

uploadAttachment presigns via crm.media.presignUpload + PUTs bytes to IIOS
storage; mailReply/composeInternal/composeExternal forward attachment refs
to crm.mail.reply/internal/send (server already supported them).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 01:05:14 +05:30
parent facc50e82f
commit 462eb7f036
2 changed files with 38 additions and 7 deletions
+1 -1
View File
@@ -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"
+37 -6
View File
@@ -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<string, string> = {
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<void> {
await this.sdk.command("crm.mail.reply", { threadId, content });
async mailReply(threadId: string, content: string, attachment?: MailAttachment): Promise<void> {
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<MailAttachment> {
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<MailPerson[]> {
@@ -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<void> {
await this.sdk.command("crm.mail.internal", { recipientUserId, subject, text, html: `<p>${escapeHtml(text)}</p>` });
async composeInternal(recipientUserId: string, subject: string, text: string, attachments?: MailAttachment[]): Promise<void> {
await this.sdk.command("crm.mail.internal", { recipientUserId, subject, text, html: `<p>${escapeHtml(text)}</p>`, ...attachmentsVar(attachments) });
}
async composeExternal(target: string, subject: string, text: string): Promise<void> {
await this.sdk.command("crm.mail.send", { target, subject, text, html: `<p>${escapeHtml(text)}</p>` });
async composeExternal(target: string, subject: string, text: string, attachments?: MailAttachment[]): Promise<void> {
await this.sdk.command("crm.mail.send", { target, subject, text, html: `<p>${escapeHtml(text)}</p>`, ...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 })) };
}