mirror of
https://github.com/root-fr/jmap-webmail.git
synced 2026-10-09 00:00:49 +00:00
175 lines
6.4 KiB
TypeScript
175 lines
6.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { createTestClient as createClient, jsonResponse } from './jmap-test-helpers';
|
|
|
|
const GROUP_MAILBOXES = {
|
|
methodResponses: [[
|
|
'Mailbox/get',
|
|
{
|
|
list: [
|
|
{ id: 'g-drafts', role: 'drafts', name: 'Drafts' },
|
|
{ id: 'g-sent', role: 'sent', name: 'Sent' },
|
|
],
|
|
},
|
|
'0',
|
|
]],
|
|
};
|
|
|
|
const CREATE_AND_SUBMIT_OK = {
|
|
methodResponses: [
|
|
['Email/set', { created: { 'draft-x': { id: 'email-real-1' } } }, '0'],
|
|
['EmailSubmission/set', { created: { '1': { id: 'sub-1' } } }, '1'],
|
|
],
|
|
};
|
|
|
|
const COPY_AND_SUBMIT_OK = {
|
|
methodResponses: [
|
|
['Email/copy', { created: { 'draft-x': { id: 'email-copy-1' } } }, '0'],
|
|
['EmailSubmission/set', { created: { '1': { id: 'sub-1' } } }, '1'],
|
|
],
|
|
};
|
|
|
|
const DESTROY_OK = {
|
|
methodResponses: [['Email/set', { destroyed: ['stale-draft-1'] }, '0']],
|
|
};
|
|
|
|
function bodiesOf(fetchSpy: { mock: { calls: unknown[][] } }) {
|
|
return fetchSpy.mock.calls.map(
|
|
(call) => JSON.parse((call[1] as { body: string }).body)
|
|
);
|
|
}
|
|
|
|
describe('JMAPClient.sendEmail send-as group account', () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('routes draft, submission and Sent copy through the target account', async () => {
|
|
const client = createClient();
|
|
const fetchSpy = vi.spyOn(globalThis, 'fetch')
|
|
.mockResolvedValueOnce(jsonResponse(GROUP_MAILBOXES))
|
|
.mockResolvedValueOnce(jsonResponse(CREATE_AND_SUBMIT_OK));
|
|
|
|
await client.sendEmail(
|
|
['to@example.com'], 'Subject', 'Body',
|
|
undefined, undefined, 'identity-g1', 'support@example.com', undefined, undefined,
|
|
'group-1',
|
|
);
|
|
|
|
const bodies = bodiesOf(fetchSpy);
|
|
|
|
expect(bodies[0].methodCalls[0][0]).toBe('Mailbox/get');
|
|
expect(bodies[0].methodCalls[0][1].accountId).toBe('group-1');
|
|
|
|
const [createCall, submitCall] = bodies[1].methodCalls;
|
|
expect(createCall[0]).toBe('Email/set');
|
|
expect(createCall[1].accountId).toBe('group-1');
|
|
const created = Object.values(createCall[1].create)[0] as {
|
|
mailboxIds: Record<string, boolean>;
|
|
};
|
|
expect(created.mailboxIds).toEqual({ 'g-drafts': true });
|
|
|
|
expect(submitCall[0]).toBe('EmailSubmission/set');
|
|
expect(submitCall[1].accountId).toBe('group-1');
|
|
expect(submitCall[1].onSuccessUpdateEmail['#1']['mailboxIds/g-sent']).toBe(true);
|
|
|
|
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('copies an autosaved primary draft into the target account and destroys the stale one after success', async () => {
|
|
const client = createClient();
|
|
const fetchSpy = vi.spyOn(globalThis, 'fetch')
|
|
.mockResolvedValueOnce(jsonResponse(GROUP_MAILBOXES))
|
|
.mockResolvedValueOnce(jsonResponse(COPY_AND_SUBMIT_OK))
|
|
.mockResolvedValueOnce(jsonResponse(DESTROY_OK));
|
|
|
|
await client.sendEmail(
|
|
['to@example.com'], 'Subject', 'Body',
|
|
undefined, undefined, 'identity-g1', 'support@example.com', 'stale-draft-1', undefined,
|
|
'group-1',
|
|
);
|
|
|
|
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
|
|
|
const bodies = bodiesOf(fetchSpy);
|
|
|
|
// The stale primary draft is never submitted directly: it is copied
|
|
// into the target account (body, headers and attachments intact) and
|
|
// the copy is back-referenced by the submission.
|
|
const [copyCall, submitCall] = bodies[1].methodCalls;
|
|
expect(copyCall[0]).toBe('Email/copy');
|
|
expect(copyCall[1].fromAccountId).toBe('account-1');
|
|
expect(copyCall[1].accountId).toBe('group-1');
|
|
const copied = Object.values(copyCall[1].create)[0] as {
|
|
id: string;
|
|
mailboxIds: Record<string, boolean>;
|
|
};
|
|
expect(copied.id).toBe('stale-draft-1');
|
|
expect(copied.mailboxIds).toEqual({ 'g-drafts': true });
|
|
|
|
expect(submitCall[0]).toBe('EmailSubmission/set');
|
|
const submissionEmailId = (Object.values(submitCall[1].create)[0] as { emailId: string }).emailId;
|
|
expect(submissionEmailId.startsWith('#')).toBe(true);
|
|
|
|
// Cleanup destroys the stale draft in the primary account, after success.
|
|
const cleanupCall = bodies[2].methodCalls[0];
|
|
expect(cleanupCall[0]).toBe('Email/set');
|
|
expect(cleanupCall[1].accountId).toBe('account-1');
|
|
expect(cleanupCall[1].destroy).toEqual(['stale-draft-1']);
|
|
});
|
|
|
|
it('destroys the copied message in the target account when the submission is refused', async () => {
|
|
const client = createClient();
|
|
const fetchSpy = vi.spyOn(globalThis, 'fetch')
|
|
.mockResolvedValueOnce(jsonResponse(GROUP_MAILBOXES))
|
|
.mockResolvedValueOnce(jsonResponse({
|
|
methodResponses: [
|
|
['Email/copy', { created: { 'draft-x': { id: 'copy-1' } } }, '0'],
|
|
['EmailSubmission/set', { notCreated: { '1': { type: 'forbiddenFrom', description: 'Refused' } } }, '1'],
|
|
],
|
|
}))
|
|
.mockResolvedValueOnce(jsonResponse({
|
|
methodResponses: [['Email/set', { destroyed: ['copy-1'] }, '0']],
|
|
}));
|
|
|
|
await expect(
|
|
client.sendEmail(
|
|
['to@example.com'], 'Subject', 'Body',
|
|
undefined, undefined, 'identity-g1', 'support@example.com', 'stale-draft-1', undefined,
|
|
'group-1',
|
|
),
|
|
).rejects.toThrow('Refused');
|
|
|
|
// The refused submission must not strand the copy in the group
|
|
// account's Drafts, and the primary draft must survive untouched.
|
|
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
|
const bodies = bodiesOf(fetchSpy);
|
|
const cleanupCall = bodies[2].methodCalls[0];
|
|
expect(cleanupCall[0]).toBe('Email/set');
|
|
expect(cleanupCall[1].accountId).toBe('group-1');
|
|
expect(cleanupCall[1].destroy).toEqual(['copy-1']);
|
|
});
|
|
|
|
it('throws on a group-account submission error and leaves the primary draft alone', async () => {
|
|
const client = createClient();
|
|
const fetchSpy = vi.spyOn(globalThis, 'fetch')
|
|
.mockResolvedValueOnce(jsonResponse(GROUP_MAILBOXES))
|
|
.mockResolvedValueOnce(jsonResponse({
|
|
methodResponses: [['error', { type: 'forbidden', description: 'Not allowed' }, '0']],
|
|
}));
|
|
|
|
await expect(
|
|
client.sendEmail(
|
|
['to@example.com'], 'Subject', 'Body',
|
|
undefined, undefined, 'identity-g1', 'support@example.com', 'stale-draft-1', undefined,
|
|
'group-1',
|
|
),
|
|
).rejects.toThrow('Not allowed');
|
|
|
|
const bodies = bodiesOf(fetchSpy);
|
|
expect(bodies[1].methodCalls[0][1].accountId).toBe('group-1');
|
|
|
|
// No cleanup request: the autosaved primary draft must survive a failed send.
|
|
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|