Files
webmail/lib/__tests__/send-failure-message.test.ts
T
Linus Rath 65eb4b1029 fix: report failed undo and reschedule with where the mail stands
Several failures around scheduled and undoable sends said the wrong thing
or nothing at all:

- "Undo send failed" also appeared when the cancel went through and only
  putting the message back in Drafts failed, suggesting the mail might
  still go out. Now that case says the send was cancelled and the message
  is still in Sent (or, for S/MIME, that its copy could not be removed);
  a failed cancel keeps the old message.
- "Cancel and edit" had no error handling, so a failure ended as an
  unhandled rejection. It now reports a failed cancel and a failed restore
  the same way as undo.
- A failed "Send now" or reschedule at a full submission quota did not say
  that the message is still scheduled. It now does, with its time.
- The reschedule retry after making room could send a message twice: the
  prune and change-log walk take a while, and the original could go out
  meanwhile. The original is read again first and nothing is resubmitted
  unless it is still pending ("This message was already sent" otherwise),
  and HOLDFOR is worked out again after the slow step.
- The List-Unsubscribe mailto path showed only a generic error at a full
  quota; it now shows the quota explanation too.
- The Stalwart explanation of a full quota (Bulwark removed what it
  could, an administrator has to raise the limit) was shown for any
  server's `overQuota`. It now needs `urn:stalwart:jmap` on the
  submission account; other servers get a generic sentence with their
  own description.
- At a full quota every attempt, including each automatic read receipt,
  ran the forced prune, the change-log walk and the gets again. When they
  find nothing that can go, the account is left alone for five minutes.
2026-09-25 20:10:01 +02:00

79 lines
4.3 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { NoLongerScheduledError, RequestTimeoutError, ScheduleTooLateError, SubmissionQuotaError } from '../jmap/client';
import { SendCancelledCleanupError, cancelFailureToast, rescheduleFailureToast, sendFailureMessage } from '../send-failure-message';
// Keys of a namespace, returned with their values so the test sees which
// message was chosen and what went into it.
const translator = (namespace: string) => (key: string, values?: Record<string, string>) =>
values ? `${namespace}.${key} ${JSON.stringify(values)}` : `${namespace}.${key}`;
const t = translator('email_composer');
const tViewer = translator('email_viewer');
const STALWART_QUOTA = 'There are too many email submissions, please delete some before adding a new one.';
describe('sendFailureMessage', () => {
it('explains a full submission quota instead of showing the server text on Stalwart 1.0', () => {
const error = new SubmissionQuotaError(STALWART_QUOTA, true);
expect(sendFailureMessage(error, t)).toBe('email_composer.submission_quota');
});
it("gives another server's overQuota its own description, not Stalwart's explanation", () => {
expect(sendFailureMessage(new SubmissionQuotaError('Daily sending limit exceeded'), t))
.toBe('email_composer.submission_limit {"reason":"Daily sending limit exceeded"}');
expect(sendFailureMessage(new SubmissionQuotaError(undefined, false), t))
.toBe('email_composer.submission_limit {"reason":"overQuota"}');
});
it('points at Sent after a timeout and names the hold limit', () => {
expect(sendFailureMessage(new RequestTimeoutError(30_000), t)).toBe('email_composer.send_timeout');
expect(sendFailureMessage(new ScheduleTooLateError(604800), t)).toBe('email_composer.schedule_send_too_late');
});
it("shows other errors' own text, and a generic message when there is none", () => {
expect(sendFailureMessage(new Error('Envelope mailFrom does not match identity email address. [forbiddenFrom]'), t))
.toBe('Envelope mailFrom does not match identity email address. [forbiddenFrom]');
expect(sendFailureMessage(new Error(''), t)).toBe('email_composer.send_failed');
expect(sendFailureMessage('nope', t)).toBe('email_composer.send_failed');
});
});
describe('rescheduleFailureToast', () => {
it('says the message is still scheduled, and when, at a full quota on Stalwart 1.0', () => {
const error = new SubmissionQuotaError(STALWART_QUOTA, true);
expect(rescheduleFailureToast(error, tViewer, t, 'Sep 26, 10:00')).toEqual({
title: 'email_viewer.still_scheduled_at {"date":"Sep 26, 10:00"}',
message: 'email_composer.submission_quota',
});
expect(rescheduleFailureToast(error, tViewer, t).title).toBe('email_viewer.still_scheduled');
});
it('says the message went out, or is no longer scheduled, when the original left the queue meanwhile', () => {
expect(rescheduleFailureToast(new NoLongerScheduledError(true), tViewer, t, 'Sep 26, 10:00'))
.toEqual({ title: 'email_viewer.already_sent' });
expect(rescheduleFailureToast(new NoLongerScheduledError(false), tViewer, t))
.toEqual({ title: 'email_viewer.no_longer_scheduled' });
});
it('falls back to the send failure message otherwise', () => {
expect(rescheduleFailureToast(new RequestTimeoutError(30_000), tViewer, t, 'Sep 26, 10:00'))
.toEqual({ title: 'email_composer.send_timeout' });
expect(rescheduleFailureToast(new Error('cannotUnsend'), tViewer, t)).toEqual({ title: 'cannotUnsend' });
});
});
describe('cancelFailureToast', () => {
it('reports a failed cancel with its reason: the mail may still go out', () => {
expect(cancelFailureToast(new Error('The requested message is no longer in the queue.'), tViewer, 'Undo send failed'))
.toEqual({ title: 'Undo send failed', message: 'The requested message is no longer in the queue.' });
});
it('says the send was cancelled when only a step after the cancel failed', () => {
expect(cancelFailureToast(new SendCancelledCleanupError('restore', new Error('503')), tViewer, 'Undo send failed'))
.toEqual({ title: 'email_viewer.send_cancelled_restore_failed' });
expect(cancelFailureToast(new SendCancelledCleanupError('remove', new Error('503')), tViewer, 'Undo send failed'))
.toEqual({ title: 'email_viewer.send_cancelled_remove_failed' });
});
});