mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-15 11:27:53 +08:00
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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
@@ -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": {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -49,6 +49,7 @@ pub fn imap_to_config(w: WizardImapConfig) -> Result<ImapConfig> {
|
||||
tls: Default::default(),
|
||||
starttls,
|
||||
sasl,
|
||||
id: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user