mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
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.
86 lines
3.8 KiB
TypeScript
86 lines
3.8 KiB
TypeScript
import { NoLongerScheduledError, RequestTimeoutError, ScheduleTooLateError, SubmissionQuotaError } from '@/lib/jmap/client';
|
|
|
|
type Translate = (key: string, values?: Record<string, string>) => string;
|
|
|
|
/**
|
|
* Thrown by "Undo send" and "Cancel and edit" when the cancel went through
|
|
* but a step after it failed: putting the message back in Drafts
|
|
* (`restore`), or removing the copy of an S/MIME message, which is composed
|
|
* again from scratch (`remove`). Nothing goes out; the message is still in
|
|
* Sent. Reported apart from a failed cancel, which may still let it go.
|
|
*/
|
|
export class SendCancelledCleanupError extends Error {
|
|
constructor(readonly step: 'restore' | 'remove', readonly reason: unknown) {
|
|
super(step === 'restore'
|
|
? 'Sending was cancelled, but the message could not be put back in Drafts'
|
|
: 'Sending was cancelled, but the unsent message could not be removed from Sent');
|
|
this.name = 'SendCancelledCleanupError';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* What to tell the user about a send (or reschedule) that failed, from the
|
|
* `email_composer` messages. Most errors carry the server's own description;
|
|
* these are the ones it would misrepresent or leave unexplained:
|
|
* - a full submission quota on Stalwart: it asks the user to "delete some",
|
|
* but the client has already removed every record it may, so only an
|
|
* administrator can help. Another server's overQuota may stand for any
|
|
* sending limit, so its own description goes with a generic sentence;
|
|
* - a timeout, which is not a clean failure: the submission may have reached
|
|
* the server and gone out, with only the answer lost. Saying "send failed"
|
|
* would invite a re-send and a duplicate, so point at Sent instead (#702);
|
|
* - a hold beyond the server's limit.
|
|
*/
|
|
export function sendFailureMessage(error: unknown, t: Translate): string {
|
|
if (error instanceof SubmissionQuotaError) {
|
|
return error.countsRecords
|
|
? t('submission_quota')
|
|
: t('submission_limit', { reason: error.description || error.jmapType });
|
|
}
|
|
if (error instanceof RequestTimeoutError) return t('send_timeout');
|
|
if (error instanceof ScheduleTooLateError) return t('schedule_send_too_late');
|
|
return error instanceof Error && error.message ? error.message : t('send_failed');
|
|
}
|
|
|
|
/**
|
|
* The toast for a failed "Send now" or "Reschedule", from the `email_viewer`
|
|
* (`tViewer`) and `email_composer` (`tComposer`) messages. At a full
|
|
* submission quota no replacement was made, so the message still goes out at
|
|
* its old time, `scheduledAt` (formatted) when known. Making room takes a
|
|
* while, and the original may have gone out meanwhile.
|
|
*/
|
|
export function rescheduleFailureToast(
|
|
error: unknown,
|
|
tViewer: Translate,
|
|
tComposer: Translate,
|
|
scheduledAt?: string,
|
|
): { title: string; message?: string } {
|
|
if (error instanceof NoLongerScheduledError) {
|
|
return { title: tViewer(error.sent ? 'already_sent' : 'no_longer_scheduled') };
|
|
}
|
|
if (error instanceof SubmissionQuotaError) {
|
|
return {
|
|
title: scheduledAt ? tViewer('still_scheduled_at', { date: scheduledAt }) : tViewer('still_scheduled'),
|
|
message: sendFailureMessage(error, tComposer),
|
|
};
|
|
}
|
|
return { title: sendFailureMessage(error, tComposer) };
|
|
}
|
|
|
|
/**
|
|
* The toast for a failed "Undo send" or "Cancel and edit". A failed cancel
|
|
* may still let the mail go, so it must never read as done: it gets
|
|
* `cancelFailedTitle` and the reason. One that went through while a step
|
|
* after it failed says where the message is instead.
|
|
*/
|
|
export function cancelFailureToast(
|
|
error: unknown,
|
|
tViewer: Translate,
|
|
cancelFailedTitle: string,
|
|
): { title: string; message?: string } {
|
|
if (error instanceof SendCancelledCleanupError) {
|
|
return { title: tViewer(error.step === 'restore' ? 'send_cancelled_restore_failed' : 'send_cancelled_remove_failed') };
|
|
}
|
|
return { title: cancelFailedTitle, message: error instanceof Error ? error.message : undefined };
|
|
}
|