fix
This commit is contained in:
+12
-12
@@ -630,12 +630,12 @@ export default function Home() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingUndoSend) return;
|
||||
const sendAt = new Date(pendingUndoSend.sendAt).getTime();
|
||||
if (!Number.isFinite(sendAt) || sendAt <= Date.now()) {
|
||||
const pendingSendTime = new Date(pendingUndoSend.sendAt).getTime();
|
||||
if (!Number.isFinite(pendingSendTime) || pendingSendTime <= Date.now()) {
|
||||
clearPendingUndoSend();
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(clearPendingUndoSend, sendAt - Date.now());
|
||||
const timer = setTimeout(clearPendingUndoSend, pendingSendTime - Date.now());
|
||||
return () => clearTimeout(timer);
|
||||
}, [clearPendingUndoSend, pendingUndoSend]);
|
||||
|
||||
@@ -944,7 +944,7 @@ export default function Home() {
|
||||
attachments?: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }>;
|
||||
inReplyTo?: string[];
|
||||
references?: string[];
|
||||
sendAt?: string;
|
||||
delayedUntil?: string;
|
||||
}) => {
|
||||
if (!client) return;
|
||||
|
||||
@@ -952,7 +952,7 @@ export default function Home() {
|
||||
const effectiveMode = pendingDraft?.mode ?? composerMode;
|
||||
const originalEmailId = selectedEmail?.id;
|
||||
|
||||
const result = await sendEmail(client, data.to, data.subject, data.body, data.cc, data.bcc, data.identityId, data.fromEmail, data.draftId, data.fromName, data.htmlBody, data.attachments, data.inReplyTo, data.references, data.sendAt);
|
||||
const result = await sendEmail(client, data.to, data.subject, data.body, data.cc, data.bcc, data.identityId, data.fromEmail, data.draftId, data.fromName, data.htmlBody, data.attachments, data.inReplyTo, data.references, data.delayedUntil);
|
||||
setShowComposer(false);
|
||||
if (result.scheduled) {
|
||||
await refreshScheduledMetadata(client);
|
||||
@@ -1703,13 +1703,13 @@ export default function Home() {
|
||||
|
||||
const originalEmailId = selectedEmail.id;
|
||||
const sendDelaySeconds = useSettingsStore.getState().sendDelaySeconds;
|
||||
let sendAt: string | undefined;
|
||||
let delayedUntil: string | undefined;
|
||||
if (sendDelaySeconds > 0) {
|
||||
if (!client.hasDelayedSend()) {
|
||||
const confirmed = window.confirm(t('email_composer.send_delay_unsupported_confirm'));
|
||||
if (!confirmed) return;
|
||||
} else {
|
||||
sendAt = new Date(Date.now() + sendDelaySeconds * 1000).toISOString();
|
||||
delayedUntil = new Date(Date.now() + sendDelaySeconds * 1000).toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1735,7 +1735,7 @@ export default function Home() {
|
||||
undefined,
|
||||
threading?.inReplyTo,
|
||||
threading?.references,
|
||||
sendAt,
|
||||
delayedUntil,
|
||||
);
|
||||
|
||||
if (result.scheduled) {
|
||||
@@ -2312,9 +2312,9 @@ export default function Home() {
|
||||
setShowComposer(true);
|
||||
if (isMobile) setActiveView('viewer');
|
||||
}}
|
||||
onRescheduleScheduled={async (email, sendAt) => {
|
||||
onRescheduleScheduled={async (email, delayedUntil) => {
|
||||
if (client && email.emailSubmissionId && email.scheduledIdentityId) {
|
||||
await rescheduleScheduledEmail(client, email.emailSubmissionId, email.id, email.scheduledIdentityId, sendAt);
|
||||
await rescheduleScheduledEmail(client, email.emailSubmissionId, email.id, email.scheduledIdentityId, delayedUntil);
|
||||
}
|
||||
}}
|
||||
onEmailSelect={handleEmailSelect}
|
||||
@@ -2561,9 +2561,9 @@ export default function Home() {
|
||||
}
|
||||
if (restored) await handleEditDraft(restored);
|
||||
}}
|
||||
onRescheduleScheduled={async (sendAt) => {
|
||||
onRescheduleScheduled={async (delayedUntil) => {
|
||||
if (client && selectedEmail?.emailSubmissionId && selectedEmail.scheduledIdentityId) {
|
||||
await rescheduleScheduledEmail(client, selectedEmail.emailSubmissionId, selectedEmail.id, selectedEmail.scheduledIdentityId, sendAt);
|
||||
await rescheduleScheduledEmail(client, selectedEmail.emailSubmissionId, selectedEmail.id, selectedEmail.scheduledIdentityId, delayedUntil);
|
||||
}
|
||||
}}
|
||||
onCompose={() => {
|
||||
|
||||
@@ -1551,16 +1551,19 @@ function handleThreadGet(args: MethodArgs, callId: string): MethodResult {
|
||||
}
|
||||
|
||||
function handleEmailSubmissionSet(args: MethodArgs, callId: string): MethodResult {
|
||||
const created: Record<string, { id: string }> = {};
|
||||
const created: Record<string, { id: string; sendAt?: string }> = {};
|
||||
const updated: Record<string, null> = {};
|
||||
const create = args.create as Record<string, { emailId?: string; identityId?: string; sendAt?: string }> | undefined;
|
||||
const create = args.create as Record<string, { emailId?: string; identityId?: string; envelope?: { mailFrom?: { parameters?: { HOLDUNTIL?: string } } } }> | undefined;
|
||||
if (create) {
|
||||
for (const [key, value] of Object.entries(create)) {
|
||||
const id = `submission-${Date.now()}-${key}`;
|
||||
created[key] = { id };
|
||||
if (value.sendAt && value.emailId && value.identityId) {
|
||||
const holdUntil = value.envelope?.mailFrom?.parameters?.HOLDUNTIL;
|
||||
const holdUntilTime = holdUntil ? new Date(holdUntil).getTime() : Number.NaN;
|
||||
const delayedUntil = Number.isFinite(holdUntilTime) ? new Date(holdUntilTime).toISOString() : undefined;
|
||||
created[key] = { id, ...(delayedUntil ? { sendAt: delayedUntil } : {}) };
|
||||
if (delayedUntil && value.emailId && value.identityId) {
|
||||
const emailId = value.emailId.startsWith('#') ? emails[emails.length - 1]?.id || value.emailId : value.emailId;
|
||||
scheduledSubmissions.push({ id, emailId, identityId: value.identityId, sendAt: value.sendAt, undoStatus: 'pending' });
|
||||
scheduledSubmissions.push({ id, emailId, identityId: value.identityId, sendAt: delayedUntil, undoStatus: 'pending' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ interface EmailComposerProps {
|
||||
attachments?: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }>;
|
||||
inReplyTo?: string[];
|
||||
references?: string[];
|
||||
sendAt?: string;
|
||||
delayedUntil?: string;
|
||||
}) => void | Promise<void>;
|
||||
onScheduledSendCreated?: () => void | Promise<void>;
|
||||
onClose?: () => void;
|
||||
@@ -876,8 +876,8 @@ export function EmailComposer({
|
||||
return null;
|
||||
};
|
||||
|
||||
const getEffectiveSendAt = async (explicitSendAt?: string): Promise<string | undefined> => {
|
||||
if (explicitSendAt) return explicitSendAt;
|
||||
const resolveDelayedUntil = async (requestedDelayedUntil?: string): Promise<string | undefined> => {
|
||||
if (requestedDelayedUntil) return requestedDelayedUntil;
|
||||
if (sendDelaySeconds === 0) return undefined;
|
||||
if (client?.hasDelayedSend()) {
|
||||
return new Date(Date.now() + sendDelaySeconds * 1000).toISOString();
|
||||
@@ -932,7 +932,7 @@ export function EmailComposer({
|
||||
};
|
||||
};
|
||||
|
||||
const handleSend = async (skipAttachmentCheck = false, sendAt?: string) => {
|
||||
const handleSend = async (skipAttachmentCheck = false, delayedUntil?: string) => {
|
||||
const ccAddresses = cc.split(",").map(e => e.trim()).filter(Boolean);
|
||||
const bccAddresses = bcc.split(",").map(e => e.trim()).filter(Boolean);
|
||||
|
||||
@@ -1014,7 +1014,7 @@ export function EmailComposer({
|
||||
const inlineAttachments = rewritten?.attachments ?? [];
|
||||
|
||||
try {
|
||||
const effectiveSendAt = await getEffectiveSendAt(sendAt);
|
||||
const effectiveDelayedUntil = await resolveDelayedUntil(delayedUntil);
|
||||
// Let plugins veto the send (external-mail warning, mistyped-domain
|
||||
// guards, etc.). Returning false from any handler aborts before either
|
||||
// the S/MIME or standard JMAP path runs.
|
||||
@@ -1151,8 +1151,8 @@ export function EmailComposer({
|
||||
}
|
||||
|
||||
// 7. Send via raw email path
|
||||
const result = await sendRawEmail(client, payload, currentIdentity.id, effectiveSendAt);
|
||||
if (effectiveSendAt && finalDraftId) {
|
||||
const result = await sendRawEmail(client, payload, currentIdentity.id, effectiveDelayedUntil);
|
||||
if (effectiveDelayedUntil && finalDraftId) {
|
||||
client.deleteEmail(finalDraftId).catch(err => {
|
||||
debug.warn('email', 'Scheduled S/MIME send created, but plaintext draft cleanup failed:', err);
|
||||
toast.warning(t('schedule_send_cleanup_warning'));
|
||||
@@ -1199,7 +1199,7 @@ export function EmailComposer({
|
||||
attachments: uploadedAttachments.length > 0 ? uploadedAttachments : undefined,
|
||||
inReplyTo: threadingHeaders?.inReplyTo,
|
||||
references: threadingHeaders?.references,
|
||||
sendAt: effectiveSendAt,
|
||||
delayedUntil: effectiveDelayedUntil,
|
||||
});
|
||||
|
||||
if (mode === 'reply' || mode === 'replyAll') {
|
||||
@@ -1700,20 +1700,21 @@ export function EmailComposer({
|
||||
>
|
||||
<BookmarkPlus className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setScheduleError('');
|
||||
setScheduleValue('');
|
||||
setShowScheduleDialog(true);
|
||||
}}
|
||||
disabled={!client?.hasDelayedSend()}
|
||||
title={client?.hasDelayedSend() ? t('schedule_send') : t('schedule_send_unsupported')}
|
||||
className="h-9 w-9"
|
||||
>
|
||||
<CalendarClock className="w-4 h-4" />
|
||||
</Button>
|
||||
{client?.hasDelayedSend() && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setScheduleError('');
|
||||
setScheduleValue('');
|
||||
setShowScheduleDialog(true);
|
||||
}}
|
||||
title={t('schedule_send')}
|
||||
className="h-9 w-9"
|
||||
>
|
||||
<CalendarClock className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* S/MIME toggles */}
|
||||
{canSmimeSign && (
|
||||
|
||||
@@ -43,7 +43,7 @@ interface EmailListProps {
|
||||
onLoadMoreScheduled?: () => void;
|
||||
onCancelScheduled?: (email: Email) => void | Promise<void>;
|
||||
onCancelScheduledForEdit?: (email: Email) => void | Promise<void>;
|
||||
onRescheduleScheduled?: (email: Email, sendAt: string) => void | Promise<void>;
|
||||
onRescheduleScheduled?: (email: Email, delayedUntil: string) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function EmailList({
|
||||
@@ -235,7 +235,7 @@ export function EmailList({
|
||||
}
|
||||
}, [client, hasMoreEmails, isLoadingMore, isLoading, isScheduledView, loadMoreEmails, onLoadMoreScheduled]);
|
||||
|
||||
const promptForRescheduleSendAt = useCallback((): string | null => {
|
||||
const promptForRescheduleDelayedUntil = useCallback((): string | null => {
|
||||
const value = window.prompt(t('reschedule_prompt'));
|
||||
if (!value) return null;
|
||||
const time = new Date(value).getTime();
|
||||
@@ -518,8 +518,8 @@ export function EmailList({
|
||||
size="sm"
|
||||
className="h-7 px-2"
|
||||
onClick={() => {
|
||||
const sendAt = promptForRescheduleSendAt();
|
||||
if (sendAt) onRescheduleScheduled?.(thread.latestEmail, sendAt);
|
||||
const delayedUntil = promptForRescheduleDelayedUntil();
|
||||
if (delayedUntil) onRescheduleScheduled?.(thread.latestEmail, delayedUntil);
|
||||
}}
|
||||
>
|
||||
<CalendarClock className="w-3.5 h-3.5 mr-1" />
|
||||
@@ -581,8 +581,8 @@ export function EmailList({
|
||||
onCancelScheduled={() => onCancelScheduled?.(contextMenu.data!)}
|
||||
onCancelScheduledForEdit={() => onCancelScheduledForEdit?.(contextMenu.data!)}
|
||||
onRescheduleScheduled={() => {
|
||||
const sendAt = promptForRescheduleSendAt();
|
||||
if (sendAt) onRescheduleScheduled?.(contextMenu.data!, sendAt);
|
||||
const delayedUntil = promptForRescheduleDelayedUntil();
|
||||
if (delayedUntil) onRescheduleScheduled?.(contextMenu.data!, delayedUntil);
|
||||
}}
|
||||
onBatchMarkAsRead={(read) => client && batchMarkAsRead(client, read)}
|
||||
onBatchDelete={() => client && batchDelete(client)}
|
||||
|
||||
@@ -121,7 +121,7 @@ interface EmailViewerProps {
|
||||
onEditDraft?: () => void;
|
||||
onCancelScheduled?: () => void;
|
||||
onCancelScheduledForEdit?: () => void;
|
||||
onRescheduleScheduled?: (sendAt: string) => void;
|
||||
onRescheduleScheduled?: (delayedUntil: string) => void;
|
||||
onCompose?: () => void;
|
||||
currentUserEmail?: string;
|
||||
currentUserName?: string;
|
||||
@@ -883,7 +883,7 @@ export function EmailViewer({
|
||||
const { isTablet, isMobile } = useDeviceDetection();
|
||||
const { tabletListVisible } = useUIStore();
|
||||
const { identities, client, isDemoMode, activeAccountId } = useAuthStore();
|
||||
const promptForRescheduleSendAt = useCallback((): string | null => {
|
||||
const promptForRescheduleDelayedUntil = useCallback((): string | null => {
|
||||
const value = window.prompt(t('reschedule_prompt'));
|
||||
if (!value) return null;
|
||||
const time = new Date(value).getTime();
|
||||
@@ -3147,8 +3147,8 @@ export function EmailViewer({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const sendAt = promptForRescheduleSendAt();
|
||||
if (sendAt) onRescheduleScheduled?.(sendAt);
|
||||
const delayedUntil = promptForRescheduleDelayedUntil();
|
||||
if (delayedUntil) onRescheduleScheduled?.(delayedUntil);
|
||||
}}
|
||||
className="hidden sm:flex sm:h-8"
|
||||
title={t('reschedule_send')}
|
||||
@@ -4598,8 +4598,8 @@ export function EmailViewer({
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const sendAt = promptForRescheduleSendAt();
|
||||
if (sendAt) onRescheduleScheduled?.(sendAt);
|
||||
const delayedUntil = promptForRescheduleDelayedUntil();
|
||||
if (delayedUntil) onRescheduleScheduled?.(delayedUntil);
|
||||
}}
|
||||
>
|
||||
{t('reschedule_send')}
|
||||
|
||||
@@ -16,14 +16,14 @@ function enableDelayedSend(client: JMAPClient) {
|
||||
capabilities: {
|
||||
'urn:ietf:params:jmap:core': {},
|
||||
'urn:ietf:params:jmap:mail': {},
|
||||
'urn:ietf:params:jmap:submission': {},
|
||||
'urn:ietf:params:jmap:submission': { maxDelayedSend: 3600, submissionExtensions: ['FUTURERELEASE'] },
|
||||
},
|
||||
session: {
|
||||
accounts: {
|
||||
'account-1': {
|
||||
accountCapabilities: {
|
||||
'urn:ietf:params:jmap:mail': {},
|
||||
'urn:ietf:params:jmap:submission': { maxDelayedSend: 3600 },
|
||||
'urn:ietf:params:jmap:submission': { maxDelayedSend: 3600, submissionExtensions: ['FUTURERELEASE'] },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -82,7 +82,7 @@ function mockSendEmailFlow() {
|
||||
payload = {
|
||||
methodResponses: [
|
||||
['Email/set', { created: { [Object.keys((captured[callIdx].methodCalls[0][1] as { create: Record<string, unknown> }).create)[0]]: { id: 'sent-id-1' } } }, '0'],
|
||||
['EmailSubmission/set', { created: { '1': { id: 'sub-1' } } }, '1'],
|
||||
['EmailSubmission/set', { created: { '1': { id: 'sub-1', sendAt: '2026-05-08T18:00:00Z' } } }, '1'],
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -169,11 +169,11 @@ describe('JMAPClient.sendEmail threading headers', () => {
|
||||
expect(draft.references).toBeUndefined();
|
||||
});
|
||||
|
||||
it('includes sendAt and submission capability for scheduled sends', async () => {
|
||||
it('uses FUTURERELEASE envelope and submission capability for scheduled sends', async () => {
|
||||
const client = createClient();
|
||||
enableDelayedSend(client);
|
||||
const captured = mockSendEmailFlow();
|
||||
const sendAt = new Date(Date.now() + 60_000).toISOString();
|
||||
const delayedUntil = new Date(Date.now() + 60_000).toISOString();
|
||||
|
||||
const result = await client.sendEmail(
|
||||
['recipient@example.com'],
|
||||
@@ -183,14 +183,26 @@ describe('JMAPClient.sendEmail threading headers', () => {
|
||||
undefined, undefined, undefined, undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
sendAt,
|
||||
delayedUntil,
|
||||
);
|
||||
|
||||
const identityRequest = captured[1];
|
||||
expect(identityRequest.using).toContain('urn:ietf:params:jmap:submission');
|
||||
const submissionCall = captured[2].methodCalls.find(call => call[0] === 'EmailSubmission/set');
|
||||
expect(submissionCall?.[1].create).toEqual({ '1': { emailId: expect.stringMatching(/^#send-/), identityId: 'identity-1', sendAt } });
|
||||
expect(result).toMatchObject({ scheduled: true, emailSubmissionId: 'sub-1', sendAt });
|
||||
expect(submissionCall?.[1].create).toEqual({
|
||||
'1': {
|
||||
emailId: expect.stringMatching(/^#send-/),
|
||||
identityId: 'identity-1',
|
||||
envelope: {
|
||||
mailFrom: {
|
||||
email: 'user@example.com',
|
||||
parameters: { HOLDUNTIL: expect.stringMatching(/^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} \+0000$/) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify(submissionCall?.[1].create)).not.toContain('sendAt');
|
||||
expect(result).toMatchObject({ scheduled: true, emailSubmissionId: 'sub-1', sendAt: '2026-05-08T18:00:00Z' });
|
||||
});
|
||||
|
||||
it('cleans up replacement submission if canceling the original fails during reschedule', async () => {
|
||||
@@ -200,6 +212,9 @@ describe('JMAPClient.sendEmail threading headers', () => {
|
||||
{ id: 'mb-drafts', name: 'Drafts', role: 'drafts' },
|
||||
{ id: 'mb-sent', name: 'Sent', role: 'sent' },
|
||||
] as never);
|
||||
vi.spyOn(client, 'getIdentities').mockResolvedValue([
|
||||
{ id: 'identity-1', name: 'User', email: 'user@example.com', mayDelete: false },
|
||||
]);
|
||||
const requestSpy = vi.spyOn(client as unknown as { request: JMAPClient['request'] }, 'request')
|
||||
.mockImplementation(async (methodCalls) => {
|
||||
const args = methodCalls[0][1] as { create?: unknown; update?: Record<string, unknown> };
|
||||
|
||||
+17
-17
@@ -58,7 +58,7 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
return {
|
||||
'urn:ietf:params:jmap:core': { maxSizeUpload: 50_000_000, maxCallsInRequest: 16, maxObjectsInGet: 500 },
|
||||
'urn:ietf:params:jmap:mail': {},
|
||||
'urn:ietf:params:jmap:submission': {},
|
||||
'urn:ietf:params:jmap:submission': { maxDelayedSend: 30 * 24 * 60 * 60, submissionExtensions: ['FUTURERELEASE'] },
|
||||
'urn:ietf:params:jmap:vacationresponse': {},
|
||||
'urn:ietf:params:jmap:contacts': {},
|
||||
'urn:ietf:params:jmap:calendars': {},
|
||||
@@ -454,7 +454,7 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
attachments?: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }>,
|
||||
inReplyTo?: string[],
|
||||
references?: string[],
|
||||
sendAt?: string,
|
||||
delayedUntil?: string,
|
||||
): Promise<SendEmailResult> {
|
||||
// Remove draft if updating
|
||||
if (draftId) {
|
||||
@@ -463,8 +463,8 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
const sentMb = this.data.mailboxes.find(m => m.role === 'sent');
|
||||
const email: Email = {
|
||||
id: generateDemoId('email'), threadId: generateDemoId('thread'),
|
||||
mailboxIds: { [sendAt ? (this.data.mailboxes.find(m => m.role === 'drafts')?.id || 'demo-mailbox-drafts') : (sentMb?.id || 'demo-mailbox-sent')]: true },
|
||||
keywords: sendAt ? { $seen: true, $draft: true } : { $seen: true },
|
||||
mailboxIds: { [delayedUntil ? (this.data.mailboxes.find(m => m.role === 'drafts')?.id || 'demo-mailbox-drafts') : (sentMb?.id || 'demo-mailbox-sent')]: true },
|
||||
keywords: delayedUntil ? { $seen: true, $draft: true } : { $seen: true },
|
||||
size: body.length + (htmlBody?.length || 0),
|
||||
receivedAt: new Date().toISOString(),
|
||||
from: [{ name: 'Demo User', email: 'demo@example.com' }],
|
||||
@@ -485,20 +485,20 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
};
|
||||
this.data.emails.push(email);
|
||||
let emailSubmissionId: string | undefined;
|
||||
if (sendAt) {
|
||||
if (delayedUntil) {
|
||||
emailSubmissionId = generateDemoId('submission');
|
||||
this.scheduledSubmissions.set(emailSubmissionId, {
|
||||
id: emailSubmissionId,
|
||||
emailId: email.id,
|
||||
identityId: _identityId || 'demo-identity',
|
||||
sendAt,
|
||||
sendAt: delayedUntil,
|
||||
undoStatus: 'pending',
|
||||
isSmime: false,
|
||||
});
|
||||
}
|
||||
this.recalcMailboxCounts();
|
||||
return sendAt
|
||||
? { scheduled: true, emailId: email.id, emailSubmissionId, sendAt }
|
||||
return delayedUntil
|
||||
? { scheduled: true, emailId: email.id, emailSubmissionId, sendAt: delayedUntil }
|
||||
: { scheduled: false, emailId: email.id };
|
||||
}
|
||||
|
||||
@@ -919,15 +919,15 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
|
||||
async importRawEmail(): Promise<string> { return generateDemoId('email'); }
|
||||
async submitEmail(): Promise<void> { /* no-op */ }
|
||||
async sendRawEmail(_blob?: Blob, identityId = 'demo-identity', _sentMailboxId?: string, _draftMailboxId?: string, sendAt?: string): Promise<SendEmailResult> {
|
||||
async sendRawEmail(_blob?: Blob, identityId = 'demo-identity', _sentMailboxId?: string, _draftMailboxId?: string, delayedUntil?: string): Promise<SendEmailResult> {
|
||||
const emailId = generateDemoId('email');
|
||||
const draftsMailbox = this.data.mailboxes.find(m => m.role === 'drafts');
|
||||
const sentMailbox = this.data.mailboxes.find(m => m.role === 'sent');
|
||||
const email: Email = {
|
||||
id: emailId,
|
||||
threadId: generateDemoId('thread'),
|
||||
mailboxIds: { [(sendAt ? draftsMailbox?.id : sentMailbox?.id) || 'demo-mailbox-sent']: true },
|
||||
keywords: sendAt ? { $seen: true, $draft: true } : { $seen: true },
|
||||
mailboxIds: { [(delayedUntil ? draftsMailbox?.id : sentMailbox?.id) || 'demo-mailbox-sent']: true },
|
||||
keywords: delayedUntil ? { $seen: true, $draft: true } : { $seen: true },
|
||||
size: 1024,
|
||||
receivedAt: new Date().toISOString(),
|
||||
from: [{ name: 'Demo User', email: 'demo@example.com' }],
|
||||
@@ -938,12 +938,12 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
};
|
||||
this.data.emails.push(email);
|
||||
let emailSubmissionId: string | undefined;
|
||||
if (sendAt) {
|
||||
if (delayedUntil) {
|
||||
emailSubmissionId = generateDemoId('submission');
|
||||
this.scheduledSubmissions.set(emailSubmissionId, { id: emailSubmissionId, emailId, identityId, sendAt, undoStatus: 'pending', isSmime: true });
|
||||
this.scheduledSubmissions.set(emailSubmissionId, { id: emailSubmissionId, emailId, identityId, sendAt: delayedUntil, undoStatus: 'pending', isSmime: true });
|
||||
}
|
||||
this.recalcMailboxCounts();
|
||||
return sendAt ? { scheduled: true, emailId, emailSubmissionId, sendAt, isSmime: true } : { scheduled: false, emailId, isSmime: true };
|
||||
return delayedUntil ? { scheduled: true, emailId, emailSubmissionId, sendAt: delayedUntil, isSmime: true } : { scheduled: false, emailId, isSmime: true };
|
||||
}
|
||||
|
||||
async getScheduledEmails(limit = 50, position = 0): Promise<{ emails: ScheduledEmail[]; hasMore: boolean; total: number }> {
|
||||
@@ -972,11 +972,11 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
if (submission) submission.undoStatus = 'canceled';
|
||||
}
|
||||
|
||||
async rescheduleEmailSubmission(submissionId: string, emailId: string, identityId: string, sendAt: string): Promise<SendEmailResult> {
|
||||
async rescheduleEmailSubmission(submissionId: string, emailId: string, identityId: string, delayedUntil: string): Promise<SendEmailResult> {
|
||||
await this.cancelEmailSubmission(submissionId);
|
||||
const replacement = generateDemoId('submission');
|
||||
this.scheduledSubmissions.set(replacement, { id: replacement, emailId, identityId, sendAt, undoStatus: 'pending', isSmime: false });
|
||||
return { scheduled: true, emailId, emailSubmissionId: replacement, sendAt };
|
||||
this.scheduledSubmissions.set(replacement, { id: replacement, emailId, identityId, sendAt: delayedUntil, undoStatus: 'pending', isSmime: false });
|
||||
return { scheduled: true, emailId, emailSubmissionId: replacement, sendAt: delayedUntil };
|
||||
}
|
||||
|
||||
async restoreEmailToDraft(emailId: string, draftMailboxId: string, sentMailboxId?: string): Promise<void> {
|
||||
|
||||
@@ -148,13 +148,13 @@ export interface IJMAPClient {
|
||||
attachments?: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }>,
|
||||
inReplyTo?: string[],
|
||||
references?: string[],
|
||||
sendAt?: string,
|
||||
delayedUntil?: string,
|
||||
): Promise<SendEmailResult>;
|
||||
|
||||
sendRawEmail(blob: Blob, identityId: string, sentMailboxId: string, draftMailboxId?: string, sendAt?: string): Promise<SendEmailResult>;
|
||||
sendRawEmail(blob: Blob, identityId: string, sentMailboxId: string, draftMailboxId?: string, delayedUntil?: string): Promise<SendEmailResult>;
|
||||
getScheduledEmails(limit?: number, position?: number): Promise<{ emails: ScheduledEmail[]; hasMore: boolean; total: number }>;
|
||||
cancelEmailSubmission(submissionId: string): Promise<void>;
|
||||
rescheduleEmailSubmission(submissionId: string, emailId: string, identityId: string, sendAt: string): Promise<SendEmailResult>;
|
||||
rescheduleEmailSubmission(submissionId: string, emailId: string, identityId: string, delayedUntil: string): Promise<SendEmailResult>;
|
||||
restoreEmailToDraft(emailId: string, draftMailboxId: string, sentMailboxId?: string): Promise<void>;
|
||||
|
||||
sendImipReply(opts: {
|
||||
|
||||
+94
-21
@@ -337,6 +337,27 @@ function sanitizeIdentityDisplayName(name: string | undefined | null): string {
|
||||
return name.replace(/\s*<[^>]*>\s*$/, '').trim();
|
||||
}
|
||||
|
||||
function formatHoldUntil(delayedUntil: string): string {
|
||||
const date = new Date(delayedUntil);
|
||||
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
const pad = (value: number) => String(value).padStart(2, '0');
|
||||
|
||||
return `${weekdays[date.getUTCDay()]}, ${pad(date.getUTCDate())} ${months[date.getUTCMonth()]} ${date.getUTCFullYear()} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} +0000`;
|
||||
}
|
||||
|
||||
function createDelayedSubmissionEnvelope(fromEmail: string, delayedUntil?: string): Record<string, unknown> | undefined {
|
||||
if (!delayedUntil) return undefined;
|
||||
return {
|
||||
mailFrom: {
|
||||
email: fromEmail,
|
||||
parameters: {
|
||||
HOLDUNTIL: formatHoldUntil(delayedUntil),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export class JMAPClient implements IJMAPClient {
|
||||
private static readonly RATE_LIMIT_TOAST_THROTTLE_MS = 10_000;
|
||||
|
||||
@@ -2082,9 +2103,9 @@ export class JMAPClient implements IJMAPClient {
|
||||
attachments?: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }>,
|
||||
inReplyTo?: string[],
|
||||
references?: string[],
|
||||
sendAt?: string
|
||||
delayedUntil?: string
|
||||
): Promise<SendEmailResult> {
|
||||
if (sendAt) this.validateSendAt(sendAt);
|
||||
if (delayedUntil) this.validateDelayedUntil(delayedUntil);
|
||||
const emailId = `send-${Date.now()}`;
|
||||
const mailboxes = await this.getMailboxes();
|
||||
const sentMailbox = mailboxes.find(mb => mb.role === 'sent');
|
||||
@@ -2189,9 +2210,14 @@ export class JMAPClient implements IJMAPClient {
|
||||
accountId: this.accountId,
|
||||
create: { [emailId]: emailCreate },
|
||||
}, "1"]);
|
||||
const submissionCreate = {
|
||||
emailId: `#${emailId}`,
|
||||
identityId: finalIdentityId,
|
||||
...(delayedUntil ? { envelope: createDelayedSubmissionEnvelope(fromEmail || this.username, delayedUntil) } : {}),
|
||||
};
|
||||
methodCalls.push(["EmailSubmission/set", {
|
||||
accountId: this.accountId,
|
||||
create: { "1": { emailId: `#${emailId}`, identityId: finalIdentityId, ...(sendAt ? { sendAt } : {}) } },
|
||||
create: { "1": submissionCreate },
|
||||
onSuccessUpdateEmail,
|
||||
}, "2"]);
|
||||
} else {
|
||||
@@ -2199,9 +2225,14 @@ export class JMAPClient implements IJMAPClient {
|
||||
accountId: this.accountId,
|
||||
create: { [emailId]: emailCreate },
|
||||
}, "0"]);
|
||||
const submissionCreate = {
|
||||
emailId: `#${emailId}`,
|
||||
identityId: finalIdentityId,
|
||||
...(delayedUntil ? { envelope: createDelayedSubmissionEnvelope(fromEmail || this.username, delayedUntil) } : {}),
|
||||
};
|
||||
methodCalls.push(["EmailSubmission/set", {
|
||||
accountId: this.accountId,
|
||||
create: { "1": { emailId: `#${emailId}`, identityId: finalIdentityId, ...(sendAt ? { sendAt } : {}) } },
|
||||
create: { "1": submissionCreate },
|
||||
onSuccessUpdateEmail,
|
||||
}, "1"]);
|
||||
}
|
||||
@@ -2210,6 +2241,7 @@ export class JMAPClient implements IJMAPClient {
|
||||
|
||||
let createdEmailId: string | undefined;
|
||||
let emailSubmissionId: string | undefined;
|
||||
let serverSendAt: string | undefined;
|
||||
|
||||
if (response.methodResponses) {
|
||||
for (const [methodName, result] of response.methodResponses) {
|
||||
@@ -2230,12 +2262,17 @@ export class JMAPClient implements IJMAPClient {
|
||||
}
|
||||
if (methodName === 'EmailSubmission/set' && result.created?.['1']?.id) {
|
||||
emailSubmissionId = result.created['1'].id;
|
||||
serverSendAt = result.created['1'].sendAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sendAt
|
||||
? { scheduled: true, emailId: createdEmailId, emailSubmissionId, sendAt }
|
||||
if (delayedUntil && emailSubmissionId && !serverSendAt) {
|
||||
serverSendAt = await this.getEmailSubmissionSendAt(emailSubmissionId);
|
||||
}
|
||||
|
||||
return delayedUntil
|
||||
? { scheduled: true, emailId: createdEmailId, emailSubmissionId, sendAt: serverSendAt }
|
||||
: { scheduled: false, emailId: createdEmailId, emailSubmissionId };
|
||||
}
|
||||
|
||||
@@ -2866,19 +2903,27 @@ export class JMAPClient implements IJMAPClient {
|
||||
|
||||
getMaxDelayedSend(accountId?: string): number {
|
||||
const id = accountId || this.accountId;
|
||||
const submissionCapability = this.session?.accounts?.[id]?.accountCapabilities?.["urn:ietf:params:jmap:submission"] as { maxDelayedSend?: number } | undefined;
|
||||
return typeof submissionCapability?.maxDelayedSend === 'number' ? submissionCapability.maxDelayedSend : 0;
|
||||
const accountCapability = this.session?.accounts?.[id]?.accountCapabilities?.["urn:ietf:params:jmap:submission"] as { maxDelayedSend?: number } | undefined;
|
||||
const sessionCapability = this.session?.capabilities?.["urn:ietf:params:jmap:submission"] as { maxDelayedSend?: number } | undefined;
|
||||
const maxDelayedSend = accountCapability?.maxDelayedSend ?? sessionCapability?.maxDelayedSend;
|
||||
return typeof maxDelayedSend === 'number' ? maxDelayedSend : 0;
|
||||
}
|
||||
|
||||
hasDelayedSend(accountId?: string): boolean {
|
||||
const id = accountId || this.accountId;
|
||||
const accountCapability = this.session?.accounts?.[id]?.accountCapabilities?.["urn:ietf:params:jmap:submission"] as { submissionExtensions?: unknown } | undefined;
|
||||
const sessionCapability = this.session?.capabilities?.["urn:ietf:params:jmap:submission"] as { submissionExtensions?: unknown } | undefined;
|
||||
const submissionExtensions = accountCapability?.submissionExtensions ?? sessionCapability?.submissionExtensions;
|
||||
const hasFutureRelease = Array.isArray(submissionExtensions)
|
||||
&& submissionExtensions.some(extension => typeof extension === 'string' && extension.toUpperCase() === 'FUTURERELEASE');
|
||||
|
||||
return this.supportsEmailSubmission()
|
||||
&& this.hasAccountCapability('urn:ietf:params:jmap:submission', id)
|
||||
&& hasFutureRelease
|
||||
&& this.getMaxDelayedSend(id) > 0;
|
||||
}
|
||||
|
||||
private validateSendAt(sendAt: string, accountId?: string): void {
|
||||
const time = new Date(sendAt).getTime();
|
||||
private validateDelayedUntil(delayedUntil: string, accountId?: string): void {
|
||||
const time = new Date(delayedUntil).getTime();
|
||||
if (!Number.isFinite(time)) {
|
||||
throw new Error('Scheduled send time is invalid');
|
||||
}
|
||||
@@ -2895,6 +2940,18 @@ export class JMAPClient implements IJMAPClient {
|
||||
}
|
||||
}
|
||||
|
||||
private async getEmailSubmissionSendAt(submissionId: string): Promise<string | undefined> {
|
||||
const response = await this.request([
|
||||
['EmailSubmission/get', {
|
||||
accountId: this.accountId,
|
||||
ids: [submissionId],
|
||||
properties: ['sendAt', 'undoStatus'],
|
||||
}, '0'],
|
||||
]);
|
||||
const submission = response.methodResponses?.[0]?.[1]?.list?.[0] as { sendAt?: string } | undefined;
|
||||
return submission?.sendAt;
|
||||
}
|
||||
|
||||
getEventSourceUrl(): string | null {
|
||||
if (!this.session) return null;
|
||||
|
||||
@@ -5361,9 +5418,9 @@ export class JMAPClient implements IJMAPClient {
|
||||
identityId: string,
|
||||
sentMailboxId: string,
|
||||
draftMailboxId?: string,
|
||||
sendAt?: string,
|
||||
delayedUntil?: string,
|
||||
): Promise<SendEmailResult> {
|
||||
if (sendAt) this.validateSendAt(sendAt);
|
||||
if (delayedUntil) this.validateDelayedUntil(delayedUntil);
|
||||
// Upload the raw message
|
||||
const file = new File([blob], 'message.eml', { type: 'message/rfc822' });
|
||||
const { blobId } = await this.uploadBlob(file);
|
||||
@@ -5371,6 +5428,10 @@ export class JMAPClient implements IJMAPClient {
|
||||
// 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 identities = await this.getIdentities();
|
||||
const identity = identities.find(item => item.id === identityId);
|
||||
const envelope = createDelayedSubmissionEnvelope(identity?.email || this.username, delayedUntil);
|
||||
|
||||
const methodCalls: [string, Record<string, unknown>, string][] = [
|
||||
['Email/import', {
|
||||
accountId: this.accountId,
|
||||
@@ -5388,7 +5449,7 @@ export class JMAPClient implements IJMAPClient {
|
||||
'raw-submit': {
|
||||
emailId: '#raw-import',
|
||||
identityId,
|
||||
...(sendAt ? { sendAt } : {}),
|
||||
...(envelope ? { envelope } : {}),
|
||||
},
|
||||
},
|
||||
...(draftMailboxId ? {
|
||||
@@ -5406,6 +5467,7 @@ export class JMAPClient implements IJMAPClient {
|
||||
const response = await this.request(methodCalls);
|
||||
let emailId: string | undefined;
|
||||
let emailSubmissionId: string | undefined;
|
||||
let serverSendAt: string | undefined;
|
||||
|
||||
// Check for errors
|
||||
for (const [methodName, result] of response.methodResponses ?? []) {
|
||||
@@ -5421,12 +5483,18 @@ export class JMAPClient implements IJMAPClient {
|
||||
emailId = (result as { created?: Record<string, { id?: string }> }).created?.['raw-import']?.id;
|
||||
}
|
||||
if (methodName === 'EmailSubmission/set') {
|
||||
emailSubmissionId = (result as { created?: Record<string, { id?: string }> }).created?.['raw-submit']?.id;
|
||||
const created = (result as { created?: Record<string, { id?: string; sendAt?: string }> }).created?.['raw-submit'];
|
||||
emailSubmissionId = created?.id;
|
||||
serverSendAt = created?.sendAt;
|
||||
}
|
||||
}
|
||||
|
||||
return sendAt
|
||||
? { scheduled: true, emailId, emailSubmissionId, sendAt, isSmime: true }
|
||||
if (delayedUntil && emailSubmissionId && !serverSendAt) {
|
||||
serverSendAt = await this.getEmailSubmissionSendAt(emailSubmissionId);
|
||||
}
|
||||
|
||||
return delayedUntil
|
||||
? { scheduled: true, emailId, emailSubmissionId, sendAt: serverSendAt, isSmime: true }
|
||||
: { scheduled: false, emailId, emailSubmissionId, isSmime: true };
|
||||
}
|
||||
|
||||
@@ -5517,15 +5585,18 @@ export class JMAPClient implements IJMAPClient {
|
||||
}
|
||||
}
|
||||
|
||||
async rescheduleEmailSubmission(submissionId: string, emailId: string, identityId: string, sendAt: string): Promise<SendEmailResult> {
|
||||
this.validateSendAt(sendAt);
|
||||
async rescheduleEmailSubmission(submissionId: string, emailId: string, identityId: string, delayedUntil: string): Promise<SendEmailResult> {
|
||||
this.validateDelayedUntil(delayedUntil);
|
||||
const mailboxes = await this.getMailboxes();
|
||||
const draftsMailbox = mailboxes.find(mb => mb.role === 'drafts');
|
||||
const sentMailbox = mailboxes.find(mb => mb.role === 'sent');
|
||||
const identities = await this.getIdentities();
|
||||
const identity = identities.find(item => item.id === identityId);
|
||||
const envelope = createDelayedSubmissionEnvelope(identity?.email || this.username, delayedUntil);
|
||||
const response = await this.request([
|
||||
['EmailSubmission/set', {
|
||||
accountId: this.accountId,
|
||||
create: { replacement: { emailId, identityId, sendAt } },
|
||||
create: { replacement: { emailId, identityId, ...(envelope ? { envelope } : {}) } },
|
||||
...(draftsMailbox && sentMailbox ? {
|
||||
onSuccessUpdateEmail: {
|
||||
'#replacement': {
|
||||
@@ -5543,9 +5614,11 @@ export class JMAPClient implements IJMAPClient {
|
||||
throw new Error(createError.description || createError.type || 'Failed to reschedule email');
|
||||
}
|
||||
const replacementId = result?.created?.replacement?.id;
|
||||
const serverSendAt = result?.created?.replacement?.sendAt;
|
||||
if (!replacementId) {
|
||||
throw new Error('Server did not return a replacement scheduled send ID');
|
||||
}
|
||||
const finalSendAt = serverSendAt || await this.getEmailSubmissionSendAt(replacementId);
|
||||
try {
|
||||
await this.cancelEmailSubmission(submissionId);
|
||||
} catch (error) {
|
||||
@@ -5557,7 +5630,7 @@ export class JMAPClient implements IJMAPClient {
|
||||
const message = error instanceof Error ? error.message : 'Failed to cancel original scheduled send';
|
||||
throw new Error(`Reschedule created a replacement but could not cancel the original: ${message}`);
|
||||
}
|
||||
return { scheduled: true, emailId, emailSubmissionId: replacementId, sendAt };
|
||||
return { scheduled: true, emailId, emailSubmissionId: replacementId, sendAt: finalSendAt };
|
||||
}
|
||||
|
||||
async restoreEmailToDraft(emailId: string, draftMailboxId: string, sentMailboxId?: string): Promise<void> {
|
||||
|
||||
+12
-12
@@ -94,8 +94,8 @@ interface EmailStore {
|
||||
loadMoreEmails: (client: IJMAPClient) => Promise<void>;
|
||||
fetchEmailContent: (client: IJMAPClient, emailId: string) => Promise<Email | null>;
|
||||
fetchQuota: (client: IJMAPClient) => Promise<void>;
|
||||
sendEmail: (client: IJMAPClient, to: string[], subject: string, body: string, cc?: string[], bcc?: string[], identityId?: string, fromEmail?: string, draftId?: string, fromName?: string, htmlBody?: string, attachments?: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }>, inReplyTo?: string[], references?: string[], sendAt?: string) => Promise<SendEmailResult>;
|
||||
sendRawEmail: (client: IJMAPClient, rawMimeBlob: Blob, identityId: string, sendAt?: string) => Promise<SendEmailResult>;
|
||||
sendEmail: (client: IJMAPClient, to: string[], subject: string, body: string, cc?: string[], bcc?: string[], identityId?: string, fromEmail?: string, draftId?: string, fromName?: string, htmlBody?: string, attachments?: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }>, inReplyTo?: string[], references?: string[], delayedUntil?: string) => Promise<SendEmailResult>;
|
||||
sendRawEmail: (client: IJMAPClient, rawMimeBlob: Blob, identityId: string, delayedUntil?: string) => Promise<SendEmailResult>;
|
||||
deleteEmail: (client: IJMAPClient, emailId: string, forceDelete?: boolean) => Promise<void>;
|
||||
markAsRead: (client: IJMAPClient, emailId: string, read: boolean) => Promise<void>;
|
||||
moveToMailbox: (client: IJMAPClient, emailId: string, mailboxId: string) => Promise<void>;
|
||||
@@ -153,7 +153,7 @@ interface EmailStore {
|
||||
loadMoreScheduledEmails: (client: IJMAPClient) => Promise<void>;
|
||||
cancelScheduledEmail: (client: IJMAPClient, submissionId: string) => Promise<void>;
|
||||
cancelScheduledEmailForEdit: (client: IJMAPClient, email: ScheduledEmail | Email) => Promise<Email | null>;
|
||||
rescheduleScheduledEmail: (client: IJMAPClient, submissionId: string, emailId: string, identityId: string, sendAt: string) => Promise<SendEmailResult>;
|
||||
rescheduleScheduledEmail: (client: IJMAPClient, submissionId: string, emailId: string, identityId: string, delayedUntil: string) => Promise<SendEmailResult>;
|
||||
cancelUndoSend: (client: IJMAPClient, pending: PendingUndoSend) => Promise<Email | null>;
|
||||
clearPendingUndoSend: () => void;
|
||||
refreshScheduledMetadata: (client: IJMAPClient) => Promise<void>;
|
||||
@@ -219,8 +219,8 @@ function annotateScheduledEmail(
|
||||
|
||||
function shouldClearPendingUndoSend(pending: PendingUndoSend | null, scheduledEmails: ScheduledEmail[]): boolean {
|
||||
if (!pending) return false;
|
||||
const sendAt = new Date(pending.sendAt).getTime();
|
||||
if (Number.isFinite(sendAt) && sendAt <= Date.now()) return true;
|
||||
const pendingSendTime = new Date(pending.sendAt).getTime();
|
||||
if (Number.isFinite(pendingSendTime) && pendingSendTime <= Date.now()) return true;
|
||||
const scheduledEmail = scheduledEmails.find(email => email.emailSubmissionId === pending.submissionId);
|
||||
return scheduledEmail?.scheduledUndoStatus !== undefined && scheduledEmail.scheduledUndoStatus !== 'pending';
|
||||
}
|
||||
@@ -607,10 +607,10 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
sendEmail: async (client, to, subject, body, cc, bcc, identityId, fromEmail, draftId, fromName, htmlBody, attachments, inReplyTo, references, sendAt) => {
|
||||
sendEmail: async (client, to, subject, body, cc, bcc, identityId, fromEmail, draftId, fromName, htmlBody, attachments, inReplyTo, references, delayedUntil) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const result = await client.sendEmail(to, subject, body, cc, bcc, identityId, fromEmail, draftId, fromName, htmlBody, attachments, inReplyTo, references, sendAt);
|
||||
const result = await client.sendEmail(to, subject, body, cc, bcc, identityId, fromEmail, draftId, fromName, htmlBody, attachments, inReplyTo, references, delayedUntil);
|
||||
// Refresh handled by UI layer for immediate feedback
|
||||
set({
|
||||
isLoading: false,
|
||||
@@ -628,14 +628,14 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
sendRawEmail: async (client, rawMimeBlob, identityId, sendAt) => {
|
||||
sendRawEmail: async (client, rawMimeBlob, identityId, delayedUntil) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const mailboxes = await client.getMailboxes();
|
||||
const sentMailbox = mailboxes.find(mb => mb.role === 'sent');
|
||||
if (!sentMailbox) throw new Error('No sent mailbox found');
|
||||
const draftsMailbox = mailboxes.find(mb => mb.role === 'drafts');
|
||||
const result = await client.sendRawEmail(rawMimeBlob, identityId, sentMailbox.id, draftsMailbox?.id, sendAt);
|
||||
const result = await client.sendRawEmail(rawMimeBlob, identityId, sentMailbox.id, draftsMailbox?.id, delayedUntil);
|
||||
set({
|
||||
isLoading: false,
|
||||
pendingUndoSend: result.scheduled && result.emailSubmissionId && result.sendAt
|
||||
@@ -2152,12 +2152,12 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
return restored;
|
||||
},
|
||||
|
||||
rescheduleScheduledEmail: async (client, submissionId, emailId, identityId, sendAt) => {
|
||||
rescheduleScheduledEmail: async (client, submissionId, emailId, identityId, delayedUntil) => {
|
||||
try {
|
||||
const result = await client.rescheduleEmailSubmission(submissionId, emailId, identityId, sendAt);
|
||||
const result = await client.rescheduleEmailSubmission(submissionId, emailId, identityId, delayedUntil);
|
||||
const pendingUndoSend = get().pendingUndoSend;
|
||||
if (pendingUndoSend?.submissionId === submissionId) {
|
||||
set({ pendingUndoSend: { ...pendingUndoSend, sendAt } });
|
||||
set({ pendingUndoSend: { ...pendingUndoSend, sendAt: result.sendAt || delayedUntil } });
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user