diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 0000000..dd25de4 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,9 @@ +[profile.default] +retries = 0 +slow-timeout = { period = "30s", terminate-after = 2 } +failure-output = "immediate-final" +final-status-level = "fail" + +[profile.ci] +retries = 1 +fail-fast = false diff --git a/CHANGELOG.md b/CHANGELOG.md index b6d3fce..16b0607 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **MCP tasks** — added task management to support background execution and polling of long-running tools. - **Mailbox roles** — added `role_from_attributes` to parse RFC 6154 roles with fallback logic for older servers. - **Tool synchronization** — added async mutexes to serialize destructive tool executions per-account. +- **Keychain tests** — added unit tests for `Secret` (Raw/Command paths plus a keyring roundtrip via `keyring_core::mock::Store`) and for the macOS error-code classifier (-25307, -25308, -34018). ### Changed - **Mailbox detection** — replaced hardcoded mailbox names with auto-detection using RFC 6154 special-use attributes (`Trash`, `Drafts`). - **MCP transport** — replaced custom `CompatStdioWorker` with the standard `rmcp` stdio transport. - **Mailbox info** — updated `MailboxInfo` to expose `no_select`, `no_inferiors`, and `role`. - **Tool configurations** — updated all applicable tools to include `task_support = "optional"`. +- **rmcp** — bumped to 1.6 (adds 2025-11-25 protocol support; Origin validation, session store, and other HTTP-only features are not used since agentmail is stdio-only). +- **macOS keychain** — prefer the data-protection keychain backend, falling back to the legacy file-based keychain when the binary lacks the entitlement. Improves reliability in headless/launchd contexts. +- **Tests** — switched `ci-check.sh` to `cargo nextest run` (with a `cargo test` fallback) and added a `.config/nextest.toml`. + +### Fixed +- **Keychain errors** — surface `errSecNoDefaultKeychain` (-25307), `errSecInteractionNotAllowed` (-25308), and `errSecMissingEntitlement` (-34018) as typed `SecretError` variants with remediation hints, instead of opaque string failures. +- **Keychain init logging** — stopped silently swallowing platform-store initialization failures; they now log via `tracing::warn!`. ### Removed - **Account configuration** — removed explicit `trash_mailbox` and `drafts_mailbox` settings from `AccountConfig`. diff --git a/Cargo.lock b/Cargo.lock index 791a23a..4ced413 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,7 +31,7 @@ dependencies = [ "dirs", "fast_html2md", "futures", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "keyring-core", "lettre", "mail-parser", @@ -134,9 +134,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "apple-native-keyring-store" -version = "0.2.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdab02b2a5e47c080382cb729e8fc53bedf4a498200fc72a4e6c691033a3f7b" +checksum = "a7be2f067ccd8d4b4d4a66ddafe0f32a5dff31732f32dbff85fefc40929b1f72" dependencies = [ "keyring-core", "log", @@ -168,9 +168,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -220,9 +220,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auto_encoder" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1909575e56fe87516c438b6bf75100b2506200fa22e10cbcb4b18ccf2e4bcc5d" +checksum = "43bfb5cf22391514bc73a3905267b01ee10c767b256719ae69267661564aff7c" dependencies = [ "chardetng", "encoding_rs", @@ -232,15 +232,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ "aws-lc-sys", "zeroize", @@ -248,9 +248,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.1" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" dependencies = [ "cc", "cmake", @@ -266,9 +266,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -290,9 +290,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" @@ -317,9 +317,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.60" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -327,12 +327,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -382,9 +376,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -404,9 +398,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -447,9 +441,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "compression-core", "flate2", @@ -457,9 +451,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "concurrent-queue" @@ -608,13 +602,13 @@ dependencies = [ [[package]] name = "dbus" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" dependencies = [ "libc", "libdbus-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -637,9 +631,9 @@ dependencies = [ [[package]] name = "dbus-secret-service-keyring-store" -version = "0.3.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec3a0f31addb56ee6d5c55599068d2093bc737469a448b34e426603ef61f93e" +checksum = "21d8f54da401bb5eb2a4d873ac4b359f4a599df2ca8634bb5b8c045e5ee78757" dependencies = [ "dbus-secret-service", "keyring-core", @@ -738,9 +732,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "email-encoding" @@ -812,9 +806,9 @@ dependencies = [ [[package]] name = "fast_html2md" -version = "0.0.61" +version = "0.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd40b2369f490dc05d0efaa9301cafb160fe19aa86f1cfbb2cab186c852060ee" +checksum = "88587bc3328375d9866f0f71ffcfe5768fe0df79dde79c79216239711868f77f" dependencies = [ "auto_encoder", "futures-util", @@ -1035,9 +1029,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -1063,20 +1057,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", "equivalent", @@ -1124,9 +1107,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -1190,15 +1173,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -1360,9 +1342,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1370,9 +1352,9 @@ dependencies = [ [[package]] name = "imap-proto" -version = "0.16.6" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1f9b30846c3d04371159ef3a0413ce7c1ae0a8c619cd255c60b3d902553f22" +checksum = "25f6af35c6a517aea5c72314abe90134980d2ae6a763809b50c208b3e429d71f" dependencies = [ "nom 7.1.3", ] @@ -1384,7 +1366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1405,16 +1387,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1429,27 +1401,32 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", - "jni-sys 0.3.1", + "jni-macros", + "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror 2.0.18", "walkdir", - "windows-sys 0.45.0", + "windows-link", ] [[package]] -name = "jni-sys" -version = "0.3.1" +name = "jni-macros" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ - "jni-sys 0.4.1", + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", ] [[package]] @@ -1483,9 +1460,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -1495,9 +1472,9 @@ dependencies = [ [[package]] name = "keyring-core" -version = "0.7.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d8286c3ab222af2fa9e57874fe419893d967f739c7bf1743f98a88678edbf08" +checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d" dependencies = [ "log", ] @@ -1516,9 +1493,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lettre" -version = "0.11.21" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabda5859ee7c06b995b9d1165aa52c39110e079ef609db97178d86aeb051fa7" +checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349" dependencies = [ "email-encoding", "email_address", @@ -1532,9 +1509,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.184" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libdbus-sys" @@ -1577,22 +1554,22 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lol_html" -version = "2.7.2" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ff94cb6aef6ee52afd2c69331e9109906d855e82bd241f3110dfdf6185899ab" +checksum = "00aad58f6ec3990e795943872f13651e7a5fa59dca2c8f31a74faf8a0e0fb652" dependencies = [ "bitflags", "cfg-if", "cssparser", "encoding_rs", "foldhash 0.2.0", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "memchr", "mime", "precomputed-hash", @@ -1608,9 +1585,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "mail-parser" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82a3d6522697593ba4c683e0a6ee5a40fee93bc1a525e3cc6eeb3da11fd8897" +checksum = "d8a2420e9ce11c2b0583ca97ddff7ab2398c8a613154e9b72e3bafdbf767f1d7" dependencies = [ "hashify", ] @@ -1801,15 +1778,14 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", - "once_cell", "openssl-macros", "openssl-sys", ] @@ -1833,9 +1809,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", @@ -1880,9 +1856,9 @@ dependencies = [ [[package]] name = "pastey" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "percent-encoding" @@ -1928,7 +1904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -1987,18 +1963,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -2019,9 +1995,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "potential_utf" @@ -2096,7 +2072,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -2151,18 +2127,18 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -2195,9 +2171,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -2284,9 +2260,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -2338,9 +2314,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "1.4.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f542f74cf247da16f19bbc87e298cd201e912314f4083e88cdd671f44f5fcb53" +checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e" dependencies = [ "async-trait", "base64", @@ -2360,9 +2336,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "1.4.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2391e4ae47f314e70eaafb6c7bd82e495e770b935448864446302143019151f" +checksum = "6aefac48c364756e97f04c0401ba3231e8607882c7c1d92da0437dc16307904d" dependencies = [ "darling", "proc-macro2", @@ -2401,9 +2377,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "once_cell", @@ -2427,9 +2403,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -2437,9 +2413,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", @@ -2464,9 +2440,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.11" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -2555,9 +2531,9 @@ dependencies = [ [[package]] name = "selectors" -version = "0.33.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feef350c36147532e1b79ea5c1f3791373e61cbd9a6a2615413b3807bb164fb7" +checksum = "2cfaaa6035167f0e604e42723c7650d59ee269ef220d7bbe0565602c8a0173b9" dependencies = [ "bitflags", "cssparser", @@ -2627,9 +2603,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -2698,11 +2674,27 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -2897,9 +2889,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.51.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -3012,20 +3004,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -3109,9 +3101,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" @@ -3157,9 +3149,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -3211,11 +3203,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -3224,14 +3216,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -3242,9 +3234,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -3252,9 +3244,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3262,9 +3254,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -3275,9 +3267,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -3318,9 +3310,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -3338,9 +3330,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -3397,9 +3389,9 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-native-keyring-store" -version = "0.5.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0e26ac29479a4b7f49d46afc9893122fc345430c6307d1fb7ac0d8d066c3f54" +checksum = "063426e76fdec7438d56bb777f67e318a84a25c707b07e575cb8b78e10c028f8" dependencies = [ "byteorder", "keyring-core", @@ -3437,15 +3429,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -3455,15 +3438,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" @@ -3482,21 +3456,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -3530,12 +3489,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3548,12 +3501,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3566,12 +3513,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3596,12 +3537,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3614,12 +3549,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3632,12 +3561,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3650,12 +3573,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3670,9 +3587,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "wit-bindgen" @@ -3683,6 +3600,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -3813,9 +3736,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] diff --git a/Cargo.toml b/Cargo.toml index 9fd9fca..3273811 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,9 +38,9 @@ mail-parser = "0.11" native-tls = "0.2" parking_lot = "0.12" reqwest = { version = "0.13", features = ["json"] } -rmcp = { version = "1.4", features = ["transport-io"] } +rmcp = { version = "1.7", features = ["transport-io"] } schemars = { version = "1.2", features = ["chrono04"] } -keyring-core = "0.7" +keyring-core = "1.0" serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2" @@ -52,13 +52,13 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1", features = ["v4"] } [target.'cfg(target_os = "macos")'.dependencies] -apple-native-keyring-store = { version = "0.2", features = ["keychain"] } +apple-native-keyring-store = { version = "1.0", features = ["keychain", "protected"] } [target.'cfg(target_os = "windows")'.dependencies] -windows-native-keyring-store = "0.5" +windows-native-keyring-store = "1.1" [target.'cfg(target_os = "linux")'.dependencies] -dbus-secret-service-keyring-store = { version = "0.3", features = ["crypto-rust"] } +dbus-secret-service-keyring-store = { version = "1.0", features = ["crypto-rust"] } [lints.clippy] await_holding_lock = "deny" diff --git a/README.md b/README.md index 6e1760e..e6cfdb3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ IMAP email client exposed as both a CLI and an MCP (Model Context Protocol) server, built with Rust. -MCP protocol: [2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18) (also negotiates 2025-03-26 and 2024-11-05) | rmcp 1.4 +MCP protocol: [2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18) (also negotiates 2025-11-25, 2025-03-26, and 2024-11-05) | rmcp 1.6 One binary: `agentmail serve` starts the MCP stdio server, all other subcommands are a direct CLI. @@ -349,7 +349,7 @@ To pass passwords via environment variables instead of keychain: ``` agentmail (binary crate: agentmail-mcp) - ├── serve → MCP stdio server (tokio + rmcp 1.4) + ├── serve → MCP stdio server (tokio + rmcp 1.6) │ 21 tools + 6 prompts, tasks, progress notifications ├── list-accounts → CLI ├── list-mailboxes → CLI diff --git a/ci-check.sh b/ci-check.sh index 3570198..cf6bb40 100755 --- a/ci-check.sh +++ b/ci-check.sh @@ -28,6 +28,12 @@ echo "==> Building..." cargo build --all-features echo "==> Running tests..." -cargo test --all-features +if command -v cargo-nextest >/dev/null 2>&1; then + cargo nextest run --all-features --profile ci +else + echo " cargo-nextest not installed; falling back to 'cargo test'." + echo " Install with: cargo install cargo-nextest --locked" + cargo test --all-features +fi echo "==> All checks passed." diff --git a/src/imap_client.rs b/src/imap_client.rs index 1e62ad1..0afb2d0 100644 --- a/src/imap_client.rs +++ b/src/imap_client.rs @@ -1095,9 +1095,9 @@ pub async fn fetch_flags( } let mut flags: Vec<(String, u32)> = flag_counts.into_iter().collect(); - flags.sort_by(|a, b| b.1.cmp(&a.1)); + flags.sort_by_key(|b| std::cmp::Reverse(b.1)); let mut colors: Vec<(String, u32)> = color_counts.into_iter().collect(); - colors.sort_by(|a, b| b.1.cmp(&a.1)); + colors.sort_by_key(|b| std::cmp::Reverse(b.1)); Ok(FlagScanResult { flags, colors }) } diff --git a/src/lib.rs b/src/lib.rs index e6a3952..dd7d413 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -343,7 +343,7 @@ impl Agentmail { format!("{} <{}>", s.display_name, s.address) }; } - senders.sort_by(|a, b| b.count.cmp(&a.count)); + senders.sort_by_key(|b| std::cmp::Reverse(b.count)); let unique_senders = senders.len(); let total_messages = senders.iter().map(|s| s.count).sum::(); @@ -591,7 +591,7 @@ impl Agentmail { } }) .collect(); - lists.sort_by(|a, b| b.count.cmp(&a.count)); + lists.sort_by_key(|b| std::cmp::Reverse(b.count)); let unique_lists = lists.len(); let total_messages = lists.iter().map(|l| l.count).sum::(); @@ -752,14 +752,14 @@ impl Agentmail { session.release().await; let mut flag_list: Vec<(String, u32)> = total_flags.into_iter().collect(); - flag_list.sort_by(|a, b| b.1.cmp(&a.1)); + flag_list.sort_by_key(|b| std::cmp::Reverse(b.1)); let flags: Vec = flag_list .into_iter() .map(|(flag, count)| FlagCount { flag, count }) .collect(); let mut color_list: Vec<(String, u32)> = total_colors.into_iter().collect(); - color_list.sort_by(|a, b| b.1.cmp(&a.1)); + color_list.sort_by_key(|b| std::cmp::Reverse(b.1)); let colors: Vec = color_list .into_iter() .map(|(color, count)| ColorCount { color, count }) diff --git a/src/main.rs b/src/main.rs index 8f84850..30676f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -168,25 +168,7 @@ async fn main() -> Result<(), Box> { .init(); agentmail::secret::init_service_name("agentmail"); - // Initialize platform keyring store for standalone mode - #[cfg(target_os = "macos")] - { - if let Ok(store) = apple_native_keyring_store::keychain::Store::new() { - keyring_core::set_default_store(store); - } - } - #[cfg(target_os = "windows")] - { - if let Ok(store) = windows_native_keyring_store::Store::new() { - keyring_core::set_default_store(store); - } - } - #[cfg(target_os = "linux")] - { - if let Ok(store) = dbus_secret_service_keyring_store::Store::new() { - keyring_core::set_default_store(store); - } - } + init_platform_keyring(); let cli = Cli::parse(); match cli.command.unwrap_or(CliCommand::Serve) { @@ -370,6 +352,98 @@ async fn main() -> Result<(), Box> { } } +// --------------------------------------------------------------------------- +// Platform keyring initialization +// --------------------------------------------------------------------------- + +/// Install the platform-appropriate keyring store as the default for `keyring_core`. +/// +/// On macOS, prefers the data-protection keychain (which doesn't depend on a default +/// keychain pointer and works in headless launchd contexts). Probes it with a no-op +/// read; if the binary lacks the entitlement (typical for unsigned `cargo install` +/// builds), falls back to the legacy file-based keychain. Surfaces failures via +/// `tracing` so the cause is visible instead of silently swallowed. +fn init_platform_keyring() { + #[cfg(target_os = "macos")] + { + let protected = apple_native_keyring_store::protected::Store::new() + .expect("protected::Store::new is documented infallible"); + keyring_core::set_default_store(protected); + + // Probe: try to read a sentinel entry. NoEntry means the store works + // (entry just doesn't exist). MissingEntitlement / PlatformFailure means + // we need to fall back to the file-based keychain. + let probe = keyring_core::Entry::new("agentmail.probe.startup", "probe"); + let probe_ok = match probe { + Ok(entry) => match entry.get_password() { + Ok(_) => true, + Err(keyring_core::error::Error::NoEntry) => true, + Err(e) => { + let msg = e.to_string(); + let entitlement_issue = msg.contains("-34018") + || msg.to_lowercase().contains("missing entitlement"); + if entitlement_issue { + tracing::debug!( + "keychain: data-protection unavailable (missing entitlement); \ + falling back to file-based keychain" + ); + false + } else { + // Some other error — keep data-protection, surface details + // on the next real call. + tracing::debug!( + "keychain: data-protection probe returned {e}; keeping it active" + ); + true + } + } + }, + Err(e) => { + tracing::warn!("keychain: Entry probe failed unexpectedly: {e}"); + false + } + }; + + if !probe_ok { + match apple_native_keyring_store::keychain::Store::new() { + Ok(store) => { + keyring_core::set_default_store(store); + tracing::debug!("keychain: using file-based backend"); + } + Err(e) => { + tracing::warn!( + "keychain: no backend available (file-based failed: {e}). \ + Password operations will fail. \ + Set AGENTMAIL_PASSWORD_ to bypass the keychain." + ); + } + } + } + } + + #[cfg(target_os = "windows")] + { + match windows_native_keyring_store::Store::new() { + Ok(store) => keyring_core::set_default_store(store), + Err(e) => tracing::warn!( + "keychain: Windows credential store unavailable: {e}. \ + Set AGENTMAIL_PASSWORD_ to bypass." + ), + } + } + + #[cfg(target_os = "linux")] + { + match dbus_secret_service_keyring_store::Store::new() { + Ok(store) => keyring_core::set_default_store(store), + Err(e) => tracing::warn!( + "keychain: D-Bus secret service unavailable: {e}. \ + Set AGENTMAIL_PASSWORD_ to bypass." + ), + } + } +} + // --------------------------------------------------------------------------- // Interactive account configuration // --------------------------------------------------------------------------- diff --git a/src/secret.rs b/src/secret.rs index c05a4fd..6b559d4 100644 --- a/src/secret.rs +++ b/src/secret.rs @@ -9,6 +9,7 @@ use std::sync::OnceLock; use serde::Deserialize; +use thiserror::Error; /// Keyring service name. Set at app startup. Falls back to `"agentmail"` for standalone use. static SERVICE_NAME: OnceLock = OnceLock::new(); @@ -29,6 +30,90 @@ pub fn service_name() -> &'static str { .unwrap_or("agentmail") } +/// Typed error returned by [`Secret`] operations. +/// +/// Callers (e.g. the CLI in `main.rs`) print the `Display` form, which carries +/// remediation hints for the common macOS launch-context failures. +#[derive(Debug, Error)] +pub enum SecretError { + #[error( + "no default keychain configured for this user (errSecNoDefaultKeychain / -25307). \ + Fix: run `security default-keychain -s login.keychain-db`, \ + or set AGENTMAIL_PASSWORD_ as a fallback." + )] + NoDefaultKeychain, + + #[error( + "keychain not accessible in this context (errSecInteractionNotAllowed / -25308). \ + You're likely running headless (launchd, SSH, CI) where the keychain can't prompt. \ + Set AGENTMAIL_PASSWORD_ as a fallback." + )] + InteractionNotAllowed, + + #[error( + "keychain entry needs an entitlement this binary doesn't have (errSecMissingEntitlement / -34018). \ + The data-protection keychain requires a signed binary with a stable team identifier. \ + Use a file-based keyring entry or set AGENTMAIL_PASSWORD_." + )] + MissingEntitlement, + + #[error( + "no default keyring store has been installed in this process \ + (neither data-protection nor file-based backend could be opened)" + )] + NoDefaultStore, + + #[error("keyring backend error: {0}")] + Backend(String), + + #[error("setting a command-based secret is not supported")] + CommandNotWritable, + + #[error("command failed ({status}): {stderr}")] + CommandFailed { status: String, stderr: String }, + + #[error("command I/O error: {0}")] + CommandIo(String), + + #[error("internal task error: {0}")] + Internal(String), +} + +/// Translate a `keyring_core::Error` into our typed `SecretError`. +/// +/// `keyring-core` surfaces platform error codes via `PlatformFailure`/`NoStorageAccess` +/// with an opaque `Box`. We grep the `Display` for known macOS codes +/// (and their textual messages, since the user's locale affects which surface). +pub(crate) fn map_keyring_error(err: keyring_core::error::Error) -> SecretError { + use keyring_core::error::Error as KErr; + + match err { + KErr::NoDefaultStore => SecretError::NoDefaultStore, + KErr::PlatformFailure(ref inner) | KErr::NoStorageAccess(ref inner) => { + classify_platform_message(&inner.to_string()) + .unwrap_or_else(|| SecretError::Backend(err.to_string())) + } + other => SecretError::Backend(other.to_string()), + } +} + +/// Classify a stringified platform error from `keyring-core`/`security-framework`. +/// +/// Public to the crate so unit tests can exercise it without constructing real +/// `keyring-core` errors (their `PlatformError` field is `non_exhaustive`). +pub(crate) fn classify_platform_message(msg: &str) -> Option { + let lower = msg.to_lowercase(); + if msg.contains("-25307") || lower.contains("no default keychain") { + Some(SecretError::NoDefaultKeychain) + } else if msg.contains("-25308") || lower.contains("interaction is not allowed") { + Some(SecretError::InteractionNotAllowed) + } else if msg.contains("-34018") || lower.contains("missing entitlement") { + Some(SecretError::MissingEntitlement) + } else { + None + } +} + /// A secret value that can be stored in different backends. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "lowercase")] @@ -54,34 +139,31 @@ impl Secret { } /// Retrieve the secret value. - pub async fn get(&self) -> Result { + pub async fn get(&self) -> Result { match self { Secret::Raw(v) => Ok(v.clone()), Secret::Keyring(key) => { let service = service_name().to_string(); let key = key.clone(); tokio::task::spawn_blocking(move || { - let entry = keyring_core::Entry::new(&service, &key) - .map_err(|e| format!("keyring entry error: {e}"))?; - entry - .get_password() - .map_err(|e| format!("keyring get_password error: {e}")) + let entry = + keyring_core::Entry::new(&service, &key).map_err(map_keyring_error)?; + entry.get_password().map_err(map_keyring_error) }) .await - .map_err(|e| format!("spawn_blocking error: {e}"))? + .map_err(|e| SecretError::Internal(e.to_string()))? } Secret::Command(cmd) => { let output = tokio::process::Command::new("sh") .args(["-c", cmd]) .output() .await - .map_err(|e| format!("command error: {e}"))?; + .map_err(|e| SecretError::CommandIo(e.to_string()))?; if !output.status.success() { - return Err(format!( - "command failed ({}): {}", - output.status, - String::from_utf8_lossy(&output.stderr).trim() - )); + return Err(SecretError::CommandFailed { + status: output.status.to_string(), + stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(), + }); } Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } @@ -89,7 +171,7 @@ impl Secret { } /// Store a value into this secret's backend. - pub async fn set(&mut self, value: &str) -> Result<(), String> { + pub async fn set(&mut self, value: &str) -> Result<(), SecretError> { match self { Secret::Raw(v) => { *v = value.to_string(); @@ -100,21 +182,19 @@ impl Secret { let key = key.clone(); let value = value.to_string(); tokio::task::spawn_blocking(move || { - let entry = keyring_core::Entry::new(&service, &key) - .map_err(|e| format!("keyring entry error: {e}"))?; - entry - .set_password(&value) - .map_err(|e| format!("keyring set_password error: {e}")) + let entry = + keyring_core::Entry::new(&service, &key).map_err(map_keyring_error)?; + entry.set_password(&value).map_err(map_keyring_error) }) .await - .map_err(|e| format!("spawn_blocking error: {e}"))? + .map_err(|e| SecretError::Internal(e.to_string()))? } - Secret::Command(_) => Err("Cannot set a command-based secret".to_string()), + Secret::Command(_) => Err(SecretError::CommandNotWritable), } } /// Delete this secret from its backend. - pub async fn delete(&mut self) -> Result<(), String> { + pub async fn delete(&mut self) -> Result<(), SecretError> { match self { Secret::Raw(v) => { v.clear(); @@ -130,9 +210,111 @@ impl Secret { Ok(()) }) .await - .map_err(|e| format!("spawn_blocking error: {e}"))? + .map_err(|e| SecretError::Internal(e.to_string()))? } Secret::Command(_) => Ok(()), } } } + +#[cfg(test)] +mod tests { + use super::*; + + // ----- Raw backend ----- + + #[tokio::test] + async fn raw_get_returns_value() { + let s = Secret::new_raw("hunter2"); + assert_eq!(s.get().await.unwrap(), "hunter2"); + } + + #[tokio::test] + async fn raw_set_updates_value() { + let mut s = Secret::new_raw("old"); + s.set("new").await.unwrap(); + assert_eq!(s.get().await.unwrap(), "new"); + } + + #[tokio::test] + async fn raw_delete_clears() { + let mut s = Secret::new_raw("hunter2"); + s.delete().await.unwrap(); + assert_eq!(s.get().await.unwrap(), ""); + } + + // ----- Command backend ----- + + #[tokio::test] + async fn command_get_runs_shell() { + let s = Secret::Command("printf hunter2".to_string()); + assert_eq!(s.get().await.unwrap(), "hunter2"); + } + + #[tokio::test] + async fn command_set_errors() { + let mut s = Secret::Command("echo".to_string()); + let err = s.set("anything").await.unwrap_err(); + assert!(matches!(err, SecretError::CommandNotWritable)); + } + + #[tokio::test] + async fn command_failure_surfaces_stderr() { + let s = Secret::Command("echo boom 1>&2; exit 1".to_string()); + let err = s.get().await.unwrap_err(); + match err { + SecretError::CommandFailed { stderr, .. } => assert!(stderr.contains("boom")), + other => panic!("expected CommandFailed, got {other:?}"), + } + } + + // ----- Error mapping (pure function, no store needed) ----- + + #[test] + fn classify_no_default_keychain_by_code() { + let mapped = classify_platform_message("OSStatus error -25307"); + assert!(matches!(mapped, Some(SecretError::NoDefaultKeychain))); + } + + #[test] + fn classify_no_default_keychain_by_message() { + let mapped = classify_platform_message("No default keychain could be found."); + assert!(matches!(mapped, Some(SecretError::NoDefaultKeychain))); + } + + #[test] + fn classify_interaction_not_allowed_by_code() { + let mapped = classify_platform_message("error code -25308"); + assert!(matches!(mapped, Some(SecretError::InteractionNotAllowed))); + } + + #[test] + fn classify_missing_entitlement_by_code() { + let mapped = classify_platform_message("OSStatus error -34018"); + assert!(matches!(mapped, Some(SecretError::MissingEntitlement))); + } + + #[test] + fn classify_unknown_returns_none() { + assert!(classify_platform_message("some unrelated error").is_none()); + } + + // ----- Keyring backend roundtrip via mock store ----- + // + // `keyring_core::set_default_store` is process-global. Nextest runs each + // test in its own process by default, so this won't leak into the other + // tests. Under `cargo test` (fallback), this test still works in isolation + // because no other test in this module installs a default store. + + #[tokio::test] + async fn keyring_roundtrip_with_mock_store() { + keyring_core::set_default_store(keyring_core::mock::Store::new().unwrap()); + + let mut s = Secret::new_keyring("agentmail.test.roundtrip"); + s.set("hunter2").await.unwrap(); + assert_eq!(s.get().await.unwrap(), "hunter2"); + s.delete().await.unwrap(); + // After delete, get should fail with a backend error (NoEntry). + assert!(s.get().await.is_err()); + } +}