mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-05 16:08:17 +00:00
"Send now" and "Reschedule" create a replacement submission and then
cancel the original. The server checks the recipients again for the
replacement, and a throttle used up since the original was taken ("452
4.4.5 Rate limit exceeded") can refuse some of them. The app kept the
original only when every recipient was refused; on a partial refusal it
cancelled it, and a recipient the original had taken lost the message.
Port of the webmail's aa99f038.
- the original's delivery status is read with its envelope, in the same
request; when the replacement refuses a recipient the original took,
the replacement is withdrawn (cancelled, or only its record destroyed
when it took nobody) and the original goes at its old time to
everyone it took
- a recipient the original was refused for as well loses nothing, so
the reschedule goes ahead and names it; without a delivery status of
the original, every recipient counts as taken
- a replacement that can't be withdrawn lets the reschedule go ahead,
naming the refused recipients: "Send now" holds it for a second only,
so it may be out already, and keeping both would send the others two
copies; a withdraw whose answer got lost is read back, and when it
went through the original is kept, since going ahead would reach
nobody
- the alert in Scheduled and the undo bar's toast lead with "The
message is still scheduled for ..." and say "Nothing was changed: the
server refused ..."; they used to open with "Not sent", which invited
sending the message a second time
Checked live on 0.16.23 and 1.0 with a relay-refused recipient: the
lookup, going ahead, and withdrawing a partly refused replacement while
the original stays at its old time. One new string, with the webmail's
translations in all 27 languages.
100 lines
5.6 KiB
TypeScript
100 lines
5.6 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { RequestTimeoutError } from '../../api/jmap-client';
|
|
import {
|
|
JMAPMethodError, NoLongerScheduledError, RecipientsRefusedError, ScheduleTooLateError, SubmissionQuotaError,
|
|
formatRefusedRecipients,
|
|
} from '../../api/jmap-result';
|
|
import { partlyRefusedWarning, rescheduleFailureNotice, sendFailureMessage } from '../send-failure-message';
|
|
|
|
// The English fallback stands in for the catalog.
|
|
const t = (key: string, fallback?: string, params?: Record<string, unknown>) =>
|
|
(fallback ?? key).replace(/\{(\w+)\}/g, (_, name: string) => String(params?.[name] ?? `{${name}}`));
|
|
|
|
describe('sendFailureMessage', () => {
|
|
it('explains a full submission quota instead of showing Stalwart 1.0\'s "please delete some"', () => {
|
|
const quota = new SubmissionQuotaError(
|
|
'There are too many email submissions, please delete some before adding a new one.',
|
|
true,
|
|
);
|
|
expect(quota).toBeInstanceOf(JMAPMethodError);
|
|
expect(quota.type).toBe('overQuota');
|
|
expect(sendFailureMessage(quota, t)).toBe(
|
|
'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.',
|
|
);
|
|
});
|
|
|
|
it('points at Sent after a timeout, and names the hold limit', () => {
|
|
expect(sendFailureMessage(new RequestTimeoutError(30_000), t)).toMatch(/Check your Sent folder/);
|
|
expect(sendFailureMessage(new ScheduleTooLateError(172_800), t)).toMatch(/later than this server allows/);
|
|
});
|
|
|
|
it("keeps the server's own words for anything else", () => {
|
|
expect(sendFailureMessage(new Error('Not your address'), t)).toBe('Not your address');
|
|
// A full mailbox is Email/set's overQuota, not the submission quota.
|
|
expect(sendFailureMessage(new JMAPMethodError('overQuota', 'Account quota exceeded.'), t)).toBe('Account quota exceeded.');
|
|
expect(sendFailureMessage('boom', t)).toBe('Failed to send email');
|
|
});
|
|
});
|
|
|
|
describe('sendFailureMessage beyond Stalwart and around a reschedule', () => {
|
|
it('gives another server\'s overQuota a generic sentence with its own words (no urn:stalwart:jmap)', () => {
|
|
expect(sendFailureMessage(new SubmissionQuotaError('Daily sending limit reached', false), t))
|
|
.toBe('The server refused to send the message because a limit was reached: Daily sending limit reached');
|
|
expect(sendFailureMessage(new SubmissionQuotaError(undefined, false), t))
|
|
.toBe('The server refused to send the message because a limit was reached: overQuota');
|
|
});
|
|
|
|
it('says a rescheduled message already went out, or is no longer scheduled', () => {
|
|
expect(sendFailureMessage(new NoLongerScheduledError(true), t)).toBe('This message was already sent.');
|
|
expect(sendFailureMessage(new NoLongerScheduledError(false), t)).toBe('This message is no longer scheduled.');
|
|
});
|
|
});
|
|
|
|
// Stalwart 0.16 and 1.0 take the submission and refuse recipients at RCPT,
|
|
// each with its own reply.
|
|
describe('recipients the server refused', () => {
|
|
const refused = new RecipientsRefusedError([
|
|
{ email: 'nobody@example.net', smtpReply: '550 5.1.2 Relay not allowed.' },
|
|
{ email: 'busy@example.org', smtpReply: '452 4.4.5 Rate limit exceeded, try again later.' },
|
|
]);
|
|
|
|
it('says the message was not sent and names every refused recipient with its reply', () => {
|
|
expect(sendFailureMessage(refused, t)).toBe(
|
|
'Not sent: the server refused nobody@example.net (550 5.1.2 Relay not allowed.), busy@example.org (452 4.4.5 Rate limit exceeded, try again later.).',
|
|
);
|
|
});
|
|
|
|
it('lists an address without its reply when the server gave none', () => {
|
|
expect(formatRefusedRecipients([{ email: 'a@example.net' }, { email: 'b@example.net', smtpReply: '550 5.1.1 No such user' }]))
|
|
.toBe('a@example.net, b@example.net (550 5.1.1 No such user)');
|
|
});
|
|
|
|
// The original is kept for all of its recipients, so the headline must not
|
|
// read as "not sent", which invites sending the message a second time.
|
|
it('leads with the kept schedule when the server refused recipients of the replacement (Stalwart 0.16 and 1.0)', () => {
|
|
const error = new RecipientsRefusedError([{ email: 'bob@example.org', smtpReply: '452 4.4.5 Rate limit exceeded, try again later.' }]);
|
|
expect(rescheduleFailureNotice(error, t, 'Reschedule failed', 'Sep 26, 10:00')).toEqual({
|
|
title: 'The message is still scheduled for Sep 26, 10:00.',
|
|
message: 'Nothing was changed: the server refused bob@example.org (452 4.4.5 Rate limit exceeded, try again later.).',
|
|
});
|
|
expect(rescheduleFailureNotice(error, t, 'Could not send now')).toEqual({
|
|
title: 'The message is still scheduled.',
|
|
message: 'Nothing was changed: the server refused bob@example.org (452 4.4.5 Rate limit exceeded, try again later.).',
|
|
});
|
|
});
|
|
|
|
it('gives other reschedule failures their title and the send failure message', () => {
|
|
expect(rescheduleFailureNotice(new NoLongerScheduledError(true), t, 'Reschedule failed', 'Sep 26, 10:00'))
|
|
.toEqual({ title: 'Reschedule failed', message: 'This message was already sent.' });
|
|
expect(rescheduleFailureNotice(new Error('Quota exceeded'), t, 'Could not send now'))
|
|
.toEqual({ title: 'Could not send now', message: 'Quota exceeded' });
|
|
});
|
|
|
|
it('warns about the recipients refused while the others get the message', () => {
|
|
expect(partlyRefusedWarning([{ email: 'nobody@example.net', smtpReply: '550 5.1.2 Relay not allowed.' }], t)).toEqual({
|
|
title: 'Not sent to everyone',
|
|
message: 'The server refused nobody@example.net (550 5.1.2 Relay not allowed.). The other recipients get the message.',
|
|
});
|
|
});
|
|
});
|