feat(mail): instrument protocol syncs and log destructive IMAP ops #374

Merged
KofTwentyTwo merged 1 commits from story/obs-5-mail-observability into dev 2026-08-01 00:19:17 +00:00
KofTwentyTwo commented 2026-08-01 00:16:33 +00:00 (Migrated from github.com)

Summary

  • Wraps IMAP/JMAP folder and message sync in tracing spans keyed by account/folder, logging completion counts (fetched/oversized/skipped) at INFO.
  • SMTP send attempts and their outcome are logged with the recipient COUNT only, never addresses.
  • Every destructive IMAP mutation (MOVE, DELETE + UID-scoped EXPUNGE) is logged with its UID set and the UIDVALIDITY guard's pass/fail result before the command reaches the server, so a log exists even if the guard then refuses the op. JMAP mirrors the pre-log for its Email/set move/destroy, keyed by the JMAP object id.
  • The IMAP UIDVALIDITY fail-closed guard is unchanged in behavior (restructured only so the pass/fail boolean is available for logging before the early return); all pre-existing UIDVALIDITY/UIDPLUS/MOVE-capability tests pass unmodified.
  • No proto changes (this is issue #361 / OBS-5, mail protocol observability only).

Refs #361

Details

IMAP (imap.rs)

  • #[tracing::instrument(..., err)] on sync_folder_messages_batched and MailBackend::sync_folders.
  • Completion logs: imap folder sync complete (fetched/oversized/skipped) and imap folder list sync complete (folders count).
  • Destructive pre-log in apply_mutation_with_session: MOVE at info!, DELETE at warn!, both carrying op, folder_id, uid_set, uidvalidity_guard_passed before the guard's early return.

JMAP (jmap.rs)

  • Same #[tracing::instrument(..., err)] + completion-count pattern on sync_folders/sync_messages.
  • apply_mutation pre-logs destructive MOVE/DELETE keyed by the JMAP object id (no UIDVALIDITY-equivalent to guard).

SMTP (smtp.rs)

  • send_message/send_email_with_transport log smtp send attempt before dispatch, then smtp send succeeded or smtp send failed with recipient_count (1, +1 for a non-blank cc) -- never the address.

Error-context (scoped out)

Did not do a broad #[source]/#[from] refactor of MailError's String-folding variants -- that's a pervasive change across both imap.rs and jmap.rs; flagging as a follow-up story rather than doing it here.

Test-infrastructure note

Initial hand-rolled tracing::Subscriber-per-test (mirroring nuncio-filter/src/webhook.rs's pattern) was flaky under cargo test's parallel harness: tracing-core caches a callsite's Interest process-wide on first hit, and several pre-existing tests hit the exact same log call sites with no subscriber installed, permanently poisoning the cache to "never" for that callsite before my thread-local subscriber could register. Switched to the tracing-test crate (added as a nuncio-mail dev-dependency; explicitly an allowed option in the story brief) which installs one process-wide subscriber via Once and filters by thread name, avoiding the race. Verified stable across 10 consecutive --test-threads=8 runs.

Test plan

  • cargo fmt --all -- --check -- clean
  • cargo check-all (clippy --all-targets --workspace -D warnings) -- clean, zero warnings
  • cargo test-all -- all green, 0 ignored (workspace-wide; nuncio-mail: 74 tests)
  • New tests: destructive IMAP MOVE/DELETE pre-logging (including the guard-failure path where zero commands reach the server), IMAP sync completion counts, SMTP send attempt/success/failure with recipient-count-only + no-address assertion
  • Grepped the diff: no message bodies, subjects, sender/recipient addresses, credentials, or auth headers in any tracing macro added
## Summary - Wraps IMAP/JMAP folder and message sync in tracing spans keyed by account/folder, logging completion counts (fetched/oversized/skipped) at INFO. - SMTP send attempts and their outcome are logged with the recipient COUNT only, never addresses. - Every destructive IMAP mutation (MOVE, DELETE + UID-scoped EXPUNGE) is logged with its UID set and the UIDVALIDITY guard's pass/fail result **before** the command reaches the server, so a log exists even if the guard then refuses the op. JMAP mirrors the pre-log for its `Email/set` move/destroy, keyed by the JMAP object id. - The IMAP UIDVALIDITY fail-closed guard is unchanged in behavior (restructured only so the pass/fail boolean is available for logging before the early return); all pre-existing UIDVALIDITY/UIDPLUS/MOVE-capability tests pass unmodified. - No proto changes (this is issue #361 / OBS-5, mail protocol observability only). Refs #361 ## Details ### IMAP (`imap.rs`) - `#[tracing::instrument(..., err)]` on `sync_folder_messages_batched` and `MailBackend::sync_folders`. - Completion logs: `imap folder sync complete` (fetched/oversized/skipped) and `imap folder list sync complete` (folders count). - Destructive pre-log in `apply_mutation_with_session`: MOVE at `info!`, DELETE at `warn!`, both carrying `op`, `folder_id`, `uid_set`, `uidvalidity_guard_passed` before the guard's early return. ### JMAP (`jmap.rs`) - Same `#[tracing::instrument(..., err)]` + completion-count pattern on `sync_folders`/`sync_messages`. - `apply_mutation` pre-logs destructive MOVE/DELETE keyed by the JMAP object id (no UIDVALIDITY-equivalent to guard). ### SMTP (`smtp.rs`) - `send_message`/`send_email_with_transport` log `smtp send attempt` before dispatch, then `smtp send succeeded` or `smtp send failed` with `recipient_count` (1, +1 for a non-blank `cc`) -- never the address. ### Error-context (scoped out) Did not do a broad `#[source]`/`#[from]` refactor of `MailError`'s `String`-folding variants -- that's a pervasive change across both `imap.rs` and `jmap.rs`; flagging as a follow-up story rather than doing it here. ### Test-infrastructure note Initial hand-rolled `tracing::Subscriber`-per-test (mirroring `nuncio-filter/src/webhook.rs`'s pattern) was flaky under `cargo test`'s parallel harness: `tracing-core` caches a callsite's `Interest` process-wide on first hit, and several pre-existing tests hit the exact same log call sites with no subscriber installed, permanently poisoning the cache to "never" for that callsite before my thread-local subscriber could register. Switched to the `tracing-test` crate (added as a `nuncio-mail` dev-dependency; explicitly an allowed option in the story brief) which installs one process-wide subscriber via `Once` and filters by thread name, avoiding the race. Verified stable across 10 consecutive `--test-threads=8` runs. ## Test plan - [x] `cargo fmt --all -- --check` -- clean - [x] `cargo check-all` (clippy `--all-targets --workspace -D warnings`) -- clean, zero warnings - [x] `cargo test-all` -- all green, 0 ignored (workspace-wide; `nuncio-mail`: 74 tests) - [x] New tests: destructive IMAP MOVE/DELETE pre-logging (including the guard-failure path where zero commands reach the server), IMAP sync completion counts, SMTP send attempt/success/failure with recipient-count-only + no-address assertion - [x] Grepped the diff: no message bodies, subjects, sender/recipient addresses, credentials, or auth headers in any `tracing` macro added
Sign in to join this conversation.