mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
Merge pull request #1019 from lucamzanon/fix/compose-error-recovery
fix: keep the draft when a send is not confirmed
This commit is contained in:
@@ -29,7 +29,7 @@ interface CapturedRequest { methodCalls: JMAPMethodCall[] }
|
||||
* Email/set create come back as notCreated; `failSubmission` does the same
|
||||
* for EmailSubmission/set.
|
||||
*/
|
||||
function mockFlow({ failCreate = false, failSubmission = false } = {}) {
|
||||
function mockFlow({ failCreate = false, failSubmission = false, methodError = false, missingSubmission = false, emptyErrors = false } = {}) {
|
||||
const captured: CapturedRequest[] = [];
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (_url, init) => {
|
||||
const body = JSON.parse((init as { body: string }).body) as CapturedRequest;
|
||||
@@ -56,11 +56,14 @@ function mockFlow({ failCreate = false, failSubmission = false } = {}) {
|
||||
result.created = { [key]: { id: 'email-new-1' } };
|
||||
}
|
||||
}
|
||||
if (emptyErrors) result.notCreated = {};
|
||||
if (args.destroy) {
|
||||
result.destroyed = args.destroy;
|
||||
}
|
||||
methodResponses.push(['Email/set', result, callId]);
|
||||
} else if (method === 'EmailSubmission/set') {
|
||||
if (missingSubmission) continue;
|
||||
if (methodError) { methodResponses.push(['error', { type: 'serverFail', description: 'Google unavailable; draft retained' }, callId]); continue; }
|
||||
methodResponses.push(['EmailSubmission/set',
|
||||
failSubmission
|
||||
? { notCreated: { '1': { type: 'forbiddenFrom', description: 'not allowed' } } }
|
||||
@@ -230,3 +233,16 @@ describe('draft replace lifecycle (#849)', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compose failure recovery', () => {
|
||||
beforeEach(() => { vi.restoreAllMocks(); vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
||||
it.each([{ methodError: true }, { missingSubmission: true }])('does not claim success or delete the old draft without confirmation: %j', async options => {
|
||||
const client = createClient(); const captured = mockFlow(options);
|
||||
await expect(client.sendEmail(['bob@example.com'], 'Subject', 'body', undefined, undefined, 'identity-1', 'user@example.com', 'old-draft')).rejects.toThrow();
|
||||
expect(emailSetCalls(captured).some(call => call.destroy)).toBe(false);
|
||||
});
|
||||
it('accepts empty notCreated maps as success', async () => {
|
||||
const client = createClient(); mockFlow({ emptyErrors: true });
|
||||
expect(await client.createDraft(['bob@example.com'], 'Subject', 'body')).toBe('email-new-1');
|
||||
});
|
||||
});
|
||||
|
||||
+10
-2
@@ -3868,7 +3868,8 @@ export class JMAPClient implements IJMAPClient {
|
||||
if (response.methodResponses?.[0]?.[0] === "Email/set") {
|
||||
const result = response.methodResponses[0][1];
|
||||
|
||||
if (result.notCreated) {
|
||||
// An empty notCreated map is a success some servers spell out.
|
||||
if (result.notCreated && Object.keys(result.notCreated).length) {
|
||||
const errors = result.notCreated;
|
||||
const firstError = Object.values(errors)[0] as { description?: string; type?: string };
|
||||
console.error('Draft save error:', firstError);
|
||||
@@ -4134,7 +4135,7 @@ export class JMAPClient implements IJMAPClient {
|
||||
|
||||
if (response.methodResponses) {
|
||||
for (const [methodName, result] of response.methodResponses) {
|
||||
if (result.notCreated) {
|
||||
if (result.notCreated && Object.keys(result.notCreated).length) {
|
||||
// Include method name + full error object so it's clear whether the
|
||||
// failure came from Email/set (draft create) or EmailSubmission/set
|
||||
// (actual send) and which JMAP error type/properties were returned.
|
||||
@@ -4188,6 +4189,13 @@ export class JMAPClient implements IJMAPClient {
|
||||
}
|
||||
}
|
||||
|
||||
// No EmailSubmission came back - a method error, or no answer for it at
|
||||
// all. Nothing confirms the message left: report a failure and keep the
|
||||
// old draft rather than replacing it as if the send had worked.
|
||||
if (!emailSubmissionId) {
|
||||
throw new Error('Send confirmation was not received. Check Sent before sending again. Your draft has been kept.');
|
||||
}
|
||||
|
||||
// With every recipient refused nothing was queued, yet the submission
|
||||
// exists and its onSuccessUpdateEmail filed the copy into Sent (#1123).
|
||||
// Drop that copy and keep the old draft: the send failed.
|
||||
|
||||
Reference in New Issue
Block a user