Commit Graph
13 Commits
Author SHA1 Message Date
agentloopandClaude Opus 4.7 0141d86361 fix(imap): remap local id to new UID after MOVE so caches survive
IMAP UIDs are mailbox-scoped, so MOVE assigns a fresh UID in the
destination folder. The flush previously discarded the response
from `client.uidMove(...)`, so the local row kept the *source*
UID while its `mailbox_path` already pointed at the destination.
Two things broke:

- Deletion reconciliation, which runs per mailbox and compares
  local UIDs to the server's `ALL` search result, would not find
  the source UID in the destination mailbox and wipe the row —
  taking the cached body and queued undo with it.
- `UndoLog` rows kept referencing the old `accountId:mailbox:uid`
  id, so undo had to fall back to a Message-ID lookup just to
  rediscover the moved message.

The fix captures the RFC 4315 `COPYUID` response code that
modern `UIDPLUS` servers attach to `MOVE`/`COPY` (already exposed
as `GenericImapResult.responseCodeCopyUid` in `enough_mail`).
When that's missing — i.e. the server doesn't support UIDPLUS —
we fall back to `UID SEARCH HEADER Message-ID …` in the
destination mailbox. Either way the local id is rewritten in
place to `accountId:destMailbox:newUid` and the cascading
`email_bodies`, `threads`, `pending_changes`, and `undo_actions`
references are updated in the same transaction.

`_reconcileDeletedImap` now also skips rows whose
`move`/`snooze`/`unsnooze` is still queued in `pending_changes`,
so the optimistic local move can't be wiped between the
optimistic write and the server flush.

Closes #539

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 13:19:04 +00:00
Thomas GüttlerandClaude Sonnet 4.6 650c7a70f5 docs: update DB-SYNC.md — all planned features now implemented
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>
2026-04-20 06:14:29 +02:00
Thomas GüttlerandClaude Sonnet 4.6 93ac5afbcf feat: conflict resolution hardening — server-wins policy, max-retry eviction
- 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>
2026-04-19 21:05:48 +02:00
Thomas GüttlerandClaude Sonnet 4.6 8d8dbc33db feat: JMAP send via EmailSubmission/set; role column on Mailboxes
- 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>
2026-04-19 17:41:21 +02:00
Thomas GüttlerandClaude Sonnet 4.6 7e34ca45de feat: JMAP body caching during sync and ifInState conflict detection
- 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>
2026-04-19 17:21:08 +02:00
Thomas GüttlerandClaude Sonnet 4.6 0797dd914b feat: JMAP outbound changes via pending_changes queue (Step 7)
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>
2026-04-19 16:25:44 +02:00
Thomas GüttlerandClaude Sonnet 4.6 559eb9a467 feat: JMAP background sync worker (Step 6)
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>
2026-04-19 16:22:09 +02:00
Thomas GüttlerandClaude Sonnet 4.6 2efeba9d2e feat: JMAP Email sync — full and incremental (Step 5)
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>
2026-04-19 16:19:03 +02:00
Thomas GüttlerandClaude Sonnet 4.6 f580cd0197 feat: JMAP Mailbox sync — full and incremental (Step 4)
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>
2026-04-19 16:15:34 +02:00
Thomas GüttlerandClaude Sonnet 4.6 0054322068 feat: add JmapClient session client (Step 3)
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>
2026-04-19 16:11:32 +02:00
Thomas GüttlerandClaude Sonnet 4.6 c6fb5154fb feat: add pending_changes table (Step 2 — outbound sync queue)
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>
2026-04-19 16:08:17 +02:00
Thomas GüttlerandClaude Sonnet 4.6 475ba34d28 feat: add sync_state table (Step 1 — DB foundation for incremental sync)
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>
2026-04-19 16:05:31 +02:00
Thomas Güttler 4cefc8aac3 deploy-android is working. 2026-04-19 15:30:42 +02:00