feat(imap): added back auto id feature

Refs: #688
This commit is contained in:
Clément DOUIN
2026-05-31 19:46:48 +02:00
parent f603e68268
commit aff4fddfb4
25 changed files with 311 additions and 252 deletions
+4
View File
@@ -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]).
+3 -3
View File
@@ -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 = "../<repo>"`. 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
Generated
+57 -57
View File
@@ -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",
+8 -7
View File
@@ -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"
+19 -2
View File
@@ -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=<URL>` 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.
</details>
<details>
@@ -356,6 +357,22 @@ Himalaya CLI is one of several front-ends to the Pimalaya libraries:
Set `NO_COLOR=1` in your environment.
</details>
## 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)
+11
View File
@@ -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
+2 -1
View File
@@ -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"
Generated
+7 -7
View File
@@ -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": {
+2 -1
View File
@@ -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";
+7 -6
View File
@@ -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<Sasl> = 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::<StreamStd>::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<Sasl> = 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::<StreamStd>::connect(&server, &tls, smtp_config.starttls, domain, sasl)?;
let _client = SmtpClientStd::connect(&server, &tls, smtp_config.starttls, domain, sasl)?;
Ok(())
})();
+27 -4
View File
@@ -116,13 +116,10 @@ pub struct AccountConfig {
pub downloads_dir: Option<PathBuf>,
#[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<SaslConfig>,
/// 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<String, bool>,
}
/// Maildir configuration.
+13 -6
View File
@@ -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<StreamStd>,
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<Self> {
let mut tls: Tls = config.tls.into();
tls.rustls.alpn = vec!["imap".into()];
let sasl: Option<Sasl> = 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::<StreamStd>::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<Url> {
}
impl Deref for ImapClient {
type Target = Inner<StreamStd>;
type Target = Inner;
fn deref(&self) -> &Self::Target {
&self.inner
+92 -44
View File
@@ -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<IString<'static>, 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<Option<Vec<(IString<'static>, 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))
}
+8 -3
View File
@@ -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 {
+27 -31
View File
@@ -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<Self> {
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<Sasl> = 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::<StreamStd>::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<SmtpClientStd<StreamStd>> {
let smtp = (|| -> Result<SmtpClientStd> {
let mut tls: Tls = smtp_config.tls.into();
tls.rustls.alpn = vec!["smtp".into()];
let sasl: Option<Sasl> = 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::<StreamStd>::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
+2 -2
View File
@@ -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<Flag> = 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<String> = self.flags.inner.iter().map(ToString::to_string).collect();
printer.out(AddedFlags { flags })
+2 -2
View File
@@ -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<Flag> = 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<String> = self.flags.inner.iter().map(ToString::to_string).collect();
printer.out(RemovedFlags { flags })
+2 -2
View File
@@ -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<Flag> = 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<String> = self.flags.inner.iter().map(ToString::to_string).collect();
printer.out(SetFlags { flags })
+2 -57
View File
@@ -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<String>)> {
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<String>) {
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());
}
}
}
}
}
}
+2 -4
View File
@@ -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"))
}
}
+4 -4
View File
@@ -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<StreamStd>,
inner: Inner,
#[allow(dead_code)]
pub account: Account,
}
@@ -52,7 +52,7 @@ impl SmtpClient {
let sasl: Option<Sasl> = 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::<StreamStd>::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<Url> {
}
impl Deref for SmtpClient {
type Target = Inner<StreamStd>;
type Target = Inner;
fn deref(&self) -> &Self::Target {
&self.inner
+1
View File
@@ -49,6 +49,7 @@ pub fn imap_to_config(w: WizardImapConfig) -> Result<ImapConfig> {
tls: Default::default(),
starttls,
sasl,
id: Default::default(),
})
}
+4 -4
View File
@@ -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};
+1 -1
View File
@@ -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};
+4 -4
View File
@@ -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};