Files
native/src/lib/send-failure-message.ts
T

129 lines
5.7 KiB
TypeScript

import { NetworkError, RequestTimeoutError } from '../api/jmap-client';
import { useNetworkStore } from '../stores/network-store';
import {
NoLongerScheduledError, RecipientsRefusedError, ScheduleTooLateError, SubmissionQuotaError, formatRefusedRecipients,
type RefusedRecipient,
} from '../api/jmap-result';
import type { MessageParams } from '../i18n/format';
type Translate = (key: string, fallback?: string, params?: MessageParams) => string;
/**
* What to tell the user about a send (or reschedule) that failed (webmail
* send-failure-message). 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 app 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 message may have gone out
* with only the answer lost, so point at Sent rather than invite a re-send
* (#702);
* - a hold beyond the server's limit;
* - "Send now" or a reschedule that found the message gone out, or no longer
* scheduled, while it made room for the replacement;
* - every recipient refused: the server took the submission, so nothing
* else would say that the message went nowhere. Its reply per address
* goes with it.
*/
export function sendFailureMessage(error: unknown, t: Translate): string {
if (error instanceof SubmissionQuotaError) {
return error.countsRecords
? t(
'email_composer.submission_quota',
'The server has too many sent-message records for this account. Bulwark removed the ones it could; if this keeps happening, ask your administrator to raise the email submission limit.',
)
: t(
'email_composer.submission_limit',
'The server refused to send the message because a limit was reached: {reason}',
{ reason: error.description || error.type },
);
}
if (error instanceof NoLongerScheduledError) {
return error.sent
? t('scheduled.already_sent', 'This message was already sent.')
: t('scheduled.no_longer_scheduled', 'This message is no longer scheduled.');
}
if (error instanceof RequestTimeoutError) {
return t(
'email_composer.send_timeout_body',
'The message may already have gone out. Check your Sent folder before sending it again.',
);
}
if (error instanceof ScheduleTooLateError) {
return t('email_composer.schedule_too_late_body', 'That is later than this server allows. Pick an earlier time.');
}
if (error instanceof RecipientsRefusedError) {
return t('email_composer.recipients_refused', 'Not sent: the server refused {recipients}.', {
recipients: formatRefusedRecipients(error.recipients),
});
}
return error instanceof Error && error.message
? error.message
: t('notifications.error_sending', 'Failed to send email');
}
/**
* sendFailureMessage for a message the user is writing (composer, quick
* reply): a send that never reached the server because the device is
* offline says so, and that the text is still there, instead of "Network
* request failed" (webmail email-composer.tsx send_offline). The server is
* unreachable when the transport failed (the client's NetworkError, or a
* bare fetch TypeError) or the device reports no connection.
*/
export function composerSendFailureMessage(error: unknown, t: Translate): string {
const offline = error instanceof NetworkError
|| error instanceof TypeError
|| !useNetworkStore.getState().online;
if (offline && !(error instanceof RequestTimeoutError)) {
return t(
'email_composer.send_offline',
'You seem to be offline. The message is still here, send it again once you are back online.',
);
}
return sendFailureMessage(error, t);
}
/**
* The title and message for a "Send now" or reschedule that failed: most
* failures get `failTitle` and sendFailureMessage (webmail
* rescheduleFailureToast). When the server refused recipients of the
* replacement that the original had been taken for, the original was kept
* for all of its recipients and still goes out at its old time,
* `scheduledAt` (formatted) when known. That leads, and nothing says "not
* sent": it would invite sending the message a second time.
*/
export function rescheduleFailureNotice(
error: unknown,
t: Translate,
failTitle: string,
scheduledAt?: string,
): { title: string; message: string } {
if (!(error instanceof RecipientsRefusedError)) return { title: failTitle, message: sendFailureMessage(error, t) };
return {
title: scheduledAt
? t('email_viewer.still_scheduled_at', 'The message is still scheduled for {date}.', { date: scheduledAt })
: t('email_viewer.still_scheduled', 'The message is still scheduled.'),
message: t('email_viewer.reschedule_recipients_refused', 'Nothing was changed: the server refused {recipients}.', {
recipients: formatRefusedRecipients(error.recipients),
}),
};
}
/**
* The warning after a send that the server took for some recipients only:
* nothing else, not even a bounce, would tell the user who never gets it.
* The addresses and replies go in the message, which a toast shows on more
* lines than its title.
*/
export function partlyRefusedWarning(recipients: RefusedRecipient[], t: Translate): { title: string; message: string } {
return {
title: t('email_composer.recipients_partly_refused_title', 'Not sent to everyone'),
message: t(
'email_composer.recipients_partly_refused',
'The server refused {recipients}. The other recipients get the message.',
{ recipients: formatRefusedRecipients(recipients) },
),
};
}