fix: stop Send now from dropping a group identity's delayed mail back into Drafts

This commit is contained in:
Linus Rath
2026-10-26 12:26:58 +01:00
parent 3508412b8b
commit d355e45b77
2 changed files with 31 additions and 8 deletions
+19 -1
View File
@@ -271,7 +271,10 @@ describe('JMAPClient.rescheduleEmailSubmission (#1308)', () => {
return { methodResponses: [[method, { list: [{ id: 'identity-1', email: 'user@example.com' }] }, '0']] };
}
if (method === 'EmailSubmission/get') {
return { methodResponses: [[method, { list: [{ id: 'sub-old', sendAt: originalSendAt, envelope: { rcptTo: [{ email: 'rcpt@example.com' }] } }] }, '0']] };
// Only the properties asked for, as the servers answer.
const wanted = (args.properties as string[] | undefined) ?? ['identityId', 'sendAt', 'envelope'];
const record: Record<string, unknown> = { id: 'sub-old', identityId: 'identity-1', sendAt: originalSendAt, envelope: { rcptTo: [{ email: 'rcpt@example.com' }] } };
return { methodResponses: [[method, { list: [Object.fromEntries(Object.entries(record).filter(([key]) => key === 'id' || wanted.includes(key)))] }, '0']] };
}
if (method === 'EmailSubmission/set' && args.update) {
return opts.cancel === 'fail'
@@ -317,6 +320,21 @@ describe('JMAPClient.rescheduleEmailSubmission (#1308)', () => {
expect(result).toMatchObject({ scheduled: true, emailSubmissionId: 'sub-new-1' });
});
// The undo toast's "Send now" passes the composer's identity, an id in the
// login's own account. A group identity's message is submitted in the group
// account, where that id names nothing (Stalwart 1.0: "Identity not found")
// or another identity, and the message dropped out of the schedule.
it('submits the replacement with the identity of the original submission', async () => {
const client = createClient();
const { calls, submissionSets } = scriptServer(client, { creates: ['ok'] });
await client.rescheduleEmailSubmission('sub-old', 'email-1', 'own-identity-of-group-address', inTenMinutes());
const create = submissionSets().find(c => c.args.create)?.args.create as Record<string, { identityId: string }>;
expect(create.replacement.identityId).toBe('identity-1');
expect(calls.find(c => c.method === 'Identity/get')?.args.ids).toEqual(['identity-1']);
});
it('changes nothing when the original cannot be canceled', async () => {
const client = createClient();
const { submissionSets } = scriptServer(client, { cancel: 'fail', creates: ['ok'] });
+12 -7
View File
@@ -6078,15 +6078,15 @@ export class JMAPClient implements IJMAPClient {
return submission?.sendAt;
}
private async getEmailSubmissionForReschedule(submissionId: string, accountId?: string): Promise<{ envelope?: { rcptTo?: Array<{ email: string }> }; sendAt?: string } | undefined> {
private async getEmailSubmissionForReschedule(submissionId: string, accountId?: string): Promise<{ identityId?: string; envelope?: { rcptTo?: Array<{ email: string }> }; sendAt?: string } | undefined> {
const response = await this.request([
['EmailSubmission/get', {
accountId: accountId ?? await this.resolveSubmissionAccountId(submissionId),
ids: [submissionId],
properties: ['envelope', 'sendAt'],
properties: ['identityId', 'envelope', 'sendAt'],
}, '0'],
]);
return response.methodResponses?.[0]?.[1]?.list?.[0] as { envelope?: { rcptTo?: Array<{ email: string }> }; sendAt?: string } | undefined;
return response.methodResponses?.[0]?.[1]?.list?.[0] as { identityId?: string; envelope?: { rcptTo?: Array<{ email: string }> }; sendAt?: string } | undefined;
}
/**
@@ -11058,6 +11058,12 @@ export class JMAPClient implements IJMAPClient {
const mailboxes = await this.getMailboxes(submissionAccountId);
const draftsMailbox = mailboxes.find(mb => mb.role === 'drafts');
const sentMailbox = mailboxes.find(mb => mb.role === 'sent');
const original = await this.getEmailSubmissionForReschedule(submissionId, submissionAccountId);
// The original's identity is one of the submission account's. The caller's
// may be the composer's, from the login's own account: for a group
// identity's message that id names nothing in the group account, or
// another identity.
const submissionIdentityId = original?.identityId || identityId;
// The identity and the Email live beside the submission, so look both up in
// the owning account. getIdentities() only knows the primary account's
// identities; a shared identity missing there would silently put the login
@@ -11065,14 +11071,13 @@ export class JMAPClient implements IJMAPClient {
let identity: Identity | undefined;
try {
const identityResponse = await this.request([
['Identity/get', { accountId: submissionAccountId, ids: [identityId], properties: ['id', 'email'] }, '0'],
['Identity/get', { accountId: submissionAccountId, ids: [submissionIdentityId], properties: ['id', 'email'] }, '0'],
]);
identity = ((identityResponse.methodResponses?.[0]?.[1]?.list ?? []) as Identity[])
.find(item => item.id === identityId);
.find(item => item.id === submissionIdentityId);
} catch (error) {
console.error('Failed to get identity for reschedule:', error);
}
const original = await this.getEmailSubmissionForReschedule(submissionId, submissionAccountId);
const existingEnvelope = original?.envelope;
const email = existingEnvelope?.rcptTo?.length ? undefined : await this.getEmail(emailId, submissionAccountId);
const envelopeRecipients = existingEnvelope?.rcptTo?.length
@@ -11091,7 +11096,7 @@ export class JMAPClient implements IJMAPClient {
return this.request([
['EmailSubmission/set', {
accountId: submissionAccountId,
create: { replacement: { emailId, identityId, ...(envelope ? { envelope } : {}) } },
create: { replacement: { emailId, identityId: submissionIdentityId, ...(envelope ? { envelope } : {}) } },
...(draftsMailbox && sentMailbox ? {
onSuccessUpdateEmail: {
'#replacement': {