From 1a3d359fee7db93c2c170840aac20cbd30d071df Mon Sep 17 00:00:00 2001 From: Lucas Gaitzsch Date: Wed, 20 May 2026 08:28:31 +0200 Subject: [PATCH] fix bugs --- app/[locale]/page.tsx | 4 +- app/api/dev-jmap/[...path]/route.ts | 2 +- lib/jmap/client.ts | 78 +++++++++++++++++------------ stores/email-store.ts | 7 ++- stores/settings-store.ts | 1 - 5 files changed, 53 insertions(+), 39 deletions(-) diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index ae61a96f..b46607c0 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -2593,7 +2593,7 @@ export default function Home() { isScheduledView={isScheduledView} onLoadMoreScheduled={() => client && loadMoreScheduledEmails(client)} onCancelScheduled={async (email) => { - if (client && email.emailSubmissionId) await cancelScheduledEmail(client, email.emailSubmissionId); + if (client && email.emailSubmissionId) await cancelScheduledEmail(client, email.emailSubmissionId, email.id); }} onCancelScheduledForEdit={async (email) => { if (!client) return; @@ -2878,7 +2878,7 @@ export default function Home() { onShowShortcuts={() => setShowShortcutsModal(true)} onEditDraft={handleEditDraft} onCancelScheduled={async () => { - if (client && selectedEmail?.emailSubmissionId) await cancelScheduledEmail(client, selectedEmail.emailSubmissionId); + if (client && selectedEmail?.emailSubmissionId) await cancelScheduledEmail(client, selectedEmail.emailSubmissionId, selectedEmail.id); }} onCancelScheduledForEdit={async () => { if (!client || !selectedEmail) return; diff --git a/app/api/dev-jmap/[...path]/route.ts b/app/api/dev-jmap/[...path]/route.ts index c4271bc7..366e4945 100644 --- a/app/api/dev-jmap/[...path]/route.ts +++ b/app/api/dev-jmap/[...path]/route.ts @@ -1822,7 +1822,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ isReadOnly: false, accountCapabilities: { 'urn:ietf:params:jmap:mail': {}, - 'urn:ietf:params:jmap:submission': { maxDelayedSend: 2592000 }, + 'urn:ietf:params:jmap:submission': { maxDelayedSend: 2592000, submissionExtensions: { FUTURERELEASE: true } }, 'urn:ietf:params:jmap:quota': {}, 'urn:ietf:params:jmap:vacationresponse': {}, 'urn:ietf:params:jmap:contacts': {}, diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index 23aa149b..38501f3e 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -5582,43 +5582,56 @@ export class JMAPClient implements IJMAPClient { return { emails: [], hasMore: false, total: 0 }; } - const queryResponse = await this.request([ - ['EmailSubmission/query', { - accountId: this.getSubmissionAccountId(), - limit, - position, - }, '0'], - ]); - - const query = queryResponse.methodResponses?.[0]?.[1] as { ids?: string[]; total?: number; position?: number } | undefined; - const ids = query?.ids ?? []; - if (ids.length === 0) { - return { emails: [], hasMore: false, total: query?.total ?? 0 }; - } - - const submissionResponse = await this.request([ - ['EmailSubmission/get', { - accountId: this.getSubmissionAccountId(), - ids, - properties: ['id', 'emailId', 'identityId', 'threadId', 'sendAt', 'undoStatus', 'deliveryStatus'], - }, '0'], - ]); const now = Date.now(); - const submissions = ((submissionResponse.methodResponses?.[0]?.[1]?.list ?? []) as EmailSubmission[]) - .filter(submission => { - if (!submission.sendAt) return false; - const sendAtTime = new Date(submission.sendAt).getTime(); - return Number.isFinite(sendAtTime) && sendAtTime > now; - }); + const pageSize = Math.max(limit, 50); + const submissions: EmailSubmission[] = []; + let rawPosition = 0; + let rawTotal = 0; - if (submissions.length === 0) { - return { emails: [], hasMore: false, total: query?.total ?? 0 }; + do { + const queryResponse = await this.request([ + ['EmailSubmission/query', { + accountId: this.getSubmissionAccountId(), + limit: pageSize, + position: rawPosition, + }, '0'], + ]); + + const query = queryResponse.methodResponses?.[0]?.[1] as { ids?: string[]; total?: number; position?: number } | undefined; + const ids = query?.ids ?? []; + rawTotal = query?.total ?? rawPosition + ids.length; + if (ids.length === 0) break; + + const submissionResponse = await this.request([ + ['EmailSubmission/get', { + accountId: this.getSubmissionAccountId(), + ids, + properties: ['id', 'emailId', 'identityId', 'threadId', 'sendAt', 'undoStatus', 'deliveryStatus'], + }, '0'], + ]); + + submissions.push(...((submissionResponse.methodResponses?.[0]?.[1]?.list ?? []) as EmailSubmission[]) + .filter(submission => { + if (submission.undoStatus !== 'pending' || !submission.sendAt) return false; + const sendAtTime = new Date(submission.sendAt).getTime(); + return Number.isFinite(sendAtTime) && sendAtTime > now; + })); + + rawPosition += ids.length; + } while (rawPosition < rawTotal); + + submissions.sort((a, b) => new Date(a.sendAt || '').getTime() - new Date(b.sendAt || '').getTime()); + const total = submissions.length; + const pageSubmissions = submissions.slice(position, position + limit); + + if (pageSubmissions.length === 0) { + return { emails: [], hasMore: false, total }; } const emailResponse = await this.request([ ['Email/get', { accountId: this.accountId, - ids: submissions.map(submission => submission.emailId), + ids: pageSubmissions.map(submission => submission.emailId), properties: [ 'id', 'threadId', 'mailboxIds', 'keywords', 'size', 'receivedAt', 'from', 'to', 'cc', 'bcc', 'replyTo', 'subject', 'preview', 'textBody', 'htmlBody', 'bodyValues', 'attachments', 'hasAttachment', 'sentAt', @@ -5632,7 +5645,7 @@ export class JMAPClient implements IJMAPClient { ]); const emailById = new Map(((emailResponse.methodResponses?.[0]?.[1]?.list ?? []) as Email[]).map(email => [email.id, email])); - const emails = submissions + const emails = pageSubmissions .map((submission): ScheduledEmail | null => { const email = emailById.get(submission.emailId); if (!email || !submission.sendAt) return null; @@ -5651,8 +5664,7 @@ export class JMAPClient implements IJMAPClient { .filter((email): email is ScheduledEmail => email !== null) .sort((a, b) => new Date(a.scheduledSendAt).getTime() - new Date(b.scheduledSendAt).getTime()); - const total = query?.total ?? emails.length; - return { emails, hasMore: computeHasMore(position, ids.length, total, limit), total }; + return { emails, hasMore: position + emails.length < total, total }; } async cancelEmailSubmission(submissionId: string): Promise { diff --git a/stores/email-store.ts b/stores/email-store.ts index 7ea68221..8a841cde 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -157,7 +157,7 @@ interface EmailStore { fetchScheduledEmails: (client: IJMAPClient) => Promise; loadMoreScheduledEmails: (client: IJMAPClient) => Promise; - cancelScheduledEmail: (client: IJMAPClient, submissionId: string) => Promise; + cancelScheduledEmail: (client: IJMAPClient, submissionId: string, emailId?: string) => Promise; cancelScheduledEmailForEdit: (client: IJMAPClient, email: ScheduledEmail | Email) => Promise; rescheduleScheduledEmail: (client: IJMAPClient, submissionId: string, emailId: string, identityId: string, delayedUntil: string) => Promise; cancelUndoSend: (client: IJMAPClient, pending: PendingUndoSend) => Promise; @@ -2182,8 +2182,11 @@ export const useEmailStore = create((set, get) => ({ } }, - cancelScheduledEmail: async (client, submissionId) => { + cancelScheduledEmail: async (client, submissionId, emailId) => { await client.cancelEmailSubmission(submissionId); + if (emailId) { + await client.deleteEmail(emailId); + } if (get().pendingUndoSend?.submissionId === submissionId) { set({ pendingUndoSend: null }); } diff --git a/stores/settings-store.ts b/stores/settings-store.ts index 0322a85a..b7d796c1 100644 --- a/stores/settings-store.ts +++ b/stores/settings-store.ts @@ -494,7 +494,6 @@ export const useSettingsStore = create()( plainTextMode: state.plainTextMode, subAddressDelimiter: state.subAddressDelimiter, sendDelaySeconds: state.sendDelaySeconds, - sendDelaySeconds: state.sendDelaySeconds, signaturePosition: state.signaturePosition, signatureSeparatorEnabled: state.signatureSeparatorEnabled, sessionTimeout: state.sessionTimeout,