fix: hide reply on drafts and scheduled messages in a conversation and quick reply to the newest real message (bulwarkmail/webmail#1404)

This commit is contained in:
Linus Rath
2026-10-31 03:59:16 +01:00
parent 9794dc03f7
commit 352973793f
5 changed files with 175 additions and 3 deletions
@@ -40,6 +40,7 @@ import {
queryEmailPage,
} from '../email';
import { senderWarningOf } from '../../lib/email-headers';
import { scheduledEmailIdsInThread } from '../thread-scheduled';
import { uploadBytes } from '../blob';
import { getIdentities, updateIdentity } from '../identity';
import { isJMAPSetError } from '../jmap-result';
@@ -447,5 +448,23 @@ live('live Stalwart: mail, contacts, search (0.16 and 1.0)', () => {
const [full] = await getFullEmails([id]);
expect(senderWarningOf(full)?.status).toBe('failed');
}, 60_000);
it("finds a conversation's message that waits for a scheduled send (bulwarkmail/webmail#1404)", async () => {
const [identity] = await getIdentities();
const result = await sendEmail(
{ from: [{ email: USER }], to: [{ email: USER }], subject: `scheduled reply ${mailTag}`, textBody: 'live' },
identity.id, sent, 3600, { draftsMailboxId: drafts },
);
try {
const [filed] = await getFullEmails([result.emailId!]);
const ids = await scheduledEmailIdsInThread(filed.threadId);
expect([...ids]).toEqual([result.emailId]);
} finally {
await cancelScheduledSend(result.emailSubmissionId!).catch(() => undefined);
}
// Once cancelled it can be replied to again.
const [filed] = await getFullEmails([result.emailId!]);
expect((await scheduledEmailIdsInThread(filed.threadId)).size).toBe(0);
}, 90_000);
});
});
+36
View File
@@ -0,0 +1,36 @@
import { jmapClient } from './jmap-client';
import { SUBMISSION_ONLY_USING } from './submissions';
/**
* Ids of a conversation's messages that wait for a scheduled send: a pending
* EmailSubmission whose send time is still ahead. The conversation hides
* Reply on them and points the quick reply past them, as it does for drafts
* (webmail 8b29254a6, #1404). Empty when the account can't schedule or the
* server can't answer: the cards then stay as they were.
*/
export async function scheduledEmailIdsInThread(threadId: string, accountId?: string): Promise<Set<string>> {
const account = accountId ?? jmapClient.accountId;
if (!account || !jmapClient.hasDelayedSend(account)) return new Set();
try {
const res = await jmapClient.request([
['EmailSubmission/query', { accountId: account, filter: { threadIds: [threadId], undoStatus: 'pending' } }, '0'],
['EmailSubmission/get', {
accountId: account,
'#ids': { resultOf: '0', name: 'EmailSubmission/query', path: '/ids' },
properties: ['emailId', 'sendAt', 'undoStatus'],
}, '1'],
], SUBMISSION_ONLY_USING);
const [name, body] = res.methodResponses?.[1] ?? [];
if (name !== 'EmailSubmission/get') return new Set();
const now = Date.now();
const ids = new Set<string>();
for (const s of (body.list as Array<{ emailId?: string; sendAt?: string; undoStatus?: string }>) ?? []) {
if (!s.emailId || s.undoStatus !== 'pending') continue;
const at = s.sendAt ? new Date(s.sendAt).getTime() : NaN;
if (Number.isFinite(at) && at > now) ids.add(s.emailId);
}
return ids;
} catch {
return new Set();
}
}