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>
This commit is contained in:
Thomas Güttler
2026-04-19 17:41:21 +02:00
co-authored by Claude Sonnet 4.6
parent 7e34ca45de
commit 8d8dbc33db
6 changed files with 384 additions and 14 deletions
+8 -7
View File
@@ -27,6 +27,10 @@ This document covers the mail-to-database sync layer only, not the UI.
at the start of each sync cycle.
- Email bodies are fetched on demand via `Email/get` with `bodyValues` and cached in
`email_bodies` so subsequent opens are instant.
- `syncEmails` fetches `bodyValues` during the sync pass so bodies are cached without
a separate on-demand fetch.
- `flushPendingChanges` passes `ifInState` to every `Email/set`; a `stateMismatch`
response clears the local checkpoint and triggers a full re-sync before retrying.
### IMAP
@@ -56,18 +60,15 @@ This document covers the mail-to-database sync layer only, not the UI.
### JMAP hardening
- **Body caching during sync**: `syncEmails` currently syncs headers only. Include
`bodyValues` + `htmlBody`/`textBody` in the `Email/get` properties list so bodies
are written to `email_bodies` during the sync pass, not just on first open.
- **JMAP send**: implement outgoing mail via `EmailSubmission/set` in addition to the
current SMTP path.
- **Push instead of polling**: upgrade `_JmapAccountSync._wait()` to use an
`EventSource` connection to the JMAP push URL when the server advertises push
capability. Fall back to 30 s polling when push is unavailable.
- **Conflict handling**: pass `ifInState` to `Email/set` in `flushPendingChanges` so
the server can reject a stale mutation; retry the affected change after re-syncing.
### Shared / cross-protocol
- **Explicit conflict-resolution strategy**: decide and document the policy (last-write-
wins vs. server-wins) and implement it consistently across both protocols.
- **Conflict-resolution hardening**: document and enforce the server-wins policy
consistently — check `notUpdated`/`notDestroyed` per-item errors in JMAP `Email/set`
responses, handle IMAP `NO`/`BAD` gracefully, and evict changes that exceed a
maximum retry threshold (e.g. 5 attempts) to prevent queues from growing unboundedly.