diff --git a/app/(main)/[locale]/page.tsx b/app/(main)/[locale]/page.tsx index 694dc3b3..296497e9 100644 --- a/app/(main)/[locale]/page.tsx +++ b/app/(main)/[locale]/page.tsx @@ -117,6 +117,30 @@ export default function Home() { const sendDelaySeconds = useSettingsStore((state) => state.sendDelaySeconds); const { loadTrustedSendersBook, trustedSendersLoaded } = useContactStore(); + const promptForRescheduleDelayedUntil = useCallback((): string | null => { + const value = window.prompt(t('email_viewer.reschedule_prompt')); + if (!value) return null; + const time = new Date(value).getTime(); + if (!Number.isFinite(time)) { + toast.error(t('email_composer.schedule_send_invalid')); + return null; + } + if (time <= Date.now()) { + toast.error(t('email_composer.schedule_send_future')); + return null; + } + if (!client?.hasDelayedSend()) { + toast.error(t('email_composer.schedule_send_unsupported')); + return null; + } + const maxDelayedSend = client.getMaxDelayedSend(); + if (maxDelayedSend > 0 && time > Date.now() + maxDelayedSend * 1000) { + toast.error(t('email_composer.schedule_send_too_late')); + return null; + } + return new Date(time).toISOString(); + }, [client, t]); + // Load trusted senders address book when feature is enabled useEffect(() => { if (trustedSendersAddressBook && client && !trustedSendersLoaded) { @@ -2721,6 +2745,12 @@ export default function Home() { setShowComposer(true); if (isMobile) setActiveView('viewer'); }} + onRescheduleScheduled={async (email) => { + const delayedUntil = promptForRescheduleDelayedUntil(); + if (delayedUntil && client && email.emailSubmissionId && email.scheduledIdentityId) { + await rescheduleScheduledEmail(client, email.emailSubmissionId, email.id, email.scheduledIdentityId, delayedUntil); + } + }} onEmailSelect={handleEmailSelect} onEmailDoubleClick={isEmbedded ? ((email) => { useProTabStore.getState().openEmailTab({ diff --git a/components/email/email-list.tsx b/components/email/email-list.tsx index 2d1c3e0c..124cad97 100644 --- a/components/email/email-list.tsx +++ b/components/email/email-list.tsx @@ -45,6 +45,7 @@ interface EmailListProps { onLoadMoreScheduled?: () => void; onCancelScheduled?: (email: Email) => void | Promise; onCancelScheduledForEdit?: (email: Email) => void | Promise; + onRescheduleScheduled?: (email: Email) => void | Promise; } export function EmailList({ @@ -73,6 +74,7 @@ export function EmailList({ onLoadMoreScheduled, onCancelScheduled, onCancelScheduledForEdit, + onRescheduleScheduled, }: EmailListProps) { const t = useTranslations('email_list'); const { client } = useAuthStore(); @@ -535,9 +537,9 @@ export function EmailList({ onMarkAsSpam={() => onMarkAsSpam?.(contextMenu.data!)} onUndoSpam={() => onUndoSpam?.(contextMenu.data!)} onEditDraft={() => onEditDraft?.(contextMenu.data!)} - onCancelScheduled={() => onCancelScheduled?.(contextMenu.data!)} - onCancelScheduledForEdit={() => onCancelScheduledForEdit?.(contextMenu.data!)} - onRescheduleScheduled={undefined} + onCancelScheduled={onCancelScheduled ? () => onCancelScheduled(contextMenu.data!) : undefined} + onCancelScheduledForEdit={onCancelScheduledForEdit ? () => onCancelScheduledForEdit(contextMenu.data!) : undefined} + onRescheduleScheduled={onRescheduleScheduled ? () => onRescheduleScheduled(contextMenu.data!) : undefined} onBatchMarkAsRead={(read) => client && batchMarkAsRead(client, read)} onBatchDelete={() => client && batchDelete(client)} onBatchArchive={async () => { diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx index ca940644..aafd03f2 100644 --- a/components/email/thread-list-item.tsx +++ b/components/email/thread-list-item.tsx @@ -335,16 +335,18 @@ const SingleEmailItem = React.forwardRef( {/* Hover Quick Actions */} - + {!email.isScheduled && ( + + )} ); } @@ -751,16 +753,18 @@ export const ThreadListItem = React.forwardRef {/* Hover Quick Actions for thread header */} - onToggleStar(latestEmail) : undefined} - onMarkAsRead={onMarkAsRead ? (read) => onMarkAsRead(latestEmail, read) : undefined} - onDelete={onDelete ? () => onDelete(latestEmail) : undefined} - onArchive={onArchive ? () => onArchive(latestEmail) : undefined} - onSetColorTag={onSetColorTag ? (color) => onSetColorTag(latestEmail.id, color) : undefined} - onMarkAsSpam={onMarkAsSpam ? () => onMarkAsSpam(latestEmail) : undefined} - /> + {!latestEmail.isScheduled && ( + onToggleStar(latestEmail) : undefined} + onMarkAsRead={onMarkAsRead ? (read) => onMarkAsRead(latestEmail, read) : undefined} + onDelete={onDelete ? () => onDelete(latestEmail) : undefined} + onArchive={onArchive ? () => onArchive(latestEmail) : undefined} + onSetColorTag={onSetColorTag ? (color) => onSetColorTag(latestEmail.id, color) : undefined} + onMarkAsSpam={onMarkAsSpam ? () => onMarkAsSpam(latestEmail) : undefined} + /> + )} {isExpanded && !isMobile && !isFocusedMailLayout && ( diff --git a/stores/email-store.ts b/stores/email-store.ts index afa845ae..964013e2 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -2566,6 +2566,15 @@ export const useEmailStore = create((set, get) => ({ const submissionId = email.emailSubmissionId; if (!submissionId) return null; await client.cancelEmailSubmission(submissionId); + if (email.isSmimeScheduled) { + await client.deleteEmail(email.id); + set(state => ({ + selectedEmail: state.selectedEmail?.id === email.id ? null : state.selectedEmail, + selectedEmailIds: new Set(Array.from(state.selectedEmailIds).filter(id => id !== email.id)), + })); + await get().fetchScheduledEmails(client); + return null; + } const mailboxes = get().mailboxes.length > 0 ? get().mailboxes : await client.getMailboxes(); const draftsMailbox = mailboxes.find(mb => mb.role === 'drafts'); const sentMailbox = mailboxes.find(mb => mb.role === 'sent');