Sync
The sync log records actions taken during each sync pass.
Sync is the process that closes the gap between the local SQLite cache and the remote IMAP server.
Schema
Stored in the sync_log table:
| Column | Type | Notes |
|---|---|---|
id |
INTEGER | Primary key |
folder_id |
INTEGER | Foreign key → folders.id |
action |
TEXT | What happened (insert, expunge, flag_update, etc.) |
uid |
INTEGER | The message UID this action relates to |
details |
TEXT | Optional JSON details about the action |
synced_at |
TEXT | ISO timestamp when the action was recorded |
Sync Strategies
Initial Sync
Run when a folder has no messages in the local cache (first sync or after a UIDVALIDITY reset). The engine:
- Issues
UID FETCH 1:* (FLAGS ENVELOPE BODYSTRUCTURE RFC822.SIZE)in configurable batches. - Inserts message rows via
insertMessagesBatch(withINSERT OR IGNOREsemantics for idempotency). - Populates attachment metadata from BODYSTRUCTURE for messages with attachments.
- Runs thread computation for the folder.
- Populates the
messages_ftsFTS5 index. - Records the sync pass in
sync_log.
Incremental Sync (QRESYNC)
Run when the folder has an existing cache and the server advertises QRESYNC (RFC 7162). The engine:
- Issues
SELECT INBOX (QRESYNC (uidValidity highestModSeq)). - Processes VANISHED responses to expunge deleted messages.
- Processes FETCH responses to update flags (using
CHANGEDSINCE modSeq). - Updates
highest_mod_seqon the folder row. - Records the sync pass.
Incremental Sync (UID Fallback)
Run when QRESYNC is unavailable. The engine:
- Issues
UID FETCH <knownMax+1>:*to find new messages. - Issues
UID FETCH 1:<knownMax> FLAGSto detect flag changes and expunges. - Updates messages and folders accordingly.
- Records the sync pass.
UIDVALIDITY Reset
If the server returns a different uid_validity than the locally stored one:
- All messages for that folder are deleted from
messages,bodies,attachments, andsync_log. - The folder's
uid_validity,uid_next,highest_mod_seq, andmessage_countare reset to the server-reported values. - An initial sync is performed immediately.
- The
sync_logrecords the reset.
Body And Attachment Fetch Strategies
The sync call accepts a bodies option:
'none'(default): no body or attachment data is fetched during sync'missing': fetch bodies for messages that do not yet have abodiesrow'all': fetch bodies for all messages in scope (use sparingly)
Attachment binary data is never fetched during sync. It is always fetched on
demand via read.getAttachment().
The fetchBody and fetchAttachment functions in the sync engine include an
optimisation: they skip session.selectFolder() if the target folder is
already selected, avoiding redundant IMAP SELECT commands.
Local Store Operations
Store functions are accessible through the store namespace:
import { store } from '@ghostpaw/email';
// Record a sync action.
store.insertSyncLog(db, { folderId, action: 'insert', uid: 42 });
// List recent sync log entries for a folder.
const history = store.listSyncLog(db, folderId, { limit: 10 });
Sync is triggered through the network surface:
// Default incremental sync of all subscribed folders.
const result = await mailbox.network.sync();
// { totalNew, totalExpunged, folders: [{ path, newMessages, ... }] }
// Sync specific folders with body fetch.
await mailbox.network.sync({
folders: ['INBOX'],
bodies: 'missing',
});
IDLE / Watch
After a sync pass, the engine can enter IDLE mode to receive server-push notifications of new messages and flag changes without polling:
for await (const event of mailbox.network.watch({ signal })) {
if (event.type === 'new') await mailbox.network.sync({ folders: [event.folder] });
}
IDLE sends a DONE continuation and re-issues IDLE every 28 minutes to stay within the RFC 2177 timeout window.
Invariants
- Sync log entries provide a per-UID audit trail of actions taken during sync
- A UIDVALIDITY reset deletes and rebuilds all message data for the affected folder
highest_mod_seqis updated on thefoldersrow at the end of each QRESYNC passuid_nextis updated on thefoldersrow at the end of each sync passinsertMessagesBatchusesINSERT OR IGNOREfor idempotent re-syncs