import { jmapClient } from './jmap-client'; import { CAPABILITIES } from './types'; import type { Email, EmailAddress, Mailbox, Thread } from './types'; import { toWildcardQuery } from '../lib/search-utils'; const EMAIL_LIST_PROPERTIES = [ 'id', 'threadId', 'mailboxIds', 'keywords', 'size', 'receivedAt', 'from', 'to', 'cc', 'subject', 'preview', 'hasAttachment', ]; const EMAIL_FULL_PROPERTIES = [ ...EMAIL_LIST_PROPERTIES, 'bodyStructure', 'textBody', 'htmlBody', 'bodyValues', 'attachments', 'blobId', 'bcc', 'replyTo', 'sentAt', ]; export async function getMailboxes(): Promise { const accountId = jmapClient.accountId; const res = await jmapClient.request( [['Mailbox/get', { accountId }, '0']], ); return res.methodResponses[0][1].list; } export async function queryEmails( mailboxId: string, options?: { position?: number; limit?: number; sort?: Array<{ property: string; isAscending: boolean }>; filter?: Record; }, ): Promise<{ ids: string[]; total: number }> { const accountId = jmapClient.accountId; const res = await jmapClient.request([ ['Email/query', { accountId, filter: { inMailbox: mailboxId, ...options?.filter }, sort: options?.sort ?? [{ property: 'receivedAt', isAscending: false }], position: options?.position ?? 0, limit: options?.limit ?? 50, calculateTotal: true, }, '0'], ]); return { ids: res.methodResponses[0][1].ids, total: res.methodResponses[0][1].total, }; } export async function getEmails(ids: string[]): Promise { const accountId = jmapClient.accountId; const res = await jmapClient.request([ ['Email/get', { accountId, ids, properties: EMAIL_LIST_PROPERTIES }, '0'], ]); return res.methodResponses[0][1].list; } export async function getFullEmail(id: string): Promise { const accountId = jmapClient.accountId; const res = await jmapClient.request([ ['Email/get', { accountId, ids: [id], properties: EMAIL_FULL_PROPERTIES, fetchHTMLBodyValues: true, fetchTextBodyValues: true, fetchAllBodyValues: true, maxBodyValueBytes: 512000, }, '0'], ]); const email = res.methodResponses[0][1].list[0]; if (!email) throw new Error(`Email ${id} not found`); return email; } export async function getThread(threadId: string): Promise { const accountId = jmapClient.accountId; const res = await jmapClient.request( [['Thread/get', { accountId, ids: [threadId] }, '0']], ); return res.methodResponses[0][1].list[0]; } export async function setEmailKeywords( emailId: string, keywords: Record, ): Promise { const accountId = jmapClient.accountId; await jmapClient.request([ ['Email/set', { accountId, update: { [emailId]: { keywords } } }, '0'], ]); } export async function moveEmail( emailId: string, fromMailboxId: string, toMailboxId: string, ): Promise { const accountId = jmapClient.accountId; await jmapClient.request([ ['Email/set', { accountId, update: { [emailId]: { [`mailboxIds/${fromMailboxId}`]: null, [`mailboxIds/${toMailboxId}`]: true, }, }, }, '0'], ]); } export async function deleteEmail( emailId: string, trashMailboxId: string, currentMailboxId: string, ): Promise { if (currentMailboxId === trashMailboxId) { const accountId = jmapClient.accountId; await jmapClient.request([ ['Email/set', { accountId, destroy: [emailId] }, '0'], ]); } else { await moveEmail(emailId, currentMailboxId, trashMailboxId); } } export async function searchEmails( query: string, mailboxId?: string, limit = 30, ): Promise { const accountId = jmapClient.accountId; const filter: Record = { text: toWildcardQuery(query) }; if (mailboxId) filter.inMailbox = mailboxId; const res = await jmapClient.request([ ['Email/query', { accountId, filter, sort: [{ property: 'receivedAt', isAscending: false }], limit, }, '0'], ]); return res.methodResponses[0][1].ids; } // Cross-mailbox query with an arbitrary JMAP filter (used by contact activity // to search "from OR to " without picking a specific mailbox). export async function queryEmailsByFilter( filter: Record, limit = 5, ): Promise { const accountId = jmapClient.accountId; const res = await jmapClient.request([ ['Email/query', { accountId, filter, sort: [{ property: 'receivedAt', isAscending: false }], limit, }, '0'], ]); return res.methodResponses[0][1].ids; } export interface OutgoingAttachment { blobId: string; type: string; name: string; size?: number; disposition?: 'attachment' | 'inline'; cid?: string; } export async function sendEmail( email: { from: EmailAddress[]; to: EmailAddress[]; cc?: EmailAddress[]; bcc?: EmailAddress[]; subject: string; htmlBody?: string; textBody?: string; attachments?: OutgoingAttachment[]; inReplyTo?: string; references?: string; }, identityId: string, sentMailboxId: string, ): Promise { const accountId = jmapClient.accountId; const emailCreate: Record = { from: email.from, to: email.to, cc: email.cc, bcc: email.bcc, subject: email.subject, mailboxIds: { [sentMailboxId]: true }, keywords: { $seen: true }, }; const bodyValues: Record = {}; if (email.htmlBody) { emailCreate.htmlBody = [{ partId: 'html', type: 'text/html' }]; bodyValues.html = { value: email.htmlBody }; } if (email.textBody || !email.htmlBody) { emailCreate.textBody = [{ partId: 'text', type: 'text/plain' }]; bodyValues.text = { value: email.textBody ?? '' }; } emailCreate.bodyValues = bodyValues; if (email.attachments?.length) { emailCreate.attachments = email.attachments.map((a) => { const part: Record = { blobId: a.blobId, type: a.type, name: a.name, disposition: a.disposition ?? 'attachment', }; if (a.size != null) part.size = a.size; if (a.cid) part.cid = a.cid; return part; }); } if (email.inReplyTo) { emailCreate['header:In-Reply-To:asText'] = email.inReplyTo; emailCreate['header:References:asText'] = email.references ?? email.inReplyTo; } await jmapClient.request( [ ['Email/set', { accountId, create: { draft: emailCreate } }, '0'], ['EmailSubmission/set', { accountId, create: { 'sub-1': { emailId: '#draft', identityId } }, }, '1'], ], [CAPABILITIES.CORE, CAPABILITIES.MAIL, CAPABILITIES.SUBMISSION], ); }