Snoozing hides a thread from your inbox until a chosen time, then puts it back. It works offline-first: the local state changes immediately, and the server is updated on the next sync.

User flow

  1. Open Email Detail — or long-press threads in Combined Inbox or Email List to enter selection mode.
  2. Choose Snooze. The Snooze Picker opens with quick options (This evening, Tomorrow morning, Next week) and a Pick date & time… fallback.
  3. The email is moved to the Snoozed folder immediately, and a snackbar shows “Snoozed until …” with an Undo action.
  4. When the chosen time passes, the next sync cycle moves the email back to the INBOX of the same account.

The whole flow is offline-safe: if the device is disconnected when you snooze, the local move happens instantly and the server update is queued. Waking up also happens locally — the sync engine simply flushes the queued unsnooze whenever it reconnects.

Where the data lives

Snooze state is stored in three places.

1. Local database (Drift / SQLite)

The emails table gains two extra columns per row when a message is snoozed (see lib/data/db/database.dart):

ColumnMeaning
snoozed_untilUTC timestamp when the message should reappear. NULL for un-snoozed rows.
snoozed_from_mailbox_pathMailbox the message was in before it was moved to Snoozed. Kept for the Undo Log, so a wake-up entry can show where the message came from.

A partial index (emails_snoozed_until) makes the wake-up query cheap:

CREATE INDEX emails_snoozed_until
  ON emails (account_id, snoozed_until)
  WHERE snoozed_until IS NOT NULL;

At the same time, the row’s mailbox_path is set to the account’s Snoozed mailbox so the thread disappears from every other folder view.

2. Local pending-change queue

Every snooze also enqueues a row in the pending_changes table with change_type = 'snooze' and a JSON payload:

{
  "uid": 1234,
  "src": "INBOX",
  "dest": "Snoozed",
  "until": "2026-07-20T08:00:00.000Z"
}

Wake-ups enqueue an equivalent unsnooze change with dest = "INBOX". The sync engine flushes these on the next connection.

3. Server

The server side is protocol-specific:

IMAP. The mail is IMAP-MOVEd into a mailbox named Snoozed (auto-created if missing) and tagged with a keyword flag of the form snz:<timestamp> so the wake time survives a fresh install and a full re-sync. Unsnooze removes every snz:* keyword and IMAP-MOVEs the message back to INBOX.

JMAP. The mail is moved via Email/set between mailbox IDs — into a mailbox with role snoozed (created on demand with Mailbox/set), and back to the account’s inbox on unsnooze. The snz:<timestamp> keyword is added and removed alongside the move.

Waking up

The wake-up runs at the start of every sync cycle (AccountSyncManager._syncEmailRepository.wakeUpEmails):

  1. Select every email in the account whose snoozed_until <= now.
  2. For each match, enqueue an unsnooze pending change with the account’s INBOX as the destination and optimistically move the local row into INBOX, clearing snoozed_until and snoozed_from_mailbox_path.
  3. The pending-change flush that runs right after wakeUpEmails pushes the move to the server.

Emails always wake up into the current INBOX, regardless of which folder they were snoozed from — this matches the “get back to inbox, moved by app” behaviour agreed for the feature.

Where to look in the code

ConcernFile
Schema (snoozed_until, snoozed_from_mailbox_path)lib/data/db/database.dart
Snooze / unsnooze / wake-up logiclib/data/repositories/email_repository_impl.dartsnoozeEmail, wakeUpEmails, _applyPendingChangeImap, JMAP branch under case 'snooze'
Repository interfacelib/core/repositories/email_repository.dartsnoozeEmail, wakeUpEmails
UI trigger from a single messagelib/ui/screens/email_detail_screen.dart_snooze
UI trigger from a selectionlib/ui/screens/email_action_helpers.dartbatchSnooze
Bottom sheetlib/ui/widgets/snooze_picker.dartSnoozePicker
Sync-cycle wake-up calllib/core/sync/account_sync_manager.dart_sync