mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 00:07:01 +00:00
fix: stop reporting a send as failed when only filing it into Sent failed
`sendEmail` threw on any method error in the response. Stalwart appends one for the implicit `onSuccessUpdateEmail` Email/set that moves the sent message from Drafts to Sent, so a message that had already gone out showed "Send failed" and a retry sent it a second time. A method error that follows a created submission is now returned as a filing warning, like the record-level filing failures already were; errors before the submission exists still fail the send.
This commit is contained in:
@@ -11,7 +11,7 @@ vi.mock('../jmap-client', () => ({
|
||||
}));
|
||||
|
||||
import { jmapClient } from '../jmap-client';
|
||||
import { rescheduleScheduledSend } from '../email';
|
||||
import { rescheduleScheduledSend, sendEmail } from '../email';
|
||||
|
||||
const mockRequest = jmapClient.request as ReturnType<typeof vi.fn>;
|
||||
|
||||
@@ -102,3 +102,57 @@ describe('rescheduleScheduledSend', () => {
|
||||
expect(mockRequest.mock.calls[0][0][0][1].create.replacement).toEqual({ emailId: 'e-1', identityId: 'id-1' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendEmail', () => {
|
||||
const OUTGOING = {
|
||||
from: [{ email: 'me@example.com' }],
|
||||
to: [{ email: 'you@example.com' }],
|
||||
subject: 'Hello',
|
||||
textBody: 'Hi',
|
||||
};
|
||||
|
||||
it('reports a failed post-send filing as a warning, not a failed send (B23)', async () => {
|
||||
// Stalwart answers the implicit onSuccessUpdateEmail Email/set with a
|
||||
// method error after the submission was created: the mail went out.
|
||||
mockRequest.mockResolvedValueOnce({
|
||||
methodResponses: [
|
||||
['Email/set', { created: { draft: { id: 'e-new' } } }, '0'],
|
||||
['EmailSubmission/set', { created: { 'sub-1': { id: 's-1', sendAt: '2026-09-24T10:00:30Z' } } }, '1'],
|
||||
['error', { type: 'forbidden', description: 'You do not have access to this mailbox' }, '1'],
|
||||
],
|
||||
});
|
||||
|
||||
const result = await sendEmail(OUTGOING, 'identity-1', 'sent-mb', 30, { draftsMailboxId: 'drafts-mb' });
|
||||
|
||||
expect(result).toMatchObject({
|
||||
scheduled: true,
|
||||
emailId: 'e-new',
|
||||
emailSubmissionId: 's-1',
|
||||
sendAt: '2026-09-24T10:00:30Z',
|
||||
filingWarning: 'You do not have access to this mailbox',
|
||||
});
|
||||
});
|
||||
|
||||
it('still fails when the submission itself was refused', async () => {
|
||||
mockRequest.mockResolvedValueOnce({
|
||||
methodResponses: [
|
||||
['Email/set', { created: { draft: { id: 'e-new' } } }, '0'],
|
||||
['error', { type: 'invalidArguments', description: 'Invalid envelope' }, '1'],
|
||||
],
|
||||
});
|
||||
|
||||
await expect(sendEmail(OUTGOING, 'identity-1', 'sent-mb', undefined, { draftsMailboxId: 'drafts-mb' }))
|
||||
.rejects.toThrow('Invalid envelope');
|
||||
});
|
||||
|
||||
it('still fails when the message could not be created', async () => {
|
||||
mockRequest.mockResolvedValueOnce({
|
||||
methodResponses: [
|
||||
['error', { type: 'serverFail', description: 'Disk full' }, '0'],
|
||||
['EmailSubmission/set', { notCreated: { 'sub-1': { type: 'invalidProperties' } } }, '1'],
|
||||
],
|
||||
});
|
||||
|
||||
await expect(sendEmail(OUTGOING, 'identity-1', 'sent-mb')).rejects.toThrow('Disk full');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1177,6 +1177,15 @@ export async function sendEmail(
|
||||
let filingWarning: string | undefined;
|
||||
for (const [methodName, result] of res.methodResponses) {
|
||||
if (methodName === 'error' || methodName.endsWith('/error')) {
|
||||
// Once the submission exists the message has left (or is held), so a
|
||||
// later error is the implicit `onSuccessUpdateEmail` Email/set that
|
||||
// Stalwart appends failing to file it. Failing the send here made the
|
||||
// user retry and send the message twice.
|
||||
if (emailSubmissionId) {
|
||||
const err = result as { description?: string; type?: string };
|
||||
filingWarning = filingWarning ?? (err.description || err.type || 'post-send filing failed');
|
||||
continue;
|
||||
}
|
||||
throw new Error((result as { description?: string }).description ?? 'Send failed');
|
||||
}
|
||||
if (methodName === 'Email/set') {
|
||||
|
||||
Reference in New Issue
Block a user