fix: use onSuccessUpdateEmail to send before storing in Sent #188
This commit is contained in:
+73
-12
@@ -1783,6 +1783,10 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
if (!sentMailbox) {
|
if (!sentMailbox) {
|
||||||
throw new Error('No sent mailbox found');
|
throw new Error('No sent mailbox found');
|
||||||
}
|
}
|
||||||
|
const draftsMailbox = mailboxes.find(mb => mb.role === 'drafts');
|
||||||
|
if (!draftsMailbox) {
|
||||||
|
throw new Error('No drafts mailbox found');
|
||||||
|
}
|
||||||
|
|
||||||
let finalIdentityId = identityId;
|
let finalIdentityId = identityId;
|
||||||
let identityReplyTo: EmailAddress[] | undefined;
|
let identityReplyTo: EmailAddress[] | undefined;
|
||||||
@@ -1819,8 +1823,8 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
cc: cc?.map(email => ({ email })),
|
cc: cc?.map(email => ({ email })),
|
||||||
bcc: bcc?.map(email => ({ email })),
|
bcc: bcc?.map(email => ({ email })),
|
||||||
subject,
|
subject,
|
||||||
keywords: { "$seen": true },
|
keywords: { "$seen": true, "$draft": true },
|
||||||
mailboxIds: { [sentMailbox.id]: true },
|
mailboxIds: { [draftsMailbox.id]: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (htmlBody) {
|
if (htmlBody) {
|
||||||
@@ -1847,6 +1851,17 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
|
|
||||||
const methodCalls: JMAPMethodCall[] = [];
|
const methodCalls: JMAPMethodCall[] = [];
|
||||||
|
|
||||||
|
// Use onSuccessUpdateEmail to move from Drafts to Sent after submission.
|
||||||
|
// This ensures SMTP send happens before the email lands in Sent, avoiding
|
||||||
|
// issues with servers that encrypt on append (e.g. Stalwart). See #188.
|
||||||
|
const onSuccessUpdateEmail = {
|
||||||
|
"#1": {
|
||||||
|
[`mailboxIds/${draftsMailbox.id}`]: null,
|
||||||
|
[`mailboxIds/${sentMailbox.id}`]: true,
|
||||||
|
"keywords/$draft": null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if (draftId) {
|
if (draftId) {
|
||||||
// Destroy the old draft and create a new email with the final body
|
// Destroy the old draft and create a new email with the final body
|
||||||
methodCalls.push(["Email/set", {
|
methodCalls.push(["Email/set", {
|
||||||
@@ -1860,6 +1875,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
methodCalls.push(["EmailSubmission/set", {
|
methodCalls.push(["EmailSubmission/set", {
|
||||||
accountId: this.accountId,
|
accountId: this.accountId,
|
||||||
create: { "1": { emailId: `#${emailId}`, identityId: finalIdentityId } },
|
create: { "1": { emailId: `#${emailId}`, identityId: finalIdentityId } },
|
||||||
|
onSuccessUpdateEmail,
|
||||||
}, "2"]);
|
}, "2"]);
|
||||||
} else {
|
} else {
|
||||||
methodCalls.push(["Email/set", {
|
methodCalls.push(["Email/set", {
|
||||||
@@ -1869,6 +1885,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
methodCalls.push(["EmailSubmission/set", {
|
methodCalls.push(["EmailSubmission/set", {
|
||||||
accountId: this.accountId,
|
accountId: this.accountId,
|
||||||
create: { "1": { emailId: `#${emailId}`, identityId: finalIdentityId } },
|
create: { "1": { emailId: `#${emailId}`, identityId: finalIdentityId } },
|
||||||
|
onSuccessUpdateEmail,
|
||||||
}, "1"]);
|
}, "1"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1915,6 +1932,10 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
if (!sentMailbox) {
|
if (!sentMailbox) {
|
||||||
throw new Error('No sent mailbox found');
|
throw new Error('No sent mailbox found');
|
||||||
}
|
}
|
||||||
|
const draftsMailbox = mailboxes.find(mb => mb.role === 'drafts');
|
||||||
|
if (!draftsMailbox) {
|
||||||
|
throw new Error('No drafts mailbox found');
|
||||||
|
}
|
||||||
|
|
||||||
let finalIdentityId = opts.identityId;
|
let finalIdentityId = opts.identityId;
|
||||||
if (!finalIdentityId) {
|
if (!finalIdentityId) {
|
||||||
@@ -2015,8 +2036,8 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
from: [{ name: opts.attendeeName || undefined, email: opts.attendeeEmail }],
|
from: [{ name: opts.attendeeName || undefined, email: opts.attendeeEmail }],
|
||||||
to: [{ name: opts.organizerName || undefined, email: opts.organizerEmail }],
|
to: [{ name: opts.organizerName || undefined, email: opts.organizerEmail }],
|
||||||
subject,
|
subject,
|
||||||
keywords: { "$seen": true },
|
keywords: { "$seen": true, "$draft": true },
|
||||||
mailboxIds: { [sentMailbox.id]: true },
|
mailboxIds: { [draftsMailbox.id]: true },
|
||||||
bodyStructure: {
|
bodyStructure: {
|
||||||
type: 'multipart/alternative',
|
type: 'multipart/alternative',
|
||||||
subParts: [
|
subParts: [
|
||||||
@@ -2038,6 +2059,13 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
["EmailSubmission/set", {
|
["EmailSubmission/set", {
|
||||||
accountId: this.accountId,
|
accountId: this.accountId,
|
||||||
create: { "sub-1": { emailId: `#${emailId}`, identityId: finalIdentityId } },
|
create: { "sub-1": { emailId: `#${emailId}`, identityId: finalIdentityId } },
|
||||||
|
onSuccessUpdateEmail: {
|
||||||
|
"#sub-1": {
|
||||||
|
[`mailboxIds/${draftsMailbox.id}`]: null,
|
||||||
|
[`mailboxIds/${sentMailbox.id}`]: true,
|
||||||
|
"keywords/$draft": null,
|
||||||
|
},
|
||||||
|
},
|
||||||
}, "1"],
|
}, "1"],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -2076,6 +2104,10 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
if (!sentMailbox) {
|
if (!sentMailbox) {
|
||||||
throw new Error('No sent mailbox found');
|
throw new Error('No sent mailbox found');
|
||||||
}
|
}
|
||||||
|
const draftsMailbox = mailboxes.find(mb => mb.role === 'drafts');
|
||||||
|
if (!draftsMailbox) {
|
||||||
|
throw new Error('No drafts mailbox found');
|
||||||
|
}
|
||||||
|
|
||||||
// Find the organizer participant
|
// Find the organizer participant
|
||||||
const organizerEntry = Object.values(event.participants).find(p => p.roles?.owner);
|
const organizerEntry = Object.values(event.participants).find(p => p.roles?.owner);
|
||||||
@@ -2177,8 +2209,8 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
from: [{ name: organizerName || undefined, email: organizerEmail }],
|
from: [{ name: organizerName || undefined, email: organizerEmail }],
|
||||||
to: toAddresses,
|
to: toAddresses,
|
||||||
subject,
|
subject,
|
||||||
keywords: { "$seen": true },
|
keywords: { "$seen": true, "$draft": true },
|
||||||
mailboxIds: { [sentMailbox.id]: true },
|
mailboxIds: { [draftsMailbox.id]: true },
|
||||||
bodyStructure: {
|
bodyStructure: {
|
||||||
type: 'multipart/alternative',
|
type: 'multipart/alternative',
|
||||||
subParts: [
|
subParts: [
|
||||||
@@ -2200,6 +2232,13 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
["EmailSubmission/set", {
|
["EmailSubmission/set", {
|
||||||
accountId: this.accountId,
|
accountId: this.accountId,
|
||||||
create: { "sub-1": { emailId: `#${emailId}`, identityId } },
|
create: { "sub-1": { emailId: `#${emailId}`, identityId } },
|
||||||
|
onSuccessUpdateEmail: {
|
||||||
|
"#sub-1": {
|
||||||
|
[`mailboxIds/${draftsMailbox.id}`]: null,
|
||||||
|
[`mailboxIds/${sentMailbox.id}`]: true,
|
||||||
|
"keywords/$draft": null,
|
||||||
|
},
|
||||||
|
},
|
||||||
}, "1"],
|
}, "1"],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -2233,6 +2272,10 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
if (!sentMailbox) {
|
if (!sentMailbox) {
|
||||||
throw new Error('No sent mailbox found');
|
throw new Error('No sent mailbox found');
|
||||||
}
|
}
|
||||||
|
const draftsMailbox = mailboxes.find(mb => mb.role === 'drafts');
|
||||||
|
if (!draftsMailbox) {
|
||||||
|
throw new Error('No drafts mailbox found');
|
||||||
|
}
|
||||||
|
|
||||||
const organizerEntry = Object.values(event.participants).find(p => p.roles?.owner);
|
const organizerEntry = Object.values(event.participants).find(p => p.roles?.owner);
|
||||||
const organizerEmail = organizerEntry?.email || organizerEntry?.sendTo?.imip?.replace('mailto:', '') || this.username;
|
const organizerEmail = organizerEntry?.email || organizerEntry?.sendTo?.imip?.replace('mailto:', '') || this.username;
|
||||||
@@ -2313,8 +2356,8 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
from: [{ name: organizerName || undefined, email: organizerEmail }],
|
from: [{ name: organizerName || undefined, email: organizerEmail }],
|
||||||
to: toAddresses,
|
to: toAddresses,
|
||||||
subject,
|
subject,
|
||||||
keywords: { "$seen": true },
|
keywords: { "$seen": true, "$draft": true },
|
||||||
mailboxIds: { [sentMailbox.id]: true },
|
mailboxIds: { [draftsMailbox.id]: true },
|
||||||
bodyStructure: {
|
bodyStructure: {
|
||||||
type: 'multipart/alternative',
|
type: 'multipart/alternative',
|
||||||
subParts: [
|
subParts: [
|
||||||
@@ -2336,6 +2379,13 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
["EmailSubmission/set", {
|
["EmailSubmission/set", {
|
||||||
accountId: this.accountId,
|
accountId: this.accountId,
|
||||||
create: { "sub-1": { emailId: `#${emailId}`, identityId } },
|
create: { "sub-1": { emailId: `#${emailId}`, identityId } },
|
||||||
|
onSuccessUpdateEmail: {
|
||||||
|
"#sub-1": {
|
||||||
|
[`mailboxIds/${draftsMailbox.id}`]: null,
|
||||||
|
[`mailboxIds/${sentMailbox.id}`]: true,
|
||||||
|
"keywords/$draft": null,
|
||||||
|
},
|
||||||
|
},
|
||||||
}, "1"],
|
}, "1"],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -4747,21 +4797,23 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
blob: Blob,
|
blob: Blob,
|
||||||
identityId: string,
|
identityId: string,
|
||||||
sentMailboxId: string,
|
sentMailboxId: string,
|
||||||
_draftMailboxId?: string,
|
draftMailboxId?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Upload the raw message
|
// Upload the raw message
|
||||||
const file = new File([blob], 'message.eml', { type: 'message/rfc822' });
|
const file = new File([blob], 'message.eml', { type: 'message/rfc822' });
|
||||||
const { blobId } = await this.uploadBlob(file);
|
const { blobId } = await this.uploadBlob(file);
|
||||||
|
|
||||||
// Import into Sent, mark as seen, and submit — all in one request
|
// Import into Drafts first, then move to Sent after submission succeeds.
|
||||||
|
// This avoids encrypt-on-append affecting the SMTP send. See #188.
|
||||||
|
const importMailboxId = draftMailboxId || sentMailboxId;
|
||||||
const methodCalls: [string, Record<string, unknown>, string][] = [
|
const methodCalls: [string, Record<string, unknown>, string][] = [
|
||||||
['Email/import', {
|
['Email/import', {
|
||||||
accountId: this.accountId,
|
accountId: this.accountId,
|
||||||
emails: {
|
emails: {
|
||||||
'raw-import': {
|
'raw-import': {
|
||||||
blobId,
|
blobId,
|
||||||
mailboxIds: { [sentMailboxId]: true },
|
mailboxIds: { [importMailboxId]: true },
|
||||||
keywords: { '$seen': true },
|
keywords: draftMailboxId ? { '$seen': true, '$draft': true } : { '$seen': true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, '0'],
|
}, '0'],
|
||||||
@@ -4773,6 +4825,15 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
identityId,
|
identityId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...(draftMailboxId ? {
|
||||||
|
onSuccessUpdateEmail: {
|
||||||
|
'#raw-submit': {
|
||||||
|
[`mailboxIds/${draftMailboxId}`]: null,
|
||||||
|
[`mailboxIds/${sentMailboxId}`]: true,
|
||||||
|
'keywords/$draft': null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} : {}),
|
||||||
}, '1'],
|
}, '1'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -453,7 +453,8 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
const mailboxes = await client.getMailboxes();
|
const mailboxes = await client.getMailboxes();
|
||||||
const sentMailbox = mailboxes.find(mb => mb.role === 'sent');
|
const sentMailbox = mailboxes.find(mb => mb.role === 'sent');
|
||||||
if (!sentMailbox) throw new Error('No sent mailbox found');
|
if (!sentMailbox) throw new Error('No sent mailbox found');
|
||||||
await client.sendRawEmail(rawMimeBlob, identityId, sentMailbox.id);
|
const draftsMailbox = mailboxes.find(mb => mb.role === 'drafts');
|
||||||
|
await client.sendRawEmail(rawMimeBlob, identityId, sentMailbox.id, draftsMailbox?.id);
|
||||||
set({ isLoading: false });
|
set({ isLoading: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
|
|||||||
Reference in New Issue
Block a user