mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 00:14:12 +00:00
feat: let plugins page through mail search results
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { InstalledPlugin } from '../plugin-types';
|
||||
import type { IJMAPClient } from '../jmap/client-interface';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useEmailStore } from '@/stores/email-store';
|
||||
|
||||
vi.mock('@/stores/toast-store', () => ({
|
||||
toast: { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() },
|
||||
}));
|
||||
|
||||
import { dispatchApiCall } from '../plugin-sandbox/host-api';
|
||||
|
||||
// api.email.search pages with `position`, so a plugin can go past the
|
||||
// first 50 results (the assistant's batches of more than 50 messages).
|
||||
|
||||
const READER = {
|
||||
id: 'reader', name: 'Reader', version: '1.0.0', author: 'test', description: '',
|
||||
type: 'hook', permissions: ['email:read'], grantedPermissions: ['email:read'], entrypoint: 'index.js',
|
||||
enabled: true, status: 'running', settings: {},
|
||||
} as InstalledPlugin;
|
||||
const search = (query: unknown) => dispatchApiCall(READER, 'email.search', [query]);
|
||||
|
||||
let client: Record<string, ReturnType<typeof vi.fn>>;
|
||||
|
||||
beforeEach(() => {
|
||||
client = {
|
||||
getEmails: vi.fn(async () => ({ emails: [], hasMore: false, total: 0 })),
|
||||
getMailboxes: vi.fn(async () => []),
|
||||
getAccountId: vi.fn(() => 'acc1'),
|
||||
};
|
||||
useAuthStore.setState({ client: client as unknown as IJMAPClient });
|
||||
useEmailStore.setState({ mailboxes: [], selectedEmail: null });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
useAuthStore.setState({ client: null });
|
||||
});
|
||||
|
||||
describe('api.email.search position', () => {
|
||||
it('asks the server for the page after `position` results', async () => {
|
||||
await search({ text: 'invoice', limit: 50, position: 50 });
|
||||
expect(client.getEmails).toHaveBeenCalledWith(undefined, undefined, 50, 50, undefined, false, { text: 'invoice' });
|
||||
});
|
||||
|
||||
it('starts at the first result without it', async () => {
|
||||
await search({ text: 'invoice' });
|
||||
expect(client.getEmails).toHaveBeenCalledWith(undefined, undefined, 20, 0, undefined, false, { text: 'invoice' });
|
||||
});
|
||||
|
||||
it('takes whole numbers from 0 to 1,000,000 only', async () => {
|
||||
await expect(search({ text: 'x', position: -1 })).rejects.toThrow(/position must be a whole number from 0 to 1000000/);
|
||||
await expect(search({ text: 'x', position: 1.5 })).rejects.toThrow(/position must be a whole number/);
|
||||
await expect(search({ text: 'x', position: '10' })).rejects.toThrow(/position must be a whole number/);
|
||||
expect(client.getEmails).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -147,11 +147,13 @@ const SEARCH_DATE_FIELDS = ['after', 'before'] as const;
|
||||
* subject and body, whole words), `from`, `to`, `subject`, `after` and
|
||||
* `before` (dates), `hasAttachment`, in one folder with `mailboxId`. At
|
||||
* least one condition; summaries without the body (api.email.get reads one).
|
||||
* `position` skips that many results, to page past the first `limit`.
|
||||
*/
|
||||
export async function doEmailSearch(options: unknown): Promise<EmailSummary[]> {
|
||||
const method = 'email.search';
|
||||
const opts = readObject(options, method, 'query', [...SEARCH_TEXT_FIELDS, ...SEARCH_DATE_FIELDS, 'hasAttachment', 'mailboxId', 'limit']);
|
||||
const opts = readObject(options, method, 'query', [...SEARCH_TEXT_FIELDS, ...SEARCH_DATE_FIELDS, 'hasAttachment', 'mailboxId', 'limit', 'position']);
|
||||
const limit = opts.limit === undefined ? 20 : readInteger(opts.limit, method, 'limit', 1, MAX_PLUGIN_SEARCH_RESULTS);
|
||||
const position = opts.position === undefined ? 0 : readInteger(opts.position, method, 'position', 0, 1_000_000);
|
||||
const filter: Record<string, unknown> = {};
|
||||
for (const key of SEARCH_TEXT_FIELDS) {
|
||||
if (opts[key] === undefined) continue;
|
||||
@@ -177,7 +179,7 @@ export async function doEmailSearch(options: unknown): Promise<EmailSummary[]> {
|
||||
jmapMailboxId = mailbox.originalId || mailbox.id;
|
||||
accountId = mailbox.isShared ? mailbox.accountId : undefined;
|
||||
}
|
||||
const { emails } = await client.getEmails(jmapMailboxId, accountId, limit, 0, undefined, false, filter);
|
||||
const { emails } = await client.getEmails(jmapMailboxId, accountId, limit, position, undefined, false, filter);
|
||||
return emails.map((email) => emailSummary(email, accountId ?? client.getAccountId()));
|
||||
}
|
||||
|
||||
|
||||
@@ -828,9 +828,9 @@ function buildCoreApi(manifest: PluginManifest) {
|
||||
* body: `text` (sender, recipients, subject and body, whole words),
|
||||
* `from`, `to`, `subject`, `after`/`before` (dates), `hasAttachment`,
|
||||
* in one folder with `mailboxId`. At least one condition; limit 1-50
|
||||
* (default 20).
|
||||
* (default 20); `position` skips that many results (paging).
|
||||
*/
|
||||
search: (query: { text?: string; from?: string; to?: string; subject?: string; after?: string; before?: string; hasAttachment?: boolean; mailboxId?: string; limit?: number }) =>
|
||||
search: (query: { text?: string; from?: string; to?: string; subject?: string; after?: string; before?: string; hasAttachment?: boolean; mailboxId?: string; limit?: number; position?: number }) =>
|
||||
callApi('email.search', [query]) as Promise<EmailSummary[]>,
|
||||
/**
|
||||
* The bytes of one attachment of a message (email:blob-read, at most
|
||||
|
||||
Reference in New Issue
Block a user