Closes#189. JmapEngine is now a real RFC 8620/8621 client alongside IMAP/SMTP: with_credentials performs genuine HTTP session discovery (GET /.well-known/jmap), then real Mailbox/get and Email/query/Email/get JSON-RPC calls over reqwest against the discovered apiUrl, flowing through the exact same MailBackend -> fetch_and_persist -> DatabaseEngine::save_email path IMAP already uses. JmapEngine::new (uncredentialed) keeps the deterministic no-network fallback so existing unit tests are unaffected.
Fixed the live camelCase wire bug: JmapEmail now has #[serde(rename_all = "camelCase")], so receivedAt/isUnread/bodySnippet actually populate from real server responses instead of silently defaulting. Added jmap_email_deserializes_camel_case_wire_fields as a regression test.
nunciod::sync::build_mail_backend's AccountProtocol::Jmap arm now constructs JmapEngine::with_credentials(&config.id, &config.server_host, &config.email_address, password), mirroring the IMAP arm -- a JMAP account can now actually sync.
Removed the canned fallbacks from the credentialed path: a session-discovery or JMAP-call HTTP/parse failure now surfaces as MailError::NetworkError/ParseFailed, never fake data.
Moved reqwest from [dev-dependencies] to [dependencies] in nuncio-mail (JMAP now makes real HTTP calls from production code).
Added wiremock to nunciod's [dev-dependencies].
Tests rewritten off fake data
crates/nunciod/src/sync.rs's run_account_sync_persists_real_backend_messages_and_reports_success previously asserted on JmapEngine::new's hardcoded 3-folder/1-message static data. It's rewritten to mount a wiremock::MockServer (session + Mailbox/get + Email/query + Email/get stubs) and point the sample account's server_host at the mock server's .uri(), proving run_account_sync -> real HTTP JMAP fetch -> DatabaseEngine::save_email -> db.get_message(...) end to end.
run_all_accounts_sync_persists_across_multiple_accounts had the same problem (it would now hit jmap.nuncio.mx for real once credentials flow through build_mail_backend) and is rewritten the same way, with two independent mock servers.
New tests
crates/nuncio-mail/tests/wiremock_jmap_test.rs: jmap_engine_sync_folders_and_messages_through_mail_backend_trait drives JmapEngine::with_credentials through the MailBackend trait itself (sync_folders/sync_messages), not just the static parse helpers, against a wiremock server standing in for a real JMAP host.
jmap_engine_sync_folders_surfaces_session_discovery_failure: proves a 500 from session discovery surfaces as a genuine MailError, never a silent fallback.
nuncio-mail: 42 unit tests + 3 wiremock integration tests, all passing.
nunciod: 100 unit tests + 6 integration test files, all passing (includes the two rewritten JMAP sync tests).
Full workspace cargo test-all: every crate green, 0 failed, 0 ignored.
Local pre-commit hook (fmt + clippy -D warnings + full test suite) passed on commit.
No proto changes -- this story reuses the existing nuncio.v1.Mail service unchanged, as required by #189.
Notes for PO review
JMAP-native outbound send (Email/set/EmailSubmission) remains a stub, matching #189's explicit scope note that MailBackend::send_email is never called in production for either protocol today (SMTP send is separate, send.rs).
Incremental (since_state-aware) JMAP sync is out of scope here, matching IMAP's current same-gap (tracked as M5-S2 per the issue).
Email/query currently has no bound on result size for very large mailboxes; that's inherited from IMAP's equivalent full-refetch behavior and is the same size/scope tradeoff the issue calls out.
## Summary
- Closes #189. `JmapEngine` is now a real RFC 8620/8621 client alongside IMAP/SMTP: `with_credentials` performs genuine HTTP session discovery (`GET /.well-known/jmap`), then real `Mailbox/get` and `Email/query`/`Email/get` JSON-RPC calls over `reqwest` against the discovered `apiUrl`, flowing through the exact same `MailBackend` -> `fetch_and_persist` -> `DatabaseEngine::save_email` path IMAP already uses. `JmapEngine::new` (uncredentialed) keeps the deterministic no-network fallback so existing unit tests are unaffected.
- Fixed the live camelCase wire bug: `JmapEmail` now has `#[serde(rename_all = "camelCase")]`, so `receivedAt`/`isUnread`/`bodySnippet` actually populate from real server responses instead of silently defaulting. Added `jmap_email_deserializes_camel_case_wire_fields` as a regression test.
- `nunciod::sync::build_mail_backend`'s `AccountProtocol::Jmap` arm now constructs `JmapEngine::with_credentials(&config.id, &config.server_host, &config.email_address, password)`, mirroring the IMAP arm -- a JMAP account can now actually sync.
- Removed the canned fallbacks from the credentialed path: a session-discovery or JMAP-call HTTP/parse failure now surfaces as `MailError::NetworkError`/`ParseFailed`, never fake data.
- Moved `reqwest` from `[dev-dependencies]` to `[dependencies]` in `nuncio-mail` (JMAP now makes real HTTP calls from production code).
- Added `wiremock` to `nunciod`'s `[dev-dependencies]`.
## Tests rewritten off fake data
- `crates/nunciod/src/sync.rs`'s `run_account_sync_persists_real_backend_messages_and_reports_success` previously asserted on `JmapEngine::new`'s hardcoded 3-folder/1-message static data. It's rewritten to mount a `wiremock::MockServer` (session + `Mailbox/get` + `Email/query` + `Email/get` stubs) and point the sample account's `server_host` at the mock server's `.uri()`, proving `run_account_sync` -> real HTTP JMAP fetch -> `DatabaseEngine::save_email` -> `db.get_message(...)` end to end.
- `run_all_accounts_sync_persists_across_multiple_accounts` had the same problem (it would now hit `jmap.nuncio.mx` for real once credentials flow through `build_mail_backend`) and is rewritten the same way, with two independent mock servers.
## New tests
- `crates/nuncio-mail/tests/wiremock_jmap_test.rs`: `jmap_engine_sync_folders_and_messages_through_mail_backend_trait` drives `JmapEngine::with_credentials` through the `MailBackend` trait itself (`sync_folders`/`sync_messages`), not just the static parse helpers, against a wiremock server standing in for a real JMAP host.
- `jmap_engine_sync_folders_surfaces_session_discovery_failure`: proves a 500 from session discovery surfaces as a genuine `MailError`, never a silent fallback.
## Gate evidence
```
cargo fmt --all -- --check # clean
cargo check-all # clippy --all-targets --workspace -D warnings: clean
cargo test-all # all green, 0 ignored (see per-crate counts below)
```
- `nuncio-mail`: 42 unit tests + 3 wiremock integration tests, all passing.
- `nunciod`: 100 unit tests + 6 integration test files, all passing (includes the two rewritten JMAP sync tests).
- Full workspace `cargo test-all`: every crate green, 0 failed, 0 ignored.
- Local pre-commit hook (fmt + clippy -D warnings + full test suite) passed on commit.
No proto changes -- this story reuses the existing `nuncio.v1.Mail` service unchanged, as required by #189.
## Notes for PO review
- JMAP-native outbound send (`Email/set`/`EmailSubmission`) remains a stub, matching #189's explicit scope note that `MailBackend::send_email` is never called in production for either protocol today (SMTP send is separate, `send.rs`).
- Incremental (`since_state`-aware) JMAP sync is out of scope here, matching IMAP's current same-gap (tracked as M5-S2 per the issue).
- `Email/query` currently has no bound on result size for very large mailboxes; that's inherited from IMAP's equivalent full-refetch behavior and is the same size/scope tradeoff the issue calls out.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
JmapEngineis now a real RFC 8620/8621 client alongside IMAP/SMTP:with_credentialsperforms genuine HTTP session discovery (GET /.well-known/jmap), then realMailbox/getandEmail/query/Email/getJSON-RPC calls overreqwestagainst the discoveredapiUrl, flowing through the exact sameMailBackend->fetch_and_persist->DatabaseEngine::save_emailpath IMAP already uses.JmapEngine::new(uncredentialed) keeps the deterministic no-network fallback so existing unit tests are unaffected.JmapEmailnow has#[serde(rename_all = "camelCase")], soreceivedAt/isUnread/bodySnippetactually populate from real server responses instead of silently defaulting. Addedjmap_email_deserializes_camel_case_wire_fieldsas a regression test.nunciod::sync::build_mail_backend'sAccountProtocol::Jmaparm now constructsJmapEngine::with_credentials(&config.id, &config.server_host, &config.email_address, password), mirroring the IMAP arm -- a JMAP account can now actually sync.MailError::NetworkError/ParseFailed, never fake data.reqwestfrom[dev-dependencies]to[dependencies]innuncio-mail(JMAP now makes real HTTP calls from production code).wiremocktonunciod's[dev-dependencies].Tests rewritten off fake data
crates/nunciod/src/sync.rs'srun_account_sync_persists_real_backend_messages_and_reports_successpreviously asserted onJmapEngine::new's hardcoded 3-folder/1-message static data. It's rewritten to mount awiremock::MockServer(session +Mailbox/get+Email/query+Email/getstubs) and point the sample account'sserver_hostat the mock server's.uri(), provingrun_account_sync-> real HTTP JMAP fetch ->DatabaseEngine::save_email->db.get_message(...)end to end.run_all_accounts_sync_persists_across_multiple_accountshad the same problem (it would now hitjmap.nuncio.mxfor real once credentials flow throughbuild_mail_backend) and is rewritten the same way, with two independent mock servers.New tests
crates/nuncio-mail/tests/wiremock_jmap_test.rs:jmap_engine_sync_folders_and_messages_through_mail_backend_traitdrivesJmapEngine::with_credentialsthrough theMailBackendtrait itself (sync_folders/sync_messages), not just the static parse helpers, against a wiremock server standing in for a real JMAP host.jmap_engine_sync_folders_surfaces_session_discovery_failure: proves a 500 from session discovery surfaces as a genuineMailError, never a silent fallback.Gate evidence
nuncio-mail: 42 unit tests + 3 wiremock integration tests, all passing.nunciod: 100 unit tests + 6 integration test files, all passing (includes the two rewritten JMAP sync tests).cargo test-all: every crate green, 0 failed, 0 ignored.No proto changes -- this story reuses the existing
nuncio.v1.Mailservice unchanged, as required by #189.Notes for PO review
Email/set/EmailSubmission) remains a stub, matching #189's explicit scope note thatMailBackend::send_emailis never called in production for either protocol today (SMTP send is separate,send.rs).since_state-aware) JMAP sync is out of scope here, matching IMAP's current same-gap (tracked as M5-S2 per the issue).Email/querycurrently has no bound on result size for very large mailboxes; that's inherited from IMAP's equivalent full-refetch behavior and is the same size/scope tradeoff the issue calls out.🤖 Generated with Claude Code