* docs: mark M5 complete (feature surface done), set M6 as next
* docs: correct HANDOFF service count to eight
---------
Co-authored-by: James Maes <jmaes@dmdbrands.com>
SendMessage previously always sent from the first configured account
with no way to choose, so it silently used the wrong identity the
moment a second account existed. Add an optional account_id field to
SendMessageRequest; when set, resolve that specific account and fail
with a new AccountNotFound error if it does not exist, rather than
falling back to a different account. Omitting account_id preserves
the prior first-account behavior. Thread the selector through
ComposeRequest, resolve_sending_account, the CLI's mail send --account
flag, and regenerate the nuncio.v1 descriptor golden.
Refs #196, #170
Co-authored-by: James Maes <jmaes@dmdbrands.com>
* feat(store): add per-folder sync-state checkpoint table + CRUD
Adds folder_sync_state(account_id, folder_id, state) with
get/save methods and a round-trip test, giving inbound sync a place
to persist a per-mailbox resumption token between runs.
Refs #195#184
* feat(mail): honor since_state for incremental IMAP fetch + bound FETCH stalls
ImapEngine now derives a real UIDNEXT-based checkpoint after selecting a
folder and narrows the UID FETCH to {uidnext}:* when a checkpoint is
supplied, instead of always re-fetching 1:* and returning a state
fabricated from emails.len(). An unparseable checkpoint falls back to a
full fetch but is surfaced distinctly. Each per-item FETCH read is bounded
by a named timeout, surfacing MailError::FetchStalled instead of hanging.
MockMailBackend captures the since_state arg and simulates an incremental
(empty) second fetch. Session-level duplex tests assert the narrowed FETCH
range and the bounded stall.
Refs #195#184#183
* feat(sync): persist and thread the folder checkpoint, publish sync progress
fetch_and_persist now reads the folder's stored checkpoint, passes it to
the backend for a narrowed fetch, and persists the returned checkpoint only
after that folder's messages land -- so the second sync of an account/folder
is genuinely incremental. Adds CoreEvent::SyncProgress (per folder) as an
internal signal; map_core_event drops it from the gRPC stream since it has
no wire-contract case (proto unchanged).
Refs #195#184
* fix(mail): track UIDVALIDITY in sync checkpoint to prevent mail loss
A checkpoint stored only UIDNEXT, so a mailbox renumber (UIDVALIDITY
change, UIDs restart low) would drive {old_uidnext}:* against the new low
UIDs -- per RFC 3501 n:* returns only the single highest message and
silently skips the rest, permanently dropping mail. The checkpoint now
encodes {uidvalidity}:{uidnext}; the incremental narrowing applies only
when the stored UIDVALIDITY still matches the mailbox's current one. A
mismatch (renumber), a missing current UIDVALIDITY, or a legacy
bare-number/unparseable checkpoint falls back to a safe full 1:* re-fetch
with a debug-visible warn, then rewrites the checkpoint fresh.
Refs #195#184
---------
Co-authored-by: James Maes <jmaes@dmdbrands.com>
* feat(store): persist per-account IMAP/SMTP TLS modes; add account CRUD
Adds a backfill-safe imap_tls_mode/smtp_tls_mode column migration,
binds+reads both modes in save_account/list_accounts (previously
validated then silently discarded and hardcoded back to ImplicitTls),
and adds get_account/delete_account. Regression test proves the modes
survive a save/list/get round-trip.
Refs #194#182
* feat(mail): branch IMAP/SMTP transport on TlsMode; add connection probes
Threads TlsMode through ImapEngine/ImapDualSocketManager and
SmtpTransportEngine so implicit TLS, STARTTLS, and plain are genuine
transport paths (a unified MailStream plus a real IMAP STARTTLS
negotiation), not validate-and-discard. Adds bounded connect timeout
and per-engine probe_connection backing TestAccountConnection, with
mock/loopback-only setup tests for each mode.
Refs #194#182
* feat(accounts): edit/delete/test account lifecycle over gRPC
Implements Accounts UpdateAccount/RemoveAccount/TestAccountConnection in
the daemon (update mirrors add_account's keyring rollback discipline;
test probes real IMAP+SMTP via an injectable tester, honoring persisted
TLS modes), wires the configured TLS modes into build_mail_backend/
build_smtp_sender, and replaces the fabricated CLI account
show/edit/delete/test handlers with real thin gRPC clients. Adds an
offline full-daemon lifecycle E2E and unauth-rejection coverage for all
three new RPCs.
Refs #194#182
* fix(accounts): restore prior credential on update rollback
A password rotation that fails to persist previously deleted the
just-written secret, destroying the working credential the key already
held and orphaning the still-present account row. Snapshot the prior
secret first and restore it on save failure (falling back to delete
only when none existed). Regression tests force save_account to fail
and assert the original secret survives.
Refs #194#182
* docs(accounts): scope TestAccountConnection SMTP claim to reachability
The SMTP leg only dials and negotiates TLS/STARTTLS; it performs no AUTH
(lettre exposes no clean auth-only probe on AsyncSmtpTransport), while
IMAP genuinely logs in. Clarify the proto doc for smtp_ok/smtp_error and
the CLI output wording so nothing implies SMTP authentication was
verified. Doc-only proto change; golden unchanged.
Refs #194#182
---------
Co-authored-by: James Maes <jmaes@dmdbrands.com>
If set_secret succeeds but the subsequent save_account fails, the
credential was left in the keyring with no account row referencing it.
add_account now deletes the just-written secret before returning,
appending any rollback-delete failure to the original error message
without masking it.
Refs #197, #169
Co-authored-by: James Maes <jmaes@dmdbrands.com>