/** * Mail plugin API client. * * All requests use the shared `apiClient` (`baseURL: '/api/v1'`) and target the * Mail plugin routes under `/mail/...`. */ import { apiClient, apiDelete, apiGet, apiPatch, apiPost } from './client'; // ─── Types ───────────────────────────────────────────────────────────────── export interface MailAccount { id: string; email: string; display_name: string; imap_host: string; imap_port: number; smtp_host: string; smtp_port: number; is_shared: boolean; is_active: boolean; sent_folder_imap_name?: string | null; drafts_folder_imap_name?: string | null; spam_folder_imap_name?: string | null; trash_folder_imap_name?: string | null; created_at?: string | null; updated_at?: string | null; } export interface MailFolder { id: string; account_id: string; name: string; imap_name?: string; parent_id: string | null; unread_count: number; total_count: number; children?: MailFolder[]; } export interface MailAttachment { id: string; mail_id: string; filename: string; mime_type: string; size_bytes: number; size: number; content_id?: string | null; is_inline: boolean; dms_file_id?: string | null; } export interface Mail { id: string; folder_id: string; account_id: string; from_address: string; from_name: string; to_addresses: string[]; cc_addresses: string[]; bcc_addresses: string[]; subject: string; body_text: string; body_html: string | null; sanitized_html: string | null; date: string; is_seen: boolean; is_flagged: boolean; is_draft: boolean; is_answered: boolean; has_attachments: boolean; attachments?: MailAttachment[]; labels?: MailLabel[]; thread_id?: string | null; in_reply_to?: string | null; message_id?: string | null; } export interface MailListResult { mails: Mail[]; total: number; page: number; page_size: number; } export interface MailTemplate { id: string; name: string; subject: string; body: string; variables: string[]; created_at?: string | null; updated_at?: string | null; } export interface MailSignature { id: string; name: string; body_html: string; is_default: boolean; created_at?: string | null; updated_at?: string | null; } export type RuleConditionType = 'from_contains' | 'to_contains' | 'subject_contains' | 'has_attachment' | 'body_contains'; export type RuleActionType = 'move_to_folder' | 'mark_as_read' | 'mark_as_flagged' | 'forward_to' | 'delete'; export interface RuleCondition { type: RuleConditionType; value: string; } export interface RuleAction { type: RuleActionType; value: string; } export interface MailRule { id: string; name: string; account_id: string; priority: number; is_active: boolean; conditions: RuleCondition[]; actions: RuleAction[]; created_at?: string | null; updated_at?: string | null; } export interface MailLabel { id: string; name: string; color: string; created_at?: string | null; } export interface PgpKey { id: string; key_id: string; fingerprint: string; user_id: string; is_private: boolean; expires_at: string | null; created_at?: string | null; } export interface ContactPgpKey { contact_id: string; contact_name: string; key_id: string; fingerprint: string; added_at: string; } export interface VacationConfig { enabled: boolean; start_date: string | null; end_date: string | null; subject: string; body: string; dedup_days: number; } export interface SharedMailboxUser { user_id: string; user_name: string; permissions: string[]; } export interface DelegateAccess { id: string; delegate_user_id: string; delegate_user_name: string; permissions: string[]; folder_ids: string[]; } export interface SendPermission { id: string; grantee_user_id: string; grantee_user_name: string; from_address: string; } export interface ThreadResult { thread_id: string; subject: string; mails: Mail[]; } // ─── Payloads ────────────────────────────────────────────────────────────── export interface CreateAccountPayload { email: string; display_name: string; imap_host: string; imap_port: number; smtp_host: string; smtp_port: number; username?: string; password: string; is_shared?: boolean; sent_folder_imap_name?: string | null; drafts_folder_imap_name?: string | null; spam_folder_imap_name?: string | null; trash_folder_imap_name?: string | null; } export interface UpdateAccountPayload { display_name?: string; imap_host?: string; imap_port?: number; smtp_host?: string; smtp_port?: number; password?: string; is_active?: boolean; sent_folder_imap_name?: string | null; drafts_folder_imap_name?: string | null; spam_folder_imap_name?: string | null; trash_folder_imap_name?: string | null; } export interface CreateFolderPayload { account_id: string; name: string; parent_id?: string | null; } export interface UpdateFolderPayload { name?: string; parent_id?: string | null; } export interface SendMailPayload { account_id: string; to: string[]; cc?: string[]; bcc?: string[]; subject: string; body: string; is_html: boolean; in_reply_to?: string | null; attachments?: string[]; signature_id?: string | null; } export interface ReplyPayload { account_id: string; body: string; is_html: boolean; to?: string[]; cc?: string[]; signature_id?: string | null; } export interface ForwardPayload { account_id: string; to: string[]; body?: string; is_html: boolean; signature_id?: string | null; } export interface FlagUpdatePayload { is_seen?: boolean; is_flagged?: boolean; is_draft?: boolean; is_answered?: boolean; is_forwarded?: boolean; } export interface LinkMailPayload { contact_id?: string | null; company_id?: string | null; } export interface CreateEventFromMailPayload { title: string; start: string; end: string; description?: string; location?: string; calendar_id?: string; } export interface AssignLabelPayload { label_id: string; } export interface CreateTemplatePayload { name: string; subject: string; body: string; variables?: string[]; } export interface SubstituteTemplatePayload { template_id: string; variables: Record; } export interface SubstituteResult { subject: string; body: string; } export interface CreateSignaturePayload { name: string; body_html: string; is_default?: boolean; } export interface CreateRulePayload { name: string; account_id: string; priority: number; is_active: boolean; conditions: RuleCondition[]; actions: RuleAction[]; } export interface CreateLabelPayload { name: string; color: string; } export interface VacationPayload { enabled: boolean; start_date: string | null; end_date: string | null; subject: string; body: string; dedup_days?: number; } export interface ImportPgpKeyPayload { private_key: string; passphrase?: string; } export interface StoreContactPgpKeyPayload { public_key: string; } export interface AssignSharedMailboxUsersPayload { user_ids: string[]; permissions: string[]; } export interface CreateDelegatePayload { delegate_user_id: string; permissions: string[]; folder_ids: string[]; } export interface GrantSendPermissionPayload { grantee_user_id: string; from_address: string; } // ─── Accounts ────────────────────────────────────────────────────────────── export function fetchAccounts(): Promise { return apiGet('/mail/accounts'); } export function createAccount(payload: CreateAccountPayload): Promise { return apiPost('/mail/accounts', payload); } export function updateAccount(accountId: string, payload: UpdateAccountPayload): Promise { return apiPatch(`/mail/accounts/${accountId}`, payload); } export function deleteAccount(accountId: string): Promise { return apiDelete(`/mail/accounts/${accountId}`); } export function fetchSharedAccounts(): Promise { return apiGet('/mail/accounts/shared'); } export function assignSharedMailboxUsers(accountId: string, payload: AssignSharedMailboxUsersPayload): Promise { return apiPost(`/mail/accounts/${accountId}/users`, payload); } export function createDelegate(accountId: string, payload: CreateDelegatePayload): Promise { return apiPost(`/mail/accounts/${accountId}/delegates`, payload); } export function grantSendPermission(accountId: string, payload: GrantSendPermissionPayload): Promise { return apiPost(`/mail/accounts/${accountId}/send-permissions`, payload); } export function testConnection(accountId: string): Promise<{ success: boolean; message: string }> { return apiPost<{ success: boolean; message: string }>(`/mail/accounts/${accountId}/test-connection`); } export function triggerSync(accountId: string): Promise<{ synced: number; error?: string }> { return apiPost<{ synced: number; error?: string }>(`/mail/accounts/${accountId}/sync`); } /** * Decode MIME encoded-words (=?UTF-8?Q?...?=) to readable text. * Used for mail subjects/from names that were stored before backend decoding was added. */ export function decodeMimeHeader(value: string | undefined | null): string { if (!value) return ''; // Check if value contains MIME encoded-words if (!value.includes('=?')) return value; try { // Decode all encoded-words in the string return value.replace(/=\?([^?]+)\?([BQ])\?([^?]*)\?=/gi, (_match, _charset, encoding, encoded) => { try { if (encoding.toUpperCase() === 'B') { // Base64 decode const decoded = atob(encoded); return decodeURIComponent(escape(decoded)); } else { // Q encoding: replace _ with space, then decode =XX hex sequences const qDecoded = encoded.replace(/_/g, ' ').replace(/=([0-9A-F]{2})/gi, (_m: string, hex: string) => { return String.fromCharCode(parseInt(hex, 16)); }); return decodeURIComponent(escape(qDecoded)); } } catch { return _match; } }); } catch { return value; } } /** * Replace placeholder variables in signature HTML with actual user/tenant data. * Variables: {{user.name}}, {{user.first_name}}, {{user.last_name}}, * {{user.email}}, {{user.role}}, {{tenant.name}}, {{date}} */ export function replaceSignatureVariables( html: string, user: { name?: string; email?: string; role?: string; first_name?: string; last_name?: string }, tenant?: { name?: string }, ): string { const now = new Date(); const dateStr = now.toLocaleDateString([], { year: 'numeric', month: '2-digit', day: '2-digit' }); const firstName = user.first_name || (user.name ? user.name.split(' ')[0] : ''); const lastName = user.last_name || (user.name ? user.name.split(' ').slice(1).join(' ') : ''); return html .replace(/\{\{user\.name\}\}/gi, user.name || '') .replace(/\{\{user\.first_name\}\}/gi, firstName) .replace(/\{\{user\.last_name\}\}/gi, lastName) .replace(/\{\{user\.email\}\}/gi, user.email || '') .replace(/\{\{user\.role\}\}/gi, user.role || '') .replace(/\{\{tenant\.name\}\}/gi, tenant?.name || '') .replace(/\{\{date\}\}/gi, dateStr); } // ─── Folders ──────────────────────────────────────────────────────────────── export function fetchFolders(accountId: string): Promise { const qs = new URLSearchParams({ account_id: accountId }).toString(); return apiGet(`/mail/folders?${qs}`); } export function createFolder(payload: CreateFolderPayload): Promise { return apiPost('/mail/folders', payload); } export function updateFolder(folderId: string, payload: UpdateFolderPayload): Promise { return apiPatch(`/mail/folders/${folderId}`, payload); } export function deleteFolder(folderId: string): Promise { return apiDelete(`/mail/folders/${folderId}`); } export function emptyFolder(folderId: string): Promise<{ emptied_count: number }> { return apiPost<{ emptied_count: number }>(`/mail/folders/${folderId}/empty`, {}); } // ─── Mails ────────────────────────────────────────────────────────────────── export function fetchMails(folderId: string, page: number, sortBy?: string, sortOrder?: string): Promise { const params = new URLSearchParams({ folder_id: folderId, page: String(page) }); if (sortBy) params.set('sort_by', sortBy); if (sortOrder) params.set('sort_order', sortOrder); return apiGet(`/mail?${params.toString()}`); } export function getMail(mailId: string): Promise { return apiGet(`/mail/${mailId}`); } export function sendMail(payload: SendMailPayload): Promise<{ id: string; success: boolean }> { return apiPost<{ id: string; success: boolean }>('/mail/send', payload); } export function replyMail(mailId: string, payload: ReplyPayload): Promise<{ id: string; success: boolean }> { return apiPost<{ id: string; success: boolean }>(`/mail/${mailId}/reply`, payload); } export function forwardMail(mailId: string, payload: ForwardPayload): Promise<{ id: string; success: boolean }> { return apiPost<{ id: string; success: boolean }>(`/mail/${mailId}/forward`, payload); } export function updateFlags(mailId: string, payload: FlagUpdatePayload): Promise { return apiPatch(`/mail/${mailId}/flags`, payload); } export function linkMail(mailId: string, payload: LinkMailPayload): Promise { return apiPost(`/mail/${mailId}/link`, payload); } export function createEventFromMail(mailId: string, payload: CreateEventFromMailPayload): Promise<{ id: string; success: boolean }> { return apiPost<{ id: string; success: boolean }>(`/mail/${mailId}/create-event`, payload); } export function assignLabel(mailId: string, payload: AssignLabelPayload): Promise { return apiPost(`/mail/${mailId}/labels`, payload); } // ─── Search & Threads ────────────────────────────────────────────────────── export function searchMails(query: string): Promise { const qs = new URLSearchParams({ q: query }).toString(); return apiGet(`/mail/search?${qs}`); } export function fetchThreads(accountId: string): Promise { const qs = new URLSearchParams({ account_id: accountId }).toString(); return apiGet(`/mail/threads?${qs}`); } // ─── Attachments ───────────────────────────────────────────────────────────── export function getAttachmentDownloadUrl(mailId: string, attachmentId: string): string { return `/api/v1/mail/${mailId}/attachments/${attachmentId}`; } export function downloadAttachment(mailId: string, attachmentId: string): Promise { return apiClient.get(`/mail/${mailId}/attachments/${attachmentId}`, { responseType: 'blob', }).then((res) => res.data); } export interface UploadedAttachment { id: string; filename: string; mime_type: string; size_bytes: number; path: string; } export function uploadAttachment(file: File): Promise { const formData = new FormData(); formData.append('file', file); return apiClient.post('/mail/upload-attachment', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }).then((res) => res.data); } // ─── Templates ────────────────────────────────────────────────────────────── export function createTemplate(payload: CreateTemplatePayload): Promise { return apiPost('/mail/templates', payload); } export function fetchTemplates(): Promise { return apiGet('/mail/templates'); } export function substituteTemplate(payload: SubstituteTemplatePayload): Promise { return apiPost('/mail/templates/substitute', payload); } // ─── Signatures ────────────────────────────────────────────────────────────── export function createSignature(payload: CreateSignaturePayload): Promise { return apiPost('/mail/signatures', payload); } export function fetchSignatures(): Promise { return apiGet('/mail/signatures'); } export function deleteSignature(signatureId: string): Promise { return apiDelete(`/mail/signatures/${signatureId}`); } export function updateSignature(signatureId: string, payload: Partial): Promise { return apiPatch(`/mail/signatures/${signatureId}`, payload); } // ─── Rules ─────────────────────────────────────────────────────────────────── export function createRule(payload: CreateRulePayload): Promise { return apiPost('/mail/rules', payload); } export function fetchRules(): Promise { return apiGet('/mail/rules'); } export function deleteRule(ruleId: string): Promise { return apiDelete(`/mail/rules/${ruleId}`); } // ─── Vacation ───────────────────────────────────────────────────────────────── export function configureVacation(payload: VacationPayload): Promise<{ success: boolean }> { return apiPost<{ success: boolean }>('/mail/vacation', payload); } export function testVacationDedup(): Promise<{ dedup_count: number }> { return apiPost<{ dedup_count: number }>('/mail/vacation/test-dedup'); } // ─── PGP ────────────────────────────────────────────────────────────────────── export function importPgpKey(payload: ImportPgpKeyPayload): Promise { return apiPost('/mail/pgp/keys', payload); } export function fetchPgpKeys(): Promise { return apiGet('/mail/pgp/keys'); } export function encryptMessage(payload: { recipient_key_id: string; message: string }): Promise<{ encrypted: string }> { return apiPost<{ encrypted: string }>('/mail/pgp/encrypt', payload); } export function storeContactPgpKey(contactId: string, payload: StoreContactPgpKeyPayload): Promise { return apiPost(`/mail/contacts/${contactId}/pgp-key`, payload); } export function fetchContactPgpKeys(): Promise { return apiGet('/mail/contacts/pgp-keys'); } // ─── Labels ─────────────────────────────────────────────────────────────────── export function createLabel(payload: CreateLabelPayload): Promise { return apiPost('/mail/labels', payload); } export function fetchLabels(): Promise { return apiGet('/mail/labels'); } export function deleteLabel(labelId: string): Promise { return apiDelete(`/mail/labels/${labelId}`); } // ─── Delete / Move / Drafts ────────────────────────────────────────────────── export function deleteMail(mailId: string): Promise { return apiDelete(`/mail/${mailId}`); } export function moveMail(mailId: string, targetFolderId: string): Promise { return apiPost(`/mail/${mailId}/move`, { target_folder_id: targetFolderId }); } export interface MailDraftPayload { account_id: string; to: string[]; cc: string[]; bcc: string[]; subject: string; body_text: string; body_html: string; } export function saveDraft(payload: MailDraftPayload): Promise { return apiPost('/mail/drafts', payload); } export function updateDraft(mailId: string, payload: MailDraftPayload): Promise { return apiPatch(`/mail/drafts/${mailId}`, payload); }