Fix: stop resurrecting deleted rows in the mailbox refresh merge
Fixes #592. refreshCurrentMailbox merges the refreshed first page with the already loaded list, appending existing entries beyond a cutoff. That cutoff was derived from the refreshed list's length - so whenever a folder shrank, the fresh page was shorter than the stale list and the loop re-appended the deleted rows from stale local state, despite the comment right above promising the opposite. The visible result is the reported bug: after sending a draft, the Drafts view keeps showing a ghost row for the already-destroyed draft. The send actually succeeded - resending the ghost delivers the mail again, which we reproduced with a live JMAP trace: four successful submissions, an empty server-side Drafts folder, a notFound ghost id, and five delivered copies. Deriving the cutoff from the page size fixes the shrink case while preserving the merge's intent for arrivals and loaded deeper pages; regression tests cover all three shapes. Also surface post-send filing failures instead of dropping them, as flagged in 4dc76bbb's follow-up note: a rejected onSuccessUpdateEmail patch or old-draft destroy now logs the server's error details and returns a filingError on SendEmailResult, and the UI shows a warning toast (all 23 locales) so a stale draft row is never again mistaken for a failed send. A plugin veto of the send leaves a debug trace.
This commit is contained in:
+21
-2
@@ -2707,6 +2707,7 @@ export class JMAPClient implements IJMAPClient {
|
||||
let createdEmailId: string | undefined;
|
||||
let emailSubmissionId: string | undefined;
|
||||
let serverSendAt: string | undefined;
|
||||
let filingError: string | undefined;
|
||||
|
||||
if (response.methodResponses) {
|
||||
for (const [methodName, result] of response.methodResponses) {
|
||||
@@ -2740,6 +2741,24 @@ export class JMAPClient implements IJMAPClient {
|
||||
);
|
||||
}
|
||||
|
||||
// Post-submission filing problems (the implicit Email/set from
|
||||
// onSuccessUpdateEmail, or destroying the old draft) must not fail
|
||||
// the send - the message already left - but they must not stay
|
||||
// silent either: a silently rejected filing/cleanup is exactly how
|
||||
// "sent mail still sits in Drafts" reports look (#592, #588's
|
||||
// sibling note in 4dc76bbb). Log the details and surface a warning
|
||||
// to the caller.
|
||||
if (result.notUpdated && Object.keys(result.notUpdated).length) {
|
||||
console.error(`[sendEmail] ${methodName} notUpdated:`, JSON.stringify(result.notUpdated, null, 2));
|
||||
const first = Object.values(result.notUpdated as Record<string, { type?: string; description?: string }>)[0];
|
||||
filingError = filingError ?? (first?.description || first?.type || 'post-send filing failed');
|
||||
}
|
||||
if (result.notDestroyed && Object.keys(result.notDestroyed).length) {
|
||||
console.error(`[sendEmail] ${methodName} notDestroyed (old draft):`, JSON.stringify(result.notDestroyed, null, 2));
|
||||
const first = Object.values(result.notDestroyed as Record<string, { type?: string; description?: string }>)[0];
|
||||
filingError = filingError ?? (first?.description || first?.type || 'old draft cleanup failed');
|
||||
}
|
||||
|
||||
if (methodName === 'Email/set' && result.created?.[emailId]?.id) {
|
||||
createdEmailId = result.created[emailId].id;
|
||||
}
|
||||
@@ -2755,8 +2774,8 @@ export class JMAPClient implements IJMAPClient {
|
||||
}
|
||||
|
||||
return delayedUntil
|
||||
? { scheduled: true, emailId: createdEmailId, emailSubmissionId, sendAt: serverSendAt }
|
||||
: { scheduled: false, emailId: createdEmailId, emailSubmissionId };
|
||||
? { scheduled: true, emailId: createdEmailId, emailSubmissionId, sendAt: serverSendAt, filingError }
|
||||
: { scheduled: false, emailId: createdEmailId, emailSubmissionId, filingError };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user