From aff4fddfb4dfa40a305562e40aa7f747d58cf074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 31 May 2026 19:46:48 +0200 Subject: [PATCH] feat(imap): added back auto id feature Refs: #688 --- CHANGELOG.md | 4 + CONTRIBUTING.md | 6 +- Cargo.lock | 114 ++++++++++++++-------------- Cargo.toml | 15 ++-- README.md | 21 +++++- config.sample.toml | 11 +++ deny.toml | 3 +- flake.lock | 14 ++-- flake.nix | 3 +- src/account/check.rs | 13 ++-- src/config.rs | 31 +++++++- src/imap/client.rs | 19 +++-- src/imap/id.rs | 136 +++++++++++++++++++++++----------- src/jmap/email/query.rs | 11 ++- src/shared/client.rs | 58 +++++++-------- src/shared/flags/add.rs | 4 +- src/shared/flags/remove.rs | 4 +- src/shared/flags/set.rs | 4 +- src/shared/messages/output.rs | 59 +-------------- src/shared/messages/send.rs | 6 +- src/smtp/client.rs | 8 +- src/wizard/account.rs | 1 + src/wizard/autoconfig.rs | 8 +- src/wizard/pacc.rs | 2 +- src/wizard/srv.rs | 8 +- 25 files changed, 311 insertions(+), 252 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbcf2f42..644a38dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Restored the RFC 2971 `ID`-after-auth quirk under the new shape `imap.id.{auto, fields}`. Set `imap.id.auto = true` to chain an `ID` exchange straight after IMAP authentication (required by mail.qq.com, fastmail). `imap.id.fields` is a `{ name = bool, … }` map: missing keys are not transmitted, `false` sends `NIL`, `true` sends himalaya's canned value for the well-known keys (`name`, `version`, `vendor`, `support-url`) or `NIL` (with a warning) for any other key. Replaces the v1.2.0 `imap.extensions.id.send-after-auth` flag dropped during the v2 migration. + ### Fixed - Fixed compilation error when `wizard` feature was disabled ([#634]). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e224abe..93032ead 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ Himalaya CLI is the command-line front-end of the [Pimalaya](https://github.com/ - [io-email](https://github.com/pimalaya/io-email): cross-protocol email client (`EmailClientStd`, shared `Envelope` / `Mailbox` / `Flag` / `Address` types, search DSL). - [io-imap](https://github.com/pimalaya/io-imap), [io-jmap](https://github.com/pimalaya/io-jmap), [io-maildir](https://github.com/pimalaya/io-maildir), [io-smtp](https://github.com/pimalaya/io-smtp): per-protocol I/O-free coroutines plus the std-blocking clients that drive them. - [io-http](https://github.com/pimalaya/io-http): I/O-free HTTP request/response state machines used by JMAP and the discovery wizard. -- [io-discovery](https://github.com/pimalaya/io-discovery): provider discovery (PACC, Thunderbird Autoconfiguration, RFC 6186 SRV) consumed by the wizard. +- [pimconf](https://github.com/pimalaya/pimconf): PIM service discovery (PACC, Thunderbird Autoconfiguration, RFC 6186 SRV) consumed by the wizard. - [pimalaya/stream](https://github.com/pimalaya/stream): TCP / TLS / SASL plumbing shared by all std clients. - [pimalaya/cli](https://github.com/pimalaya/cli): cross-binary CLI helpers (printer, prompt, wizard primitives, clap args, build-time env, spinner). - [pimalaya/config](https://github.com/pimalaya/config): TOML configuration loader and shell-expanded secrets. @@ -55,7 +55,6 @@ Bugs touching protocol semantics usually live in the matching `io-*` crate; rend ```toml [patch.crates-io] -io-discovery.git = "https://github.com/pimalaya/io-discovery" io-email.git = "https://github.com/pimalaya/io-email" io-http.git = "https://github.com/pimalaya/io-http" io-imap.git = "https://github.com/pimalaya/io-imap" @@ -65,6 +64,7 @@ io-smtp.git = "https://github.com/pimalaya/io-smtp" pimalaya-cli.git = "https://github.com/pimalaya/cli" pimalaya-config.git = "https://github.com/pimalaya/config" pimalaya-stream.git = "https://github.com/pimalaya/stream" +pimconf.git = "https://github.com/pimalaya/pimconf" ``` To build against a local checkout of one of those crates, swap the matching `.git = "..."` for `.path = "../"`. For example, with `io-email` next to `himalaya`: @@ -78,7 +78,6 @@ If cargo complains about *"perhaps two different versions of crate X are being u ```toml [patch.crates-io] -io-discovery.path = "../io-discovery" io-email.path = "../io-email" io-http.path = "../io-http" io-imap.path = "../io-imap" @@ -88,6 +87,7 @@ io-smtp.path = "../io-smtp" pimalaya-cli.path = "../cli" pimalaya-config.path = "../config" pimalaya-stream.path = "../stream" +pimconf.path = "../pimconf" ``` ## Lint, test, audit diff --git a/Cargo.lock b/Cargo.lock index d73a7d86..5fd0e3ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,9 +228,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.62" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", @@ -848,7 +848,6 @@ dependencies = [ "crossterm", "dirs", "humansize", - "io-discovery", "io-email", "io-imap", "io-jmap", @@ -864,6 +863,7 @@ dependencies = [ "pimalaya-cli", "pimalaya-config", "pimalaya-stream", + "pimconf", "rfc2047-decoder", "secrecy", "serde", @@ -1026,8 +1026,7 @@ dependencies = [ [[package]] name = "imap-codec" version = "2.0.0-alpha.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82ae8750fd2186ff82f159a1d0a86e73c284d28e70e9ec3e316ccb10e273e082" +source = "git+https://github.com/soywod/imap-codec#dfd4f1f179defb6b479da3a95ed603170f0174ad" dependencies = [ "abnf-core", "base64", @@ -1040,8 +1039,7 @@ dependencies = [ [[package]] name = "imap-types" version = "2.0.0-alpha.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31d8480b20e8416d52f20b737a159bd925219273aedb023caa12190ca42781c" +source = "git+https://github.com/soywod/imap-codec#dfd4f1f179defb6b479da3a95ed603170f0174ad" dependencies = [ "base64", "bounded-static", @@ -1077,33 +1075,15 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "io-discovery" -version = "0.1.0" -source = "git+https://github.com/pimalaya/io-discovery#ec82afbb532f76a7507a8bb3f470685352e97f04" -dependencies = [ - "anyhow", - "base64", - "domain", - "io-http", - "log", - "pimalaya-stream", - "serde", - "serde-xml-rs", - "serde_json", - "sha2", - "subtle", - "thiserror", - "url", -] - [[package]] name = "io-email" version = "0.0.1" -source = "git+https://github.com/pimalaya/io-email#db314793398f252023d0a2b2ac3bc2145d93d11a" +source = "git+https://github.com/pimalaya/io-email#5d01da7acb98d8394077d40780f1cded639f4574" dependencies = [ "chrono", "chumsky", + "gethostname", + "io-http", "io-imap", "io-jmap", "io-m2dir", @@ -1123,7 +1103,7 @@ dependencies = [ [[package]] name = "io-http" version = "0.0.3" -source = "git+https://github.com/pimalaya/io-http#1e71383b97e823f9440ea2d7c2af378bcd3b5933" +source = "git+https://github.com/pimalaya/io-http#73b264b85209d86de48afed25957ab582b54c272" dependencies = [ "anyhow", "base64", @@ -1139,7 +1119,7 @@ dependencies = [ [[package]] name = "io-imap" version = "0.0.1" -source = "git+https://github.com/pimalaya/io-imap#b6871a87f5b491003dbcc61bcdde4dd5d0b17828" +source = "git+https://github.com/pimalaya/io-imap#518da423e551684fa8202a9cc4fede99f1be3f23" dependencies = [ "anyhow", "base64", @@ -1154,7 +1134,7 @@ dependencies = [ [[package]] name = "io-jmap" version = "0.0.1" -source = "git+https://github.com/pimalaya/io-jmap#740ac4f314d4de3b4c6b53eba61722787f01f24c" +source = "git+https://github.com/pimalaya/io-jmap#2463ecb104d0ff24b8c3ba2f4eaa09562ce5a1cd" dependencies = [ "anyhow", "io-http", @@ -1170,7 +1150,7 @@ dependencies = [ [[package]] name = "io-m2dir" version = "0.0.1" -source = "git+https://github.com/pimalaya/io-m2dir#0f36832cf074eae35d3b9959c7c98076fc0da57b" +source = "git+https://github.com/pimalaya/io-m2dir#0865b8301c49b2669d66fff93efdc02596cb4e66" dependencies = [ "log", "thiserror", @@ -1179,7 +1159,7 @@ dependencies = [ [[package]] name = "io-maildir" version = "0.0.1" -source = "git+https://github.com/pimalaya/io-maildir#ca5075379f7ab5879626c1b98df2471cf8c25a19" +source = "git+https://github.com/pimalaya/io-maildir#7cfb98c861a710a272ffdb48c330c6c744e289aa" dependencies = [ "gethostname", "log", @@ -1190,7 +1170,7 @@ dependencies = [ [[package]] name = "io-smtp" version = "0.0.1" -source = "git+https://github.com/pimalaya/io-smtp#f3687e2b8997463fb247d8df6a685906746eb549" +source = "git+https://github.com/pimalaya/io-smtp#3b8cf0d806c23339a46a540e159de3d4cd9898f3" dependencies = [ "anyhow", "base64", @@ -1237,9 +1217,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.27" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "392c70591e8749fe235ddaf513e6f58b26bce3dcc16524cecc8936f75afa161e" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" dependencies = [ "jiff-static", "log", @@ -1250,9 +1230,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.27" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b605b0c050d845fc355bb11eb3f9a8deddc218ea60c76e61aa1f2adfb2c96a" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" dependencies = [ "proc-macro2", "quote", @@ -1344,9 +1324,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libgit2-sys" -version = "0.18.4+1.9.3" +version = "0.18.5+1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b26f66f35e1871b22efcf7191564123d2a446ca0538cde63c23adfefa9b15b7" +checksum = "005d6ae6eac1912906073e069f7db60b1fa98e052a68227824afe3e3a1c59ca2" dependencies = [ "cc", "libc", @@ -1371,9 +1351,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.28" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "libc", @@ -1472,9 +1452,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -1684,7 +1664,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pimalaya-cli" version = "0.0.1" -source = "git+https://github.com/pimalaya/cli#b4078bd986685fdd0193e7fe5a383d02defc2bc5" +source = "git+https://github.com/pimalaya/cli#8ee488f8701b9e4a5bb29dbcbc051b399b78e5c6" dependencies = [ "anyhow", "clap", @@ -1708,7 +1688,7 @@ dependencies = [ [[package]] name = "pimalaya-config" version = "0.0.1" -source = "git+https://github.com/pimalaya/config#f8c344a34d9e86eaf1d51041c3c6daec3d12510a" +source = "git+https://github.com/pimalaya/config#c10cd14039de1ef31c56c5efa3463a20b38d2c57" dependencies = [ "anyhow", "dirs", @@ -1724,7 +1704,7 @@ dependencies = [ [[package]] name = "pimalaya-stream" version = "0.0.1" -source = "git+https://github.com/pimalaya/stream#55bbb1cbf929effdc131573724017b86450a6ddd" +source = "git+https://github.com/pimalaya/stream#6449136364c9ef81248e3b4ab6491dd154293481" dependencies = [ "anyhow", "log", @@ -1737,6 +1717,26 @@ dependencies = [ "url", ] +[[package]] +name = "pimconf" +version = "0.1.0" +source = "git+https://github.com/pimalaya/pimconf#4b01b9e105c82052a223b9d13261dd118fd159da" +dependencies = [ + "anyhow", + "base64", + "domain", + "io-http", + "log", + "pimalaya-stream", + "serde", + "serde-xml-rs", + "serde_json", + "sha2", + "subtle", + "thiserror", + "url", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -2228,9 +2228,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" @@ -2455,9 +2455,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uds_windows" @@ -2533,9 +2533,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -3003,18 +3003,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index fd226346..f7ed0f6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,15 +17,15 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["imap", "smtp", "jmap", "m2dir", "rustls-ring"] +default = ["rustls-ring", "imap", "smtp", "jmap", "m2dir"] imap = ["dep:io-imap", "dep:mail-parser", "dep:rfc2047-decoder", "io-email/imap", "io-imap/client"] jmap = ["dep:base64", "dep:io-jmap", "dep:mail-parser", "dep:serde_json", "io-email/jmap", "io-jmap/client"] smtp = ["dep:io-smtp", "dep:mail-parser", "io-email/smtp"] maildir = ["dep:convert_case", "dep:io-maildir", "dep:mail-parser", "dep:mime_guess", "io-email/maildir", "io-maildir/client"] m2dir = ["dep:io-m2dir", "dep:mail-parser", "io-email/m2dir", "io-m2dir/client"] -native-tls = ["pimalaya-stream/native-tls", "io-discovery/native-tls", "io-imap?/native-tls", "io-jmap?/native-tls", "io-smtp?/native-tls"] -rustls-aws = ["pimalaya-stream/rustls-aws", "io-discovery/rustls-aws", "io-imap?/rustls-aws", "io-jmap?/rustls-aws", "io-smtp?/rustls-aws"] -rustls-ring = ["pimalaya-stream/rustls-ring", "io-discovery/rustls-ring", "io-imap?/rustls-ring", "io-jmap?/rustls-ring", "io-smtp?/rustls-ring"] +native-tls = ["pimalaya-stream/native-tls", "pimconf/native-tls", "io-email/native-tls", "io-imap?/native-tls", "io-jmap?/native-tls", "io-smtp?/native-tls"] +rustls-aws = ["pimalaya-stream/rustls-aws", "pimconf/rustls-aws", "io-email/rustls-aws", "io-imap?/rustls-aws", "io-jmap?/rustls-aws", "io-smtp?/rustls-aws"] +rustls-ring = ["pimalaya-stream/rustls-ring", "pimconf/rustls-ring", "io-email/rustls-ring", "io-imap?/rustls-ring", "io-jmap?/rustls-ring", "io-smtp?/rustls-ring"] vendored = ["pimalaya-stream/vendored"] [profile.release] @@ -47,7 +47,6 @@ convert_case = { version = "0.11", optional = true } crossterm = { version = "0.29", default-features = false, features = ["serde"] } dirs = "6" humansize = "2" -io-discovery = { version = "0.1.0", default-features = false, features = ["pacc", "autoconfig", "rfc6186", "client"] } ariadne = "0.6" io-email = { version = "0.0.1", default-features = false, features = ["serde", "client", "search"] } io-imap = { version = "0.0.1", default-features = false, optional = true } @@ -63,6 +62,7 @@ open = "5" pimalaya-cli = { version = "0.0.1", default-features = false, features = ["terminal", "table", "prompt", "wizard", "imap", "smtp", "jmap", "spinner"] } pimalaya-config = { version = "0.0.1", default-features = false, features = ["toml", "secret"] } pimalaya-stream = { version = "0.0.1", default-features = false, features = ["std"] } +pimconf = { version = "0.1.0", default-features = false, features = ["pacc", "autoconfig", "rfc6186", "client"] } rfc2047-decoder = { version = "1", optional = true } secrecy = "0.10" serde = { version = "1", features = ["derive"] } @@ -84,7 +84,7 @@ tempfile = "3" # [patch.crates-io] # domain = { git = "https://github.com/soywod/domain", branch = "new-srv" } -# io-discovery.path = "../io-discovery" +# pimconf.path = "../pimconf" # io-email.path = "../io-email" # io-http.path = "../io-http" # io-imap.path = "../io-imap" @@ -98,7 +98,8 @@ tempfile = "3" [patch.crates-io] domain = { git = "https://github.com/soywod/domain", branch = "new-srv" } -io-discovery.git = "https://github.com/pimalaya/io-discovery" +imap-codec.git = "https://github.com/soywod/imap-codec" +pimconf.git = "https://github.com/pimalaya/pimconf" io-email.git = "https://github.com/pimalaya/io-email" io-http.git = "https://github.com/pimalaya/io-http" io-imap.git = "https://github.com/pimalaya/io-imap" diff --git a/README.md b/README.md index 9ef2dd53..eb0b1056 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ - [Re-using sessions](#re-using-sessions) - [Interfaces](#interfaces) - [FAQ](#faq) +- [AI disclosure](#ai-disclosure) - [Social](#social) - [Sponsoring](#sponsoring) @@ -64,7 +65,7 @@ ### Pre-built binary -Himalaya can be installed with the `install.sh` installer: +Himalaya can be installed with the installer: *As root:* @@ -323,7 +324,7 @@ Himalaya CLI is one of several front-ends to the Pimalaya libraries: 2. **Thunderbird Autoconfiguration**: ISP main / well-known / ISPDB lookups, then MX-based retry, then the `mailconf=` TXT redirect. 3. **RFC 6186 SRV**: `_imap._tcp`, `_imaps._tcp`, `_submission._tcp` lookups assembled into a single report. - See [io-discovery](https://github.com/pimalaya/io-discovery) for the full chain. + See [pimconf](https://github.com/pimalaya/pimconf) for the full chain.
@@ -356,6 +357,22 @@ Himalaya CLI is one of several front-ends to the Pimalaya libraries: Set `NO_COLOR=1` in your environment.
+## AI disclosure + +This project is developed with AI assistance. This section documents how, so users and downstream packagers can make informed decisions. + +- **Tools**: Claude Code (Anthropic), Opus 4.7, invoked locally with a persistent project-scoped memory and a small set of repo-specific rules. + +- **Used for**: Refactors, mechanical multi-file edits, boilerplate (feature gates, error enums, derive macros, trait impls), test scaffolding, doc polish, exploratory design conversations. + +- **Not used for**: Engineering, critical code, git manipulation (commit, merge, rebase…), real-world tests. + +- **Verification**: Every AI-assisted change is read, compiled, tested, and formatted before commit (`nix develop --command cargo check / cargo test / cargo fmt`). Behavioural correctness is verified against the relevant RFC or upstream spec, not assumed from the model output. Tests are never adjusted to fit AI-generated code; the code is adjusted to fit correct behaviour. + +- **Limitations**: AI models occasionally produce code that compiles and passes tests but is subtly wrong: off-by-one errors, missed edge cases, plausible but nonexistent APIs, stale RFC references. The verification workflow catches most of this; it does not catch all of it. Bug reports are welcome and taken seriously. + +- **Last reviewed**: 31/05/2026 + ## Social - Chat on [Matrix](https://matrix.to/#/#pimalaya:matrix.org) diff --git a/config.sample.toml b/config.sample.toml index 44385708..989a7ee7 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -233,6 +233,17 @@ imap.sasl.plain.passwd.raw = "***" #imap.sasl.scram-sha-256.username = "user@example.com" #imap.sasl.scram-sha-256.password.raw = "***" +# RFC 2971 ID extension. Some providers (mail.qq.com, fastmail) require an `ID` +# exchange straight after authentication; set `auto = true` to opt in. +# https://www.rfc-editor.org/rfc/rfc2971.html +#imap.id.auto = false + +# Per-field policy for the auto-ID command. Keys not listed here are NOT sent. +# `true` substitutes himalaya's canned value for the well-known keys (name, +# version, vendor, support-url); unknown keys with `true` fall back to NIL with +# a warning. `false` always sends NIL. An empty map sends `ID NIL`. +#imap.id.fields = { name = true, version = true, vendor = true, support-url = true } + # -------------------------------------------------------------------------------- # JMAP config # https://www.iana.org/go/rfc8620 diff --git a/deny.toml b/deny.toml index 6aa4a9b3..8a03f5aa 100644 --- a/deny.toml +++ b/deny.toml @@ -2,7 +2,6 @@ allow-git = [ "https://github.com/pimalaya/cli", "https://github.com/pimalaya/config", - "https://github.com/pimalaya/io-discovery", "https://github.com/pimalaya/io-email", "https://github.com/pimalaya/io-http", "https://github.com/pimalaya/io-imap", @@ -10,8 +9,10 @@ allow-git = [ "https://github.com/pimalaya/io-m2dir", "https://github.com/pimalaya/io-maildir", "https://github.com/pimalaya/io-smtp", + "https://github.com/pimalaya/pimconf", "https://github.com/pimalaya/stream", "https://github.com/soywod/domain", + "https://github.com/soywod/imap-codec", ] unknown-git = "deny" unknown-registry = "deny" diff --git a/flake.lock b/flake.lock index 55ecf1c4..55d48b53 100644 --- a/flake.lock +++ b/flake.lock @@ -24,28 +24,28 @@ }, "nixpkgs": { "locked": { - "lastModified": 1777673416, - "narHash": "sha256-5c2POKPOjU40Kh0MirOdScBLG0bu9TAuPYAtPRNZMBs=", + "lastModified": 1779912548, + "narHash": "sha256-C2W6RHjQwfONVCYAnqmJumOYS62sMVlSPEGM1VOP/bI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "26ef669cffa904b6f6832ab57b77892a37c1a671", + "rev": "c767db50e209f33ffce3c18165b36101079d367d", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixos-25.11", "repo": "nixpkgs", + "rev": "c767db50e209f33ffce3c18165b36101079d367d", "type": "github" } }, "pimalaya": { "flake": false, "locked": { - "lastModified": 1777930959, - "narHash": "sha256-0U5rd30a74ToN9+Cqv/wR/PN9pXXxsi+0XuUeA6Vs/8=", + "lastModified": 1780122970, + "narHash": "sha256-C/+JCfSrd5W6Poyz7ctSh1EdPRi1cx2Ah4x4w7Exa/U=", "owner": "pimalaya", "repo": "nix", - "rev": "129a1d7a378b053160adb4f5952abe8e7050cb4b", + "rev": "914039413ecfe7d89ea43269dc566768fe35fb02", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 88394027..39c7545b 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,8 @@ inputs = { nixpkgs = { - url = "github:nixos/nixpkgs/nixos-25.11"; + # until crates.io fix fully backported + url = "github:nixos/nixpkgs?tag=25.11&rev=c767db50e209f33ffce3c18165b36101079d367d"; }; fenix = { url = "github:nix-community/fenix/monthly"; diff --git a/src/account/check.rs b/src/account/check.rs index daa4666a..e7b51706 100644 --- a/src/account/check.rs +++ b/src/account/check.rs @@ -114,15 +114,17 @@ fn check_imap( imap_config: crate::config::ImapConfig, ) -> BackendCheck { use io_imap::client::ImapClientStd; - use pimalaya_stream::{sasl::Sasl, std::stream::StreamStd, tls::Tls}; + use pimalaya_stream::{sasl::Sasl, tls::Tls}; + + use crate::imap::id::resolve_auto_id_params; let result = (|| -> Result<()> { let mut tls: Tls = imap_config.tls.clone().into(); tls.rustls.alpn = vec!["imap".into()]; let sasl: Option = imap_config.sasl.clone().map(Sasl::try_from).transpose()?; + let auto_id = resolve_auto_id_params(&imap_config.id)?; let server = crate::imap::client::parse_imap_server(&imap_config.server)?; - let _client = - ImapClientStd::::connect(&server, &tls, imap_config.starttls, sasl)?; + let _ = ImapClientStd::connect(&server, &tls, imap_config.starttls, sasl, auto_id)?; Ok(()) })(); @@ -181,7 +183,7 @@ fn check_smtp( use std::net::Ipv4Addr; use io_smtp::{client::SmtpClientStd, rfc5321::types::ehlo_domain::EhloDomain}; - use pimalaya_stream::{sasl::Sasl, std::stream::StreamStd, tls::Tls}; + use pimalaya_stream::{sasl::Sasl, tls::Tls}; let result = (|| -> Result<()> { let mut tls: Tls = smtp_config.tls.clone().into(); @@ -189,8 +191,7 @@ fn check_smtp( let sasl: Option = smtp_config.sasl.clone().map(Sasl::try_from).transpose()?; let domain: EhloDomain<'static> = Ipv4Addr::new(127, 0, 0, 1).into(); let server = crate::smtp::client::parse_smtp_server(&smtp_config.server)?; - let _client = - SmtpClientStd::::connect(&server, &tls, smtp_config.starttls, domain, sasl)?; + let _client = SmtpClientStd::connect(&server, &tls, smtp_config.starttls, domain, sasl)?; Ok(()) })(); diff --git a/src/config.rs b/src/config.rs index bbae5775..a7c5c0d5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -116,13 +116,10 @@ pub struct AccountConfig { pub downloads_dir: Option, #[serde(default)] pub table: TableConfig, - #[serde(default)] pub envelope: EnvelopeConfig, - #[serde(default)] pub mailbox: MailboxConfig, - #[serde(default)] pub attachment: AttachmentConfig, @@ -362,7 +359,7 @@ pub struct ReaderConfig { pub default: bool, } -/// Global / per-account table rendering knobs shared across every list +/// Global / per-account table rendering quirks shared across every list /// command (envelopes, mailboxes, attachments). The per-column color /// blocks live under `*.list.table.*-color` (see [`EnvelopeListTableConfig`] /// & co.). @@ -418,6 +415,32 @@ pub struct ImapConfig { /// to advertise the ANONYMOUS mechanism explicitly, set /// `sasl.anonymous = {}`. pub sasl: Option, + + /// RFC 2971 `ID` extension quirks. Some providers (notably + /// mail.qq.com, fastmail) require an `ID` exchange straight after + /// authentication; set `id.auto = true` to opt in. + #[serde(default)] + pub id: ImapIdConfig, +} + +/// Per-account `imap.id.*` quirks. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct ImapIdConfig { + /// When `true`, the auth coroutine chains an `ID` round-trip + /// after the tagged auth response. Default `false` skips ID + /// entirely. + #[serde(default)] + pub auto: bool, + + /// Parameters sent with the auto-ID command. Empty (default) + /// sends `ID NIL`. For each entry: `true` substitutes himalaya's + /// canned value for the well-known keys (`name`, `version`, + /// `vendor`, `support-url`) or `NIL` for unknown keys; `false` + /// always sends `NIL`. Keys absent from this map are not + /// transmitted. + #[serde(default)] + pub fields: HashMap, } /// Maildir configuration. diff --git a/src/imap/client.rs b/src/imap/client.rs index df1f547d..df406ca0 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -30,25 +30,32 @@ use std::{ use anyhow::{Result, anyhow}; use io_imap::client::ImapClientStd as Inner; use pimalaya_config::toml::TomlConfig; -use pimalaya_stream::{sasl::Sasl, std::stream::StreamStd, tls::Tls}; +use pimalaya_stream::{sasl::Sasl, tls::Tls}; use url::Url; -use crate::{account::context::Account, cli::load_or_wizard, config::ImapConfig}; +use crate::{ + account::context::Account, cli::load_or_wizard, config::ImapConfig, + imap::id::resolve_auto_id_params, +}; pub struct ImapClient { - inner: Inner, + inner: Inner, pub account: Account, } impl ImapClient { /// Opens the IMAP connection (TCP/TLS/STARTTLS, greeting, SASL) - /// then wraps the resulting client alongside `account`. + /// then wraps the resulting client alongside `account`. The + /// capability list reported by the connect handshake is discarded; + /// IMAP-specific subcommands that need it should call + /// [`Inner::capability`] explicitly. pub fn new(config: ImapConfig, account: Account) -> Result { let mut tls: Tls = config.tls.into(); tls.rustls.alpn = vec!["imap".into()]; let sasl: Option = config.sasl.map(Sasl::try_from).transpose()?; + let auto_id = resolve_auto_id_params(&config.id)?; let server = parse_imap_server(&config.server)?; - let inner = Inner::::connect(&server, &tls, config.starttls, sasl)?; + let (inner, _capability) = Inner::connect(&server, &tls, config.starttls, sasl, auto_id)?; Ok(Self { inner, account }) } } @@ -70,7 +77,7 @@ pub fn parse_imap_server(server: &str) -> Result { } impl Deref for ImapClient { - type Target = Inner; + type Target = Inner; fn deref(&self) -> &Self::Target { &self.inner diff --git a/src/imap/id.rs b/src/imap/id.rs index a3155b31..1307768a 100644 --- a/src/imap/id.rs +++ b/src/imap/id.rs @@ -17,7 +17,7 @@ use std::{collections::HashMap, fmt}; -use anyhow::Result; +use anyhow::{Result, anyhow}; use clap::Parser; use comfy_table::{Cell, Row, Table}; use io_imap::types::{ @@ -27,7 +27,7 @@ use io_imap::types::{ use pimalaya_cli::printer::Printer; use serde::Serialize; -use crate::imap::client::ImapClient; +use crate::{config::ImapIdConfig, imap::client::ImapClient}; /// Get information about the IMAP server. /// @@ -45,26 +45,11 @@ pub struct ImapIdCommand { impl ImapIdCommand { pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> { - let mut params = HashMap::new(); - - params.extend([ - ( - IString::try_from("name").unwrap(), - NString::try_from(env!("CARGO_PKG_NAME")).unwrap(), - ), - ( - IString::try_from("version").unwrap(), - NString::try_from(env!("CARGO_PKG_VERSION")).unwrap(), - ), - ( - IString::try_from("vendor").unwrap(), - NString::try_from("Pimalaya").unwrap(), - ), - ( - IString::try_from("support-url").unwrap(), - NString::try_from("https://github.com/pimalaya/himalaya").unwrap(), - ), - ]); + let mut params: HashMap, NString<'static>> = HashMap::new(); + for key in ["name", "version", "vendor", "support-url"] { + let (k, v) = build_canned_pair(key)?; + params.insert(k, v); + } if let Some(more) = self.parameter { params.extend(more); @@ -93,28 +78,6 @@ impl ImapIdCommand { } } -fn parameter_parser(param: &str) -> Result<(IString<'static>, NString<'static>), String> { - let Some((key, val)) = param.split_once(':') else { - return Err(format!("Invalid parameter `{param}`: missing `:`")); - }; - - let Ok(ikey) = IString::try_from(key.trim()) else { - return Err(format!("Invalid parameter key `{key}`")); - }; - - let nval = if val.trim().is_empty() { - NString::NIL - } else { - let Ok(nval) = NString::try_from(val.trim()) else { - return Err(format!("Invalid parameter value `{val}` for `{key}`")); - }; - - nval - }; - - Ok((ikey.into_static(), nval.into_static())) -} - #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "kebab-case")] pub struct ServerIdTable { @@ -145,3 +108,88 @@ impl fmt::Display for ServerIdTable { writeln!(f, "{table}") } } + +/// Resolves an [`ImapIdConfig`] into the wire-level parameter list +/// passed to the io-imap auth coroutines. +/// +/// [`None`] when `auto = false`; otherwise a vec where each entry +/// maps the user-supplied key to either himalaya's canned value +/// (when the user set `true` and the key is well-known) or `NIL`. +/// Unknown keys with `true` log a warning and fall back to `NIL`. +pub fn resolve_auto_id_params( + config: &ImapIdConfig, +) -> Result, NString<'static>)>>> { + if !config.auto { + return Ok(None); + } + + let mut params = Vec::with_capacity(config.fields.len()); + for (key, &use_canned) in &config.fields { + let ikey = IString::try_from(key.clone()) + .map_err(|err| anyhow!("Invalid IMAP ID parameter key `{key}`: {err}"))? + .into_static(); + + let nval = if use_canned { + match canned_value(key) { + Some(value) => NString::try_from(value) + .map_err(|err| { + anyhow!("Invalid canned IMAP ID value `{value}` for `{key}`: {err}") + })? + .into_static(), + None => { + log::warn!("imap.id.fields.{key} = true: no canned value defined, sending NIL"); + NString::NIL + } + } + } else { + NString::NIL + }; + + params.push((ikey, nval)); + } + Ok(Some(params)) +} + +fn parameter_parser(param: &str) -> Result<(IString<'static>, NString<'static>), String> { + let Some((key, val)) = param.split_once(':') else { + return Err(format!("Invalid parameter `{param}`: missing `:`")); + }; + + let Ok(ikey) = IString::try_from(key.trim()) else { + return Err(format!("Invalid parameter key `{key}`")); + }; + + let nval = if val.trim().is_empty() { + NString::NIL + } else { + let Ok(nval) = NString::try_from(val.trim()) else { + return Err(format!("Invalid parameter value `{val}` for `{key}`")); + }; + + nval + }; + + Ok((ikey.into_static(), nval.into_static())) +} + +fn canned_value(key: &str) -> Option<&'static str> { + match key { + "name" => Some(env!("CARGO_PKG_NAME")), + "version" => Some(env!("CARGO_PKG_VERSION")), + "vendor" => Some("Pimalaya"), + "support-url" => Some("https://github.com/pimalaya/himalaya"), + _ => None, + } +} + +fn build_canned_pair(key: &str) -> Result<(IString<'static>, NString<'static>)> { + let ikey = IString::try_from(key) + .map_err(|err| anyhow!("Invalid IMAP ID parameter key `{key}`: {err}"))? + .into_static(); + let value = + canned_value(key).ok_or_else(|| anyhow!("No canned IMAP ID value defined for `{key}`"))?; + let nval = NString::try_from(value) + .map_err(|err| anyhow!("Invalid canned IMAP ID value `{value}` for `{key}`: {err}"))? + .into_static(); + Ok((ikey, nval)) +} diff --git a/src/jmap/email/query.rs b/src/jmap/email/query.rs index cef53afe..1efb8564 100644 --- a/src/jmap/email/query.rs +++ b/src/jmap/email/query.rs @@ -20,8 +20,9 @@ use std::fmt; use anyhow::Result; use clap::{Parser, ValueEnum}; use comfy_table::{Cell, Color, ContentArrangement, Row, Table}; -use io_jmap::rfc8621::email::{ - Email, EmailAddress, EmailComparator, EmailFilter, EmailSortProperty, +use io_jmap::{ + rfc8620::filter::Filter, + rfc8621::email::{Email, EmailAddress, EmailComparator, EmailFilter, EmailSortProperty}, }; use pimalaya_cli::printer::Printer; use serde::Serialize; @@ -140,7 +141,11 @@ impl JmapEmailQueryCommand { || f.subject.is_some() || f.body.is_some(); - if has_one_filter { Some(f) } else { None } + if has_one_filter { + Some(Filter::from(f)) + } else { + None + } }; let sort = Some(vec![EmailComparator { diff --git a/src/shared/client.rs b/src/shared/client.rs index e6023269..f90ed6cb 100644 --- a/src/shared/client.rs +++ b/src/shared/client.rs @@ -25,16 +25,17 @@ //! its methods directly. //! //! Construction picks the first storage backend (`jmap → imap → -//! maildir`) allowed by the `BackendFlag` that is configured on the -//! account. When the account also has SMTP configured, an SMTP slot -//! is registered on the same client so `send_message` works for -//! IMAP/Maildir accounts; JMAP accounts already send via JMAP -//! submission. SMTP connection failures are logged and skipped — the -//! client still opens for reading. +//! maildir → m2dir`) allowed by the [`Backend`] flag that is +//! configured on the account. When the account also has SMTP +//! configured, an SMTP slot is registered on the same client so +//! `send_message` works for IMAP/Maildir accounts; JMAP accounts +//! already send via JMAP submission. SMTP connection failures are +//! logged and skipped: the client still opens for reading. use std::ops::{Deref, DerefMut}; use anyhow::{Result, bail}; +use io_email::client::EmailClientStd; use crate::{ account::context::Account, @@ -43,7 +44,7 @@ use crate::{ }; pub struct EmailClient { - inner: io_email::client::EmailClientStd, + inner: EmailClientStd, pub account: Account, } @@ -53,15 +54,12 @@ impl EmailClient { mut account_config: AccountConfig, backend: Backend, ) -> Result { - use io_email::client::EmailClientStd; - let mut inner = EmailClientStd::new(); let mut configured = false; #[cfg(feature = "jmap")] if !configured && backend.allows_jmap() { if let Some(jmap_config) = account_config.jmap.take() { - use io_jmap::client::JmapClientStd; use pimalaya_stream::tls::Tls; use crate::jmap::client::{jmap_http_auth, parse_server_url}; @@ -70,10 +68,7 @@ impl EmailClient { tls.rustls.alpn = vec!["http/1.1".into()]; let http_auth = jmap_http_auth(jmap_config.auth.clone())?; let url = parse_server_url(&jmap_config.server)?; - let mut client = JmapClientStd::connect(&url, &tls, http_auth)?; - client.session_get(&url)?; - - inner = inner.with_jmap(client); + inner = inner.connect_jmap(&url, &tls, http_auth)?; configured = true; } } @@ -81,17 +76,19 @@ impl EmailClient { #[cfg(feature = "imap")] if !configured && backend.allows_imap() { if let Some(imap_config) = account_config.imap.take() { - use io_imap::client::ImapClientStd; - use pimalaya_stream::{sasl::Sasl, std::stream::StreamStd, tls::Tls}; + use io_email::imap::client::ImapClientStd; + use pimalaya_stream::{sasl::Sasl, tls::Tls}; + + use crate::imap::id::resolve_auto_id_params; let mut tls: Tls = imap_config.tls.into(); tls.rustls.alpn = vec!["imap".into()]; let sasl: Option = imap_config.sasl.map(Sasl::try_from).transpose()?; + let auto_id = resolve_auto_id_params(&imap_config.id)?; let server = crate::imap::client::parse_imap_server(&imap_config.server)?; - let client = - ImapClientStd::::connect(&server, &tls, imap_config.starttls, sasl)?; - - inner = inner.with_imap(client); + let imap = + ImapClientStd::connect(&server, &tls, imap_config.starttls, sasl, auto_id)?; + inner = inner.with_imap(imap); configured = true; } } @@ -99,10 +96,9 @@ impl EmailClient { #[cfg(feature = "maildir")] if !configured && backend.allows_maildir() { if let Some(maildir_config) = account_config.maildir.take() { - use io_maildir::client::MaildirClient; + use io_email::maildir::client::MaildirClient; let client = MaildirClient::new(maildir_config.root.to_string_lossy().into_owned()); - inner = inner.with_maildir(client); configured = true; } @@ -111,10 +107,9 @@ impl EmailClient { #[cfg(feature = "m2dir")] if !configured && backend.allows_m2dir() { if let Some(m2dir_config) = account_config.m2dir.take() { - use io_m2dir::client::M2dirClient; + use io_email::m2dir::client::M2dirClient; let client = M2dirClient::new(m2dir_config.root.to_string_lossy().into_owned()); - inner = inner.with_m2dir(client); configured = true; } @@ -126,22 +121,23 @@ impl EmailClient { // Register SMTP alongside the storage backend so shared // `send_message` works for IMAP/Maildir accounts. JMAP already - // sends via submission, but if both are present, SMTP wins - // because storage is registered first. + // sends via submission; the dispatch priority (JMAP → SMTP in + // the send path) keeps that working when both are present. #[cfg(feature = "smtp")] if let Some(smtp_config) = account_config.smtp.take() { use std::net::Ipv4Addr; - use io_smtp::{client::SmtpClientStd, rfc5321::types::ehlo_domain::EhloDomain}; - use pimalaya_stream::{sasl::Sasl, std::stream::StreamStd, tls::Tls}; + use io_email::smtp::client::SmtpClientStd; + use io_smtp::rfc5321::types::ehlo_domain::EhloDomain; + use pimalaya_stream::{sasl::Sasl, tls::Tls}; - let smtp = (|| -> Result> { + let smtp = (|| -> Result { let mut tls: Tls = smtp_config.tls.into(); tls.rustls.alpn = vec!["smtp".into()]; let sasl: Option = smtp_config.sasl.map(Sasl::try_from).transpose()?; let domain: EhloDomain<'static> = Ipv4Addr::new(127, 0, 0, 1).into(); let server = crate::smtp::client::parse_smtp_server(&smtp_config.server)?; - Ok(SmtpClientStd::::connect( + Ok(SmtpClientStd::connect( &server, &tls, smtp_config.starttls, @@ -165,7 +161,7 @@ impl EmailClient { } impl Deref for EmailClient { - type Target = io_email::client::EmailClientStd; + type Target = EmailClientStd; fn deref(&self) -> &Self::Target { &self.inner diff --git a/src/shared/flags/add.rs b/src/shared/flags/add.rs index 192ccf58..46049b9a 100644 --- a/src/shared/flags/add.rs +++ b/src/shared/flags/add.rs @@ -19,7 +19,7 @@ use std::fmt; use anyhow::Result; use clap::Parser; -use io_email::flag::Flag; +use io_email::flag::{Flag, FlagOp}; use pimalaya_cli::printer::Printer; use serde::Serialize; @@ -46,7 +46,7 @@ impl FlagAddCommand { let ids: Vec<&str> = self.message_ids.inner.iter().map(String::as_str).collect(); let flags: Vec = self.flags.inner.iter().map(Into::into).collect(); - client.add_flags(&mailbox, &ids, &flags)?; + client.store_flags(&mailbox, &ids, &flags, FlagOp::Add)?; let flags: Vec = self.flags.inner.iter().map(ToString::to_string).collect(); printer.out(AddedFlags { flags }) diff --git a/src/shared/flags/remove.rs b/src/shared/flags/remove.rs index 2e946e35..f8d69a31 100644 --- a/src/shared/flags/remove.rs +++ b/src/shared/flags/remove.rs @@ -19,7 +19,7 @@ use std::fmt; use anyhow::Result; use clap::Parser; -use io_email::flag::Flag; +use io_email::flag::{Flag, FlagOp}; use pimalaya_cli::printer::Printer; use serde::Serialize; @@ -46,7 +46,7 @@ impl FlagRemoveCommand { let ids: Vec<&str> = self.message_ids.inner.iter().map(String::as_str).collect(); let flags: Vec = self.flags.inner.iter().map(Into::into).collect(); - client.delete_flags(&mailbox, &ids, &flags)?; + client.store_flags(&mailbox, &ids, &flags, FlagOp::Remove)?; let flags: Vec = self.flags.inner.iter().map(ToString::to_string).collect(); printer.out(RemovedFlags { flags }) diff --git a/src/shared/flags/set.rs b/src/shared/flags/set.rs index 3c84532a..157b7676 100644 --- a/src/shared/flags/set.rs +++ b/src/shared/flags/set.rs @@ -19,7 +19,7 @@ use std::fmt; use anyhow::Result; use clap::Parser; -use io_email::flag::Flag; +use io_email::flag::{Flag, FlagOp}; use pimalaya_cli::printer::Printer; use serde::Serialize; @@ -46,7 +46,7 @@ impl FlagSetCommand { let ids: Vec<&str> = self.message_ids.inner.iter().map(String::as_str).collect(); let flags: Vec = self.flags.inner.iter().map(Into::into).collect(); - client.set_flags(&mailbox, &ids, &flags)?; + client.store_flags(&mailbox, &ids, &flags, FlagOp::Set)?; let flags: Vec = self.flags.inner.iter().map(ToString::to_string).collect(); printer.out(SetFlags { flags }) diff --git a/src/shared/messages/output.rs b/src/shared/messages/output.rs index a02f4da0..3a90fc13 100644 --- a/src/shared/messages/output.rs +++ b/src/shared/messages/output.rs @@ -25,8 +25,7 @@ use std::io::{Write, stdout}; -use anyhow::{Result, anyhow, bail}; -use mail_parser::{Address as ParserAddress, HeaderValue, MessageParser}; +use anyhow::Result; use pimalaya_cli::printer::{Message, Printer}; use crate::shared::client::EmailClient; @@ -53,63 +52,9 @@ pub fn route( } if send { - let (from, to) = extract_envelope(&raw)?; - let to_refs: Vec<&str> = to.iter().map(String::as_str).collect(); - client.send_message(raw, &from, &to_refs)?; + client.send_message(raw)?; return printer.out(Message::new("Message successfully sent")); } printer.out(Message::new("Message saved")) } - -/// Extracts the envelope sender from `From:` and envelope recipients -/// from `To:` / `Cc:` / `Bcc:`. Returns an error when `From:` is -/// missing or no recipient header carries at least one address. -pub fn extract_envelope(raw: &[u8]) -> Result<(String, Vec)> { - let parsed = MessageParser::default() - .parse(raw) - .ok_or_else(|| anyhow!("failed to parse outgoing message"))?; - - let mut from_emails = Vec::new(); - if let Some(HeaderValue::Address(addr)) = parsed.header("From").cloned() { - collect_emails(addr, &mut from_emails); - } - let from = from_emails - .into_iter() - .next() - .ok_or_else(|| anyhow!("outgoing message is missing a `From:` header"))?; - - let mut to = Vec::new(); - for name in ["To", "Cc", "Bcc"] { - if let Some(HeaderValue::Address(addr)) = parsed.header(name).cloned() { - collect_emails(addr, &mut to); - } - } - - if to.is_empty() { - bail!("outgoing message has no recipients (`To:` / `Cc:` / `Bcc:`)"); - } - - Ok((from, to)) -} - -fn collect_emails(addr: ParserAddress<'_>, out: &mut Vec) { - match addr { - ParserAddress::List(list) => { - for a in list { - if let Some(email) = a.address { - out.push(email.into_owned()); - } - } - } - ParserAddress::Group(groups) => { - for g in groups { - for a in g.addresses { - if let Some(email) = a.address { - out.push(email.into_owned()); - } - } - } - } - } -} diff --git a/src/shared/messages/send.rs b/src/shared/messages/send.rs index af3310c6..182eb60f 100644 --- a/src/shared/messages/send.rs +++ b/src/shared/messages/send.rs @@ -24,7 +24,7 @@ use anyhow::Result; use clap::Parser; use pimalaya_cli::printer::{Message, Printer}; -use crate::shared::{client::EmailClient, messages::output::extract_envelope}; +use crate::shared::client::EmailClient; /// Send a message via the active account. /// @@ -68,9 +68,7 @@ impl MessageSendCommand { .into_bytes() }; - let (from, to) = extract_envelope(&raw)?; - let to_refs: Vec<&str> = to.iter().map(String::as_str).collect(); - client.send_message(raw, &from, &to_refs)?; + client.send_message(raw)?; printer.out(Message::new("Message successfully sent")) } } diff --git a/src/smtp/client.rs b/src/smtp/client.rs index 702a326c..351b7d68 100644 --- a/src/smtp/client.rs +++ b/src/smtp/client.rs @@ -32,13 +32,13 @@ use std::{ use anyhow::{Result, anyhow}; use io_smtp::{client::SmtpClientStd as Inner, rfc5321::types::ehlo_domain::EhloDomain}; use pimalaya_config::toml::TomlConfig; -use pimalaya_stream::{sasl::Sasl, std::stream::StreamStd, tls::Tls}; +use pimalaya_stream::{sasl::Sasl, tls::Tls}; use url::Url; use crate::{account::context::Account, cli::load_or_wizard, config::SmtpConfig}; pub struct SmtpClient { - inner: Inner, + inner: Inner, #[allow(dead_code)] pub account: Account, } @@ -52,7 +52,7 @@ impl SmtpClient { let sasl: Option = config.sasl.map(Sasl::try_from).transpose()?; let domain: EhloDomain<'static> = Ipv4Addr::new(127, 0, 0, 1).into(); let server = parse_smtp_server(&config.server)?; - let inner = Inner::::connect(&server, &tls, config.starttls, domain, sasl)?; + let inner = Inner::connect(&server, &tls, config.starttls, domain, sasl)?; Ok(Self { inner, account }) } } @@ -74,7 +74,7 @@ pub fn parse_smtp_server(server: &str) -> Result { } impl Deref for SmtpClient { - type Target = Inner; + type Target = Inner; fn deref(&self) -> &Self::Target { &self.inner diff --git a/src/wizard/account.rs b/src/wizard/account.rs index 6e93566b..e6ee4d2c 100644 --- a/src/wizard/account.rs +++ b/src/wizard/account.rs @@ -49,6 +49,7 @@ pub fn imap_to_config(w: WizardImapConfig) -> Result { tls: Default::default(), starttls, sasl, + id: Default::default(), }) } diff --git a/src/wizard/autoconfig.rs b/src/wizard/autoconfig.rs index b249d885..f6fb6eb7 100644 --- a/src/wizard/autoconfig.rs +++ b/src/wizard/autoconfig.rs @@ -20,10 +20,6 @@ //! ISPDB in series (secure variants only); each probe owns its own //! spinner. -use io_discovery::autoconfig::{ - client::{DiscoveryAutoconfigClientStd, DiscoveryAutoconfigClientStdError}, - types::{Autoconfig, SecurityType, Server, ServerType}, -}; use log::debug; use pimalaya_cli::{ spinner::Spinner, @@ -32,6 +28,10 @@ use pimalaya_cli::{ smtp::{Encryption as SmtpEncryption, SmtpAuth, SmtpSecret, WizardSmtpConfig}, }, }; +use pimconf::autoconfig::{ + client::{DiscoveryAutoconfigClientStd, DiscoveryAutoconfigClientStdError}, + types::{Autoconfig, SecurityType, Server, ServerType}, +}; use crate::wizard::discover::{DiscoveryResult, discovery_resolver, discovery_tls}; diff --git a/src/wizard/pacc.rs b/src/wizard/pacc.rs index 8c11fa93..58b2f74b 100644 --- a/src/wizard/pacc.rs +++ b/src/wizard/pacc.rs @@ -17,7 +17,6 @@ //! PACC step of the wizard's discovery chain. -use io_discovery::pacc::{client::DiscoveryPaccClientStd, types::PaccConfig}; use log::debug; use pimalaya_cli::{ spinner::Spinner, @@ -27,6 +26,7 @@ use pimalaya_cli::{ smtp::{Encryption as SmtpEncryption, SmtpAuth, SmtpSecret, WizardSmtpConfig}, }, }; +use pimconf::pacc::{client::DiscoveryPaccClientStd, types::PaccConfig}; use crate::wizard::discover::{DiscoveryResult, discovery_resolver, discovery_tls}; diff --git a/src/wizard/srv.rs b/src/wizard/srv.rs index b03b3447..d4e3cd22 100644 --- a/src/wizard/srv.rs +++ b/src/wizard/srv.rs @@ -23,10 +23,6 @@ //! SMTP: from `_submission`; the encryption is inferred from the //! record's port (465 → implicit TLS, otherwise StartTls). -use io_discovery::rfc6186::{ - client::DiscoverySrvClientStd, - types::{SrvReport, SrvService}, -}; use log::debug; use pimalaya_cli::{ spinner::Spinner, @@ -35,6 +31,10 @@ use pimalaya_cli::{ smtp::{Encryption as SmtpEncryption, SmtpAuth, SmtpSecret, WizardSmtpConfig}, }, }; +use pimconf::rfc6186::{ + client::DiscoverySrvClientStd, + types::{SrvReport, SrvService}, +}; use crate::wizard::discover::{DiscoveryResult, discovery_resolver};