mirror of
https://github.com/root-fr/jmap-webmail.git
synced 2026-09-25 08:01:16 +00:00
fix(email): route outgoing mail through Drafts to avoid self-send dedup
Creating the outgoing Email directly in Sent before running EmailSubmission/set makes Stalwart's duplicate-message check trigger on inbound delivery when a user sends to themselves: the SMTP- delivered copy matches the local Sent copy's Message-ID and gets dropped, so the inbox stays empty. Keep the email in Drafts (or the sent mailbox as a fallback if no Drafts exists) during submission and let EmailSubmission's onSuccessUpdateEmail atomically move it to Sent — clearing the $draft keyword and setting $seen — only after the server has handed the outbound copy to SMTP. SMTP delivery for email-to-self now sees no existing Message-ID in the account and goes through. Applies to both the draft-send and the send-from-scratch paths. Closes #60.
This commit is contained in:
+33
-13
@@ -1324,6 +1324,7 @@ export class JMAPClient {
|
||||
if (!sentMailbox) {
|
||||
throw new Error('No sent mailbox found');
|
||||
}
|
||||
const draftsMailbox = mailboxes.find(mb => mb.role === 'drafts');
|
||||
|
||||
let finalIdentityId = identityId;
|
||||
if (!finalIdentityId) {
|
||||
@@ -1343,21 +1344,32 @@ export class JMAPClient {
|
||||
|
||||
const methodCalls: JMAPMethodCall[] = [];
|
||||
|
||||
// Stalwart's duplicate-message check (and many MDAs) consults the
|
||||
// sender's own account when delivering inbound mail. If we land the
|
||||
// outgoing copy in Sent before EmailSubmission runs, the subsequent
|
||||
// SMTP delivery for email-to-self matches the already-stored
|
||||
// Message-ID and gets dropped as a duplicate. Keep the email in the
|
||||
// Drafts mailbox (or any non-shared inbox proxy) during submission
|
||||
// and let EmailSubmission's onSuccessUpdateEmail move it to Sent
|
||||
// only after the server has handed the outbound copy to SMTP.
|
||||
// Closes #60.
|
||||
const holdingMailboxId = draftsMailbox?.id ?? sentMailbox.id;
|
||||
|
||||
if (draftId) {
|
||||
methodCalls.push(["Email/set", {
|
||||
accountId: this.accountId,
|
||||
update: {
|
||||
[draftId]: {
|
||||
"keywords/$draft": false,
|
||||
"keywords/$seen": true,
|
||||
mailboxIds: { [sentMailbox.id]: true },
|
||||
},
|
||||
},
|
||||
}, "0"]);
|
||||
// Draft already lives in Drafts — don't touch its mailbox until
|
||||
// after submission succeeds.
|
||||
methodCalls.push(["EmailSubmission/set", {
|
||||
accountId: this.accountId,
|
||||
create: { "1": { emailId: draftId, identityId: finalIdentityId } },
|
||||
}, "1"]);
|
||||
onSuccessUpdateEmail: {
|
||||
"#1": {
|
||||
[`mailboxIds/${holdingMailboxId}`]: null,
|
||||
[`mailboxIds/${sentMailbox.id}`]: true,
|
||||
"keywords/$draft": null,
|
||||
"keywords/$seen": true,
|
||||
},
|
||||
},
|
||||
}, "0"]);
|
||||
} else {
|
||||
methodCalls.push(["Email/set", {
|
||||
accountId: this.accountId,
|
||||
@@ -1368,8 +1380,8 @@ export class JMAPClient {
|
||||
cc: cc?.map(email => ({ email })),
|
||||
bcc: bcc?.map(email => ({ email })),
|
||||
subject,
|
||||
keywords: { "$seen": true },
|
||||
mailboxIds: { [sentMailbox.id]: true },
|
||||
keywords: { "$draft": true },
|
||||
mailboxIds: { [holdingMailboxId]: true },
|
||||
bodyValues: { "1": { value: body } },
|
||||
textBody: [{ partId: "1", type: "text/plain" }],
|
||||
},
|
||||
@@ -1378,6 +1390,14 @@ export class JMAPClient {
|
||||
methodCalls.push(["EmailSubmission/set", {
|
||||
accountId: this.accountId,
|
||||
create: { "1": { emailId: `#${emailId}`, identityId: finalIdentityId } },
|
||||
onSuccessUpdateEmail: {
|
||||
"#1": {
|
||||
[`mailboxIds/${holdingMailboxId}`]: null,
|
||||
[`mailboxIds/${sentMailbox.id}`]: true,
|
||||
"keywords/$draft": null,
|
||||
"keywords/$seen": true,
|
||||
},
|
||||
},
|
||||
}, "1"]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user