mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 08:06:36 +00:00
fix: keep the draft and say so when the server does not confirm a send
This commit is contained in:
@@ -334,6 +334,8 @@
|
||||
"send_now": "Send now",
|
||||
"send_timeout_body": "The message may already have gone out. Check your Sent folder before sending it again.",
|
||||
"send_timeout_title": "No answer from the server",
|
||||
"send_unconfirmed_body": "The server did not confirm the send. Check your Sent folder before sending again. Your draft has been kept.",
|
||||
"send_unconfirmed_title": "Send not confirmed",
|
||||
"submission_limit": "The server refused to send the message because a limit was reached: {reason}",
|
||||
"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.",
|
||||
"toolbar": {
|
||||
@@ -451,6 +453,7 @@
|
||||
"unsupported": "No preview for this file type."
|
||||
},
|
||||
"quick_reply_login_switched": "This message is in another account. Switch back to it to reply.",
|
||||
"quick_reply_unconfirmed": "The server did not confirm the send. Check your Sent folder before sending again.",
|
||||
"received_via": "to {name}",
|
||||
"remove_color": "Remove tag",
|
||||
"replied": "Replied",
|
||||
|
||||
@@ -17,7 +17,9 @@ vi.mock('../jmap-client', () => ({
|
||||
}));
|
||||
|
||||
import { jmapClient } from '../jmap-client';
|
||||
import { cancelScheduledSend, listScheduledEmails, rescheduleScheduledSend, sendEmail } from '../email';
|
||||
import {
|
||||
cancelScheduledSend, listScheduledEmails, rescheduleScheduledSend, sendEmail, SendUnconfirmedError,
|
||||
} from '../email';
|
||||
import { ScheduleTooLateError } from '../jmap-result';
|
||||
|
||||
const mockRequest = jmapClient.request as ReturnType<typeof vi.fn>;
|
||||
@@ -257,6 +259,22 @@ describe('sendEmail', () => {
|
||||
.rejects.toThrow('Invalid envelope');
|
||||
});
|
||||
|
||||
// #1019: an answer without the submission confirms nothing. The draft
|
||||
// stays, and so does the copy, which may be the one that went out.
|
||||
it('reports a send the server did not confirm as unconfirmed, and keeps the draft', async () => {
|
||||
mockRequest.mockResolvedValueOnce({
|
||||
methodResponses: [
|
||||
['Email/set', { created: { draft: { id: 'e-new' } } }, '0'],
|
||||
],
|
||||
});
|
||||
|
||||
await expect(sendEmail(OUTGOING, 'identity-1', 'sent-mb', undefined, {
|
||||
draftsMailboxId: 'drafts-mb', draftId: 'old-draft',
|
||||
})).rejects.toBeInstanceOf(SendUnconfirmedError);
|
||||
// No destroy of the old draft version, nor of the copy.
|
||||
expect(mockRequest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('still fails when the message could not be created', async () => {
|
||||
mockRequest.mockResolvedValueOnce({
|
||||
methodResponses: [
|
||||
|
||||
@@ -1650,6 +1650,18 @@ function submissionError(err: { description?: string; type?: string }, fallback:
|
||||
return new ScheduleTooLateError(limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* The send request came back without an EmailSubmission and without an
|
||||
* error: nothing confirms the message left, and it may have (webmail
|
||||
* SendUnconfirmedError, #1019). The draft is kept.
|
||||
*/
|
||||
export class SendUnconfirmedError extends Error {
|
||||
constructor() {
|
||||
super('Send confirmation was not received. Check Sent before sending again. Your draft has been kept.');
|
||||
this.name = 'SendUnconfirmedError';
|
||||
}
|
||||
}
|
||||
|
||||
export interface SendEmailOptions {
|
||||
/**
|
||||
* Drafts folder id. When given, the message is created there with `$draft`
|
||||
@@ -1804,6 +1816,14 @@ export async function sendEmail(
|
||||
}
|
||||
}
|
||||
|
||||
// No submission came back and nothing said why: an answer cut short, or a
|
||||
// server that skipped the call. Nothing confirms the message left, and it
|
||||
// may have. Reporting it as sent dropped the draft and closed the composer
|
||||
// (#1019); this keeps both, and the copy made for it too, since it may be
|
||||
// the one that went out.
|
||||
if (!failure && !emailSubmissionId) failure = new SendUnconfirmedError();
|
||||
if (failure instanceof SendUnconfirmedError) throw failure;
|
||||
|
||||
if (failure) {
|
||||
// Nothing went out. A message created before the submission was refused
|
||||
// must not stay behind: nobody tracked that copy, so a retry left a
|
||||
|
||||
@@ -11,7 +11,7 @@ import { toast } from '../../stores/toast-store';
|
||||
import { currentSendLogin, useSendUndoStore } from '../../stores/send-undo-store';
|
||||
import { useAccountStore } from '../../stores/account-store';
|
||||
import { loginMovedFrom } from '../../lib/compose-login';
|
||||
import { sendEmail, patchKeywordsForEmails } from '../../api/email';
|
||||
import { sendEmail, patchKeywordsForEmails, SendUnconfirmedError } from '../../api/email';
|
||||
import { jmapClient } from '../../api/jmap-client';
|
||||
import { pruneEmailSubmissions } from '../../api/submissions';
|
||||
import { buildReplyRecipients } from '../../lib/reply-recipients';
|
||||
@@ -167,6 +167,17 @@ export function QuickReplyBox({ email, jmapAccountId, onMoreOptions, onSent, onF
|
||||
toast.success(t('notifications.email_sent', 'Email sent successfully'));
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof SendUnconfirmedError) {
|
||||
// It may have gone out: the text stays, and Sent tells (#1019).
|
||||
Alert.alert(
|
||||
t('email_composer.send_unconfirmed_title', 'Send not confirmed'),
|
||||
t(
|
||||
'email_viewer.quick_reply_unconfirmed',
|
||||
'The server did not confirm the send. Check your Sent folder before sending again.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Alert.alert(t('email_composer.send_failed', 'Failed to send'), sendFailureMessage(err, t));
|
||||
} finally {
|
||||
setSending(false);
|
||||
|
||||
@@ -43,7 +43,8 @@ import { toast } from '../stores/toast-store';
|
||||
import { type EmailTemplate } from '../stores/templates-store';
|
||||
import { getIdentities } from '../api/identity';
|
||||
import {
|
||||
sendEmail, saveDraftVersion, destroyEmails, patchKeywordsForEmails, type OutgoingAttachment, type OutgoingEmail,
|
||||
sendEmail, saveDraftVersion, destroyEmails, patchKeywordsForEmails, SendUnconfirmedError,
|
||||
type OutgoingAttachment, type OutgoingEmail,
|
||||
} from '../api/email';
|
||||
import { jmapClient, RequestTimeoutError, ScheduleTooLateError } from '../api/jmap-client';
|
||||
import { pruneEmailSubmissions } from '../api/submissions';
|
||||
@@ -2299,6 +2300,18 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (e instanceof SendUnconfirmedError) {
|
||||
// The message may have gone out: the composer and the draft stay,
|
||||
// and the user checks Sent rather than sending it twice (#1019).
|
||||
Alert.alert(
|
||||
t('email_composer.send_unconfirmed_title', 'Send not confirmed'),
|
||||
t(
|
||||
'email_composer.send_unconfirmed_body',
|
||||
'The server did not confirm the send. Check your Sent folder before sending again. Your draft has been kept.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (e instanceof ScheduleTooLateError) {
|
||||
// The server refused the hold; the pickers now only offer times
|
||||
// within the limit it named.
|
||||
|
||||
Reference in New Issue
Block a user