Skip to content

Save a copy of sent mail to the Sent folder - #5

Merged
thepeak99 merged 2 commits into
masterfrom
feat/save-to-sent
Jul 17, 2026
Merged

Save a copy of sent mail to the Sent folder#5
thepeak99 merged 2 commits into
masterfrom
feat/save-to-sent

Conversation

@thepeak99

Copy link
Copy Markdown
Member

Problem

mail-cli send only relays the message over SMTP — it never APPENDs a copy
into the sender's Sent mailbox. SMTP delivery and the IMAP Sent folder are
separate protocols; the client is responsible for filing the copy. Gmail
auto-appends (so it "works" there), but most providers don't, so sent mail left
no trace in Sent.

Fix

After a successful send, file an identical copy (the exact RFC822 bytes, flagged
\Seen) into the Sent folder via IMAP APPEND. Folder precedence:

--sent-folder > MAILCLI_SENT_FOLDER > \Sent special-use auto-discovery
(RFC 6154) > the literal Sent.

Design

  • Best-effort, warn-not-fail. The mail is already delivered when APPEND
    runs, so a failed copy prints a warning: and still exits 0. A non-zero exit
    would invite a retry that double-sends. If IMAP isn't configured, saving is
    skipped with a one-line note: (not silent — a silent skip looks like the very
    bug), silenceable via MAILCLI_SAVE_SENT=0.
  • Default on, opt out with --no-save-sent / MAILCLI_SAVE_SENT=0.
  • Boundaries preserved: env values in config.rs, IMAP wire calls in imap.rs,
    runtime choices as flags. No new required env vars → existing setups unchanged.

ZoE review cuts (before coding)

  • Dropped --json on send (send had no JSON today; unrequested → YAGNI).
  • Dropped a --save-sent force flag; collapsed to a single bool.

Changes

  • config.rs: Env::sent_folder() (Option) + save_sent() (Result)
  • imap.rs: append_to_sent() + discover_sent() (\Sent special-use)
  • smtp.rs: formatted() exposes the sent bytes (single source of truth)
  • main.rs: --sent-folder / --no-save-sent; best-effort save_to_sent()
  • Cargo.toml: imap-proto direct dep (pinned) to name NameAttribute::Sent

Tests

  • Failing-first (TDD): config unit tests + tests/sent_integration.rs written
    and confirmed red before implementation.
  • Unit: sent_folder/save_sent resolution matrix (38 lib + 33 bin pass).
  • Integration (GreenMail, MAILCLI_IT=1): the copy lands in Sent flagged \Seen;
    discovery selects a valid mailbox. Verified passing against a live server.
  • cargo clippy --all-targets and cargo fmt --check clean.

Docs / skill

  • README (config table, "Saving to Sent" section, testing cmd), AGENTS.md, and the
    bundled skill (SKILL.md + reference.md + evals.json, +eval 8) updated.
  • install-skill --force output verified byte-identical to skill/.
  • CHANGELOG [Unreleased] entry (minor, additive).

Co-authored by PeakBot!

SMTP delivery never files a copy in the sender's Sent mailbox — that
lives on IMAP and the client must APPEND it. `send` did neither, so
sent mail vanished from Sent on every provider that doesn't auto-file
(i.e. most of them).

After a successful send, file an identical copy (the exact RFC822 bytes,
flagged \Seen) into the Sent folder via IMAP APPEND. Folder precedence:
--sent-folder > MAILCLI_SENT_FOLDER > \Sent special-use auto-discovery
(RFC 6154) > the literal "Sent".

Best-effort by design: the mail is already delivered when APPEND runs, so
a failed copy warns and still exits 0 — a non-zero exit would invite a
retry that double-sends. Default on; --no-save-sent / MAILCLI_SAVE_SENT=0
opt out; a one-line note (not a failure) when IMAP isn't configured.

- config.rs: Env::sent_folder() (Option) + save_sent() (Result<bool>)
- imap.rs: append_to_sent() + discover_sent() (\Sent special-use)
- smtp.rs: formatted() exposes the sent bytes (single source of truth)
- main.rs: --sent-folder / --no-save-sent; best-effort save_to_sent()
- tests: config unit tests + tests/sent_integration.rs (GreenMail, MAILCLI_IT=1)
- docs+skill synced (install-skill output byte-identical); CHANGELOG

Co-authored by PeakBot!
The prior integration tests self-skip without MAILCLI_IT=1 and CI never
set it, so the IMAP/SMTP-backed tests were dead weight there. Fix both:

- tests/send_e2e.rs: a real send→INBOX+Sent loop. Builds the message
  with mail-cli's real smtp::build, takes the real smtp::formatted bytes,
  delivers over GreenMail, then files the copy with mail-cli's real
  imap::append_to_sent and asserts it lands in Sent flagged \Seen. Only
  the encrypted transport hop is substituted: mail-cli's SMTP TLS (lettre)
  trusts bundled webpki roots and cannot validate GreenMail's self-signed
  cert, and there is no accept-invalid-certs escape hatch by design; the
  delivered payload is byte-identical. Also a binary smoke test asserting
  the new flags parse and that an unreachable SMTP host is a fatal error.
- .github/workflows/ci.yml: new `integration (GreenMail)` job runs
  search_integration + sent_integration + send_e2e with MAILCLI_IT=1
  against a GreenMail service container (waits for the ports first,
  serialized with --test-threads=1).
- lib.rs exposes `smtp` for the e2e test; docs/CHANGELOG updated.

Co-authored by PeakBot!
@thepeak99
thepeak99 merged commit 7da7930 into master Jul 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant