Applied from the cleanup-agent report (separate from the security
audit's 18 fixes earlier today):
HIGH:
- HIGH-1: replaced hand-rolled civil_from_unix + chrono_rfc3339_now
with chrono::Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true).
~50 LOC of brittle Hinnant-algorithm civil-calendar math gone.
Plus 5 unit tests retired with the function. chrono pulls a small
default-features=false slice (clock + serde-not-included).
- HIGH-2: extracted shared reject_imap_unsafe() helper. validate_mailbox
and the mail_thread message_id check both go through it. The
message_id check now also rejects '{' (literal-form opener) for
symmetry — same byte set as validate_mailbox.
- HIGH-3: mail_reply uses smtp::ensure_angle_brackets() on the parent
Message-Id + each References entry. mail-parser strips brackets;
lettre writes through verbatim; strict RFC-5322 receivers will drop
the threading link if brackets are missing. Now canonical.
- HIGH-4: extract_addr moved from tools.rs to smtp.rs as
smtp::extract_bare_addr. Module hygiene — RFC-5322 mailbox parsing
belongs in the SMTP-side module, not the rmcp surface.
- HIGH-5: mail_reply Re:-prefix check now non-allocating —
subject.get(..3).map(|s| s.eq_ignore_ascii_case("re:")) instead of
.to_ascii_lowercase().starts_with("re:") which allocated a fresh
String for the comparison.
MED:
- MED-1: dropped thiserror dep (workspace + crate). Never derived.
- MED-6: ReadOutput.headers is now typed BTreeMap<String,String> instead
of serde_json::Value::Object. Wire JSON shape unchanged; downstream
consumers can .get(name) directly without the .as_str() dance.
- MED-8: fetch_to_list_entry returns Option<ListEntry> and drops
entries when the server omits UID. Was uid=0 silent fallback;
now we log a warning and skip.
- MED-10: introduced PRIMARY_BODY_PART = 0 const, replaced 4 magic 0s
at parsed.body_text(0) / parsed.body_html(0) call sites.
- MED-11: skip insert of empty-valued headers in the flat headers map.
was producing "key":"" entries for headers mail-parser couldn't
render to a flat string.
LOW:
- LOW-1: collapsed MailService { inner: Arc<MailInner> } to
MailService { config: Arc<Config> }. The MailInner wrapper served no
purpose with a single field.
- LOW-2: rewrote to_field_collapses_to_vec test with let bindings
instead of single-arm match.
- LOW-4: format_imap_since year range tightened from 1900..=9999 to
1970..=9999 (unix-epoch floor; we don't use pre-epoch IMAP SINCE).
- LOW-5: promoted max_encoded to MAX_ATTACHMENT_BASE64_BYTES const.
- LOW-6: SendOutput now #[derive(Serialize)] — mail_send and
mail_reply tools use it via the new IntoMcpError trait instead of
serde_json::json!() boilerplate.
- LOW-7: added IntoMcpError trait — anyhow::Result<T: Serialize>
-> Result<String, String>. Removes 10 copy-pasted
.map_err(|e| format!("{e:#}"))? + serialize chains.
- LOW-9: documented the 20 MB read cap vs 25 MB send cap asymmetry
via comments on both consts.
- LOW-10: UID MOVE fallback log demoted to trace! and renamed field
imap_mv_err so log analytics doesn't flag the graceful fallback as
an error.
- LOW-13: SMTP From header built via Mailbox::new() instead of
format!("{} <{}>")-then-.parse(). One alloc, one parse pass gone.
INFO:
- INFO-3: lettre 'hostname' feature dropped from Cargo.toml. We
override Message-ID with our own UUID@from_domain; lettre never
needed the system hostname.
Deferred from this pass:
- MED-2 with_session wrapper — substantial refactor across 8 IMAP
functions for moderate DRY win; saving for a Phase E lifecycle pass.
- MED-7 / LOW-14 typed address shape — would change wire JSON for
mail_inbox_list/read; backwards-incompatible.
- MED-12 narrow UID MOVE fallback — needs async-imap error-variant
taxonomy research.
- LOW-11 / LOW-12 stringly format / action enums — auditor flagged as
marginal; keep stringly with description-enumerated values.
Test count: 33 -> 31 (-5 civil_from_unix, +3 extract_bare_addr, +3
ensure_angle_brackets, -1 stale extract_addr in wrong module). All
passing. Wire smoke verified — send / list / read round trip clean,
headers map is now a flat dict with no empties, chrono-rendered
timestamps match the prior shape.
83 lines
2.8 KiB
TOML
83 lines
2.8 KiB
TOML
# Cargo workspace root for mail-mcp.
|
|
#
|
|
# One crate today (mail-mcp), workspace shape so we can grow without
|
|
# rework. Same pattern as aldabra.
|
|
#
|
|
# Workspace deps pinned here; each crate references with `foo = { workspace = true }`.
|
|
[workspace]
|
|
resolver = "2"
|
|
members = ["crates/mail-mcp"]
|
|
|
|
[workspace.package]
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
license = "MIT"
|
|
repository = "http://192.168.0.5:3001/Sulkta-Coop/mail-mcp"
|
|
authors = ["Cobb <cobb@sulkta.com>", "Kayos <kayos@sulkta.com>"]
|
|
|
|
[workspace.dependencies]
|
|
tokio = { version = "1", features = ["full"] }
|
|
|
|
# MCP — same crate aldabra uses. Pinned to 0.1 series; bump together
|
|
# across repos when we move.
|
|
rmcp = { version = "0.1", features = ["server", "transport-io"] }
|
|
schemars = "0.8"
|
|
|
|
# SMTP — lettre handles RFC-5322 headers (Date, Message-ID), STARTTLS,
|
|
# multipart/alternative + multipart/mixed natively. rustls-tls so we
|
|
# don't pull openssl. No `hostname` feature — we override Message-ID
|
|
# with our own UUID@<from_domain>, so lettre never needs the system
|
|
# hostname.
|
|
lettre = { version = "0.11", default-features = false, features = [
|
|
"tokio1-rustls-tls",
|
|
"smtp-transport",
|
|
"builder",
|
|
] }
|
|
|
|
# IMAP — async-imap is tokio-native and supports UID-based addressing
|
|
# (which we use throughout the API surface).
|
|
async-imap = { version = "0.10", default-features = false, features = ["runtime-tokio"] }
|
|
tokio-rustls = "0.26"
|
|
rustls = { version = "0.23", default-features = false, features = ["std", "tls12", "ring"] }
|
|
rustls-pki-types = "1"
|
|
webpki-roots = "0.26"
|
|
|
|
# Email parsing on the read side. mail-parser is fast, no_std-friendly,
|
|
# and handles the RFC-5322 + MIME zoo without surprises.
|
|
mail-parser = "0.9"
|
|
|
|
# Config + serde
|
|
toml = "0.8"
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
|
|
# UUID for Message-ID generation when lettre's auto isn't appropriate
|
|
# (we want our own domain in the Message-ID, not lettre's local-hostname
|
|
# default).
|
|
uuid = { version = "1", features = ["v4"] }
|
|
|
|
# Base64 for attachments
|
|
base64 = "0.22"
|
|
|
|
# Errors — anyhow at module boundaries; rmcp tool methods return
|
|
# `Result<String, String>` and convert via the IntoMcpError trait.
|
|
anyhow = "1"
|
|
|
|
# Stream adapter (.next() on async-imap fetch streams)
|
|
futures = "0.3"
|
|
|
|
# RFC-3339 timestamps for SendOutput.sent_at and parsed header dates.
|
|
# default-features=false keeps us off the system-locale crate; we only
|
|
# need the UTC clock + serialization helpers.
|
|
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
|
|
|
# Logging — stderr only, never stdout (stdio is the MCP transport).
|
|
tracing = "0.1"
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
|
|
# Dirs lookup for `~/.config/mail-mcp/config.toml` default path
|
|
dirs = "5"
|
|
|
|
# Shell-style env-var expansion for the `password_file` setting
|
|
# (`~/.config/...` paths). shellexpand is small + maintained.
|
|
shellexpand = "3"
|