This commit is contained in:
Lucas Gaitzsch
2026-05-20 08:28:31 +02:00
parent 891c3250be
commit 1a3d359fee
5 changed files with 53 additions and 39 deletions
+2 -2
View File
@@ -2593,7 +2593,7 @@ export default function Home() {
isScheduledView={isScheduledView} isScheduledView={isScheduledView}
onLoadMoreScheduled={() => client && loadMoreScheduledEmails(client)} onLoadMoreScheduled={() => client && loadMoreScheduledEmails(client)}
onCancelScheduled={async (email) => { 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) => { onCancelScheduledForEdit={async (email) => {
if (!client) return; if (!client) return;
@@ -2878,7 +2878,7 @@ export default function Home() {
onShowShortcuts={() => setShowShortcutsModal(true)} onShowShortcuts={() => setShowShortcutsModal(true)}
onEditDraft={handleEditDraft} onEditDraft={handleEditDraft}
onCancelScheduled={async () => { onCancelScheduled={async () => {
if (client && selectedEmail?.emailSubmissionId) await cancelScheduledEmail(client, selectedEmail.emailSubmissionId); if (client && selectedEmail?.emailSubmissionId) await cancelScheduledEmail(client, selectedEmail.emailSubmissionId, selectedEmail.id);
}} }}
onCancelScheduledForEdit={async () => { onCancelScheduledForEdit={async () => {
if (!client || !selectedEmail) return; if (!client || !selectedEmail) return;
+1 -1
View File
@@ -1822,7 +1822,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
isReadOnly: false, isReadOnly: false,
accountCapabilities: { accountCapabilities: {
'urn:ietf:params:jmap:mail': {}, '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:quota': {},
'urn:ietf:params:jmap:vacationresponse': {}, 'urn:ietf:params:jmap:vacationresponse': {},
'urn:ietf:params:jmap:contacts': {}, 'urn:ietf:params:jmap:contacts': {},
+45 -33
View File
@@ -5582,43 +5582,56 @@ export class JMAPClient implements IJMAPClient {
return { emails: [], hasMore: false, total: 0 }; 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 now = Date.now();
const submissions = ((submissionResponse.methodResponses?.[0]?.[1]?.list ?? []) as EmailSubmission[]) const pageSize = Math.max(limit, 50);
.filter(submission => { const submissions: EmailSubmission[] = [];
if (!submission.sendAt) return false; let rawPosition = 0;
const sendAtTime = new Date(submission.sendAt).getTime(); let rawTotal = 0;
return Number.isFinite(sendAtTime) && sendAtTime > now;
});
if (submissions.length === 0) { do {
return { emails: [], hasMore: false, total: query?.total ?? 0 }; 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([ const emailResponse = await this.request([
['Email/get', { ['Email/get', {
accountId: this.accountId, accountId: this.accountId,
ids: submissions.map(submission => submission.emailId), ids: pageSubmissions.map(submission => submission.emailId),
properties: [ properties: [
'id', 'threadId', 'mailboxIds', 'keywords', 'size', 'receivedAt', 'from', 'to', 'cc', 'bcc', 'replyTo', 'id', 'threadId', 'mailboxIds', 'keywords', 'size', 'receivedAt', 'from', 'to', 'cc', 'bcc', 'replyTo',
'subject', 'preview', 'textBody', 'htmlBody', 'bodyValues', 'attachments', 'hasAttachment', 'sentAt', '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 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 => { .map((submission): ScheduledEmail | null => {
const email = emailById.get(submission.emailId); const email = emailById.get(submission.emailId);
if (!email || !submission.sendAt) return null; if (!email || !submission.sendAt) return null;
@@ -5651,8 +5664,7 @@ export class JMAPClient implements IJMAPClient {
.filter((email): email is ScheduledEmail => email !== null) .filter((email): email is ScheduledEmail => email !== null)
.sort((a, b) => new Date(a.scheduledSendAt).getTime() - new Date(b.scheduledSendAt).getTime()); .sort((a, b) => new Date(a.scheduledSendAt).getTime() - new Date(b.scheduledSendAt).getTime());
const total = query?.total ?? emails.length; return { emails, hasMore: position + emails.length < total, total };
return { emails, hasMore: computeHasMore(position, ids.length, total, limit), total };
} }
async cancelEmailSubmission(submissionId: string): Promise<void> { async cancelEmailSubmission(submissionId: string): Promise<void> {
+5 -2
View File
@@ -157,7 +157,7 @@ interface EmailStore {
fetchScheduledEmails: (client: IJMAPClient) => Promise<void>; fetchScheduledEmails: (client: IJMAPClient) => Promise<void>;
loadMoreScheduledEmails: (client: IJMAPClient) => Promise<void>; loadMoreScheduledEmails: (client: IJMAPClient) => Promise<void>;
cancelScheduledEmail: (client: IJMAPClient, submissionId: string) => Promise<void>; cancelScheduledEmail: (client: IJMAPClient, submissionId: string, emailId?: string) => Promise<void>;
cancelScheduledEmailForEdit: (client: IJMAPClient, email: ScheduledEmail | Email) => Promise<Email | null>; cancelScheduledEmailForEdit: (client: IJMAPClient, email: ScheduledEmail | Email) => Promise<Email | null>;
rescheduleScheduledEmail: (client: IJMAPClient, submissionId: string, emailId: string, identityId: string, delayedUntil: string) => Promise<SendEmailResult>; rescheduleScheduledEmail: (client: IJMAPClient, submissionId: string, emailId: string, identityId: string, delayedUntil: string) => Promise<SendEmailResult>;
cancelUndoSend: (client: IJMAPClient, pending: PendingUndoSend) => Promise<Email | null>; cancelUndoSend: (client: IJMAPClient, pending: PendingUndoSend) => Promise<Email | null>;
@@ -2182,8 +2182,11 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
} }
}, },
cancelScheduledEmail: async (client, submissionId) => { cancelScheduledEmail: async (client, submissionId, emailId) => {
await client.cancelEmailSubmission(submissionId); await client.cancelEmailSubmission(submissionId);
if (emailId) {
await client.deleteEmail(emailId);
}
if (get().pendingUndoSend?.submissionId === submissionId) { if (get().pendingUndoSend?.submissionId === submissionId) {
set({ pendingUndoSend: null }); set({ pendingUndoSend: null });
} }
-1
View File
@@ -494,7 +494,6 @@ export const useSettingsStore = create<SettingsState>()(
plainTextMode: state.plainTextMode, plainTextMode: state.plainTextMode,
subAddressDelimiter: state.subAddressDelimiter, subAddressDelimiter: state.subAddressDelimiter,
sendDelaySeconds: state.sendDelaySeconds, sendDelaySeconds: state.sendDelaySeconds,
sendDelaySeconds: state.sendDelaySeconds,
signaturePosition: state.signaturePosition, signaturePosition: state.signaturePosition,
signatureSeparatorEnabled: state.signatureSeparatorEnabled, signatureSeparatorEnabled: state.signatureSeparatorEnabled,
sessionTimeout: state.sessionTimeout, sessionTimeout: state.sessionTimeout,