mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
- Implement markdown formatting in the email composer, allowing users to use bold, italic, lists, and blockquotes. - Add functionality to upload and manage attachments, including inline images. - Introduce a new compose-format utility for converting markdown to HTML. - Update the ComposeScreen to handle attachments and formatting actions. - Refactor ContactDetailScreen and EmailListScreen for improved code clarity and performance. - Add tests for markdown formatting and selection wrapping functionalities.
244 lines
6.7 KiB
TypeScript
244 lines
6.7 KiB
TypeScript
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<Mailbox[]> {
|
|
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<string, unknown>;
|
|
},
|
|
): 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<Email[]> {
|
|
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<Email> {
|
|
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<Thread> {
|
|
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<string, boolean>,
|
|
): Promise<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<string[]> {
|
|
const accountId = jmapClient.accountId;
|
|
const filter: Record<string, unknown> = { 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 <addresses>" without picking a specific mailbox).
|
|
export async function queryEmailsByFilter(
|
|
filter: Record<string, unknown>,
|
|
limit = 5,
|
|
): Promise<string[]> {
|
|
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<void> {
|
|
const accountId = jmapClient.accountId;
|
|
const emailCreate: Record<string, unknown> = {
|
|
from: email.from,
|
|
to: email.to,
|
|
cc: email.cc,
|
|
bcc: email.bcc,
|
|
subject: email.subject,
|
|
mailboxIds: { [sentMailboxId]: true },
|
|
keywords: { $seen: true },
|
|
};
|
|
|
|
const bodyValues: Record<string, { value: string }> = {};
|
|
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<string, unknown> = {
|
|
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],
|
|
);
|
|
}
|