Move JMAP send, push, and conflict-resolution items from Next steps
into Implemented features. Replace the next-steps section with
optional future work (CONDSTORE, blob expiry, UI for stuck mutations).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Check notUpdated/notDestroyed per-item errors in Email/set; throw
JmapSetItemException for permanent failures (notFound, forbidden) so
they are discarded immediately rather than retried
- Add _maxChangeAttempts=5 constant; _recordChangeError() evicts the
pending-change row when attempts reach the limit, preventing unbounded
queue growth from transient errors
- Both IMAP and JMAP flush paths now use _recordChangeError() consistently
- Document server-wins conflict-resolution policy in DB-SYNC.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- sendEmail dispatches on account type: IMAP keeps SMTP+APPEND path,
JMAP chains Email/set create + EmailSubmission/set in one API call
- Sent mailbox looked up by role='sent' from local DB so sent mail lands
in the right folder
- JmapClient gains uploadUrl/eventSourceUrl/capabilities from session,
supportsSubmission getter, withSubmission flag on call(), and uploadBlob()
for attachment upload before send
- Mailboxes table gains nullable role column (schema v8); _upsertJmapMailboxes
persists role from JMAP Mailbox/get response
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Include bodyValues/textBody/htmlBody/attachments in every Email/get call
during syncEmails; _upsertJmapEmails writes to email_bodies so first open
is instant even for freshly synced messages
- Extract _parseJmapBody helper shared by sync path and on-demand fetch
- Add JmapStateMismatchException; _applyPendingChangeJmap passes ifInState
and returns newState; on stateMismatch the local checkpoint is cleared so
the next cycle does a full re-sync before retrying the mutation
- Update DB-SYNC.md to reflect what has been implemented
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For JMAP accounts, setFlag/moveEmail/deleteEmail now write to the
pending_changes table instead of making direct server calls, enabling
offline-first mutation with durable retries.
flushPendingChanges() drains the queue at the start of each JMAP
sync cycle via Email/set (flag updates use keyword patches; move
updates mailboxIds; delete uses Email/set destroy). On failure the
attempt count and last error are recorded; the change remains queued.
Local DB is updated optimistically on mutation so the UI responds
immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AccountSyncManager now starts a _JmapAccountSync loop for JMAP accounts
alongside the existing _AccountSync for IMAP accounts.
_JmapAccountSync:
- Syncs mailboxes then emails for each known mailbox per cycle.
- Polls every 30 seconds (no IDLE for JMAP; EventSource deferred).
- Reuses the same exponential backoff (5–300 s) on failure.
- stop() interrupts the poll wait immediately via a Completer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EmailRepositoryImpl.syncEmails now dispatches on account type.
For JMAP accounts:
- First run: Email/query (filtered by mailbox, limit 500) + Email/get
via back-reference → upsert emails, persist state.
- Subsequent runs: Email/changes → fetch new/updated via Email/get,
delete destroyed rows, update state in sync_state.
Maps JMAP keywords ($seen, $flagged), mailboxIds, addresses, and
hasAttachment to the existing Emails table. uid stored as 0 for
JMAP emails (unused; JMAP operations go through Email/set in Step 7).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MailboxRepositoryImpl.syncMailboxes now dispatches on account type.
For JMAP accounts:
- First run: Mailbox/get → upsert all mailboxes, persist state.
- Subsequent runs: Mailbox/changes → fetch new/updated via Mailbox/get,
delete destroyed rows, update state in sync_state.
path stores the JMAP mailbox ID so Email rows can reference it via
mailboxPath consistently with the IMAP convention.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parses the JMAP Session object (RFC 8620 §2): fetches GET {jmapUrl},
extracts apiUrl and primary accountId, and wraps API calls via
call(methodCalls) which POSTs to apiUrl with Basic Auth.
Handles relative apiUrl, primaryAccounts fallback, and top-level
JMAP error responses. Covered by unit tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Protocol-agnostic queue for local mutations (flag, move, delete) that
need to be sent to the server. Enables offline-first behaviour: changes
are written here first and drained by the sync worker. Tracks attempt
count and last error for durable retries.
DB schema bumped to v6.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Protocol-agnostic checkpoint table stores one state token per
(account_id, resource_type). JMAP uses the opaque state string from
Mailbox/get and Email/get; IMAP will use a JSON checkpoint per mailbox.
DB schema bumped to v5.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>