diff --git a/Cargo.lock b/Cargo.lock index c8150cff..b63d29b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,6 +146,15 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bounded-static" version = "0.8.0" @@ -174,6 +183,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + [[package]] name = "bytes" version = "1.11.1" @@ -338,6 +353,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "convert_case" version = "0.11.0" @@ -363,6 +387,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crossterm" version = "0.29.0" @@ -371,9 +404,13 @@ checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ "bitflags", "crossterm_winapi", + "derive_more", "document-features", + "mio", "parking_lot", "rustix", + "signal-hook", + "signal-hook-mio", "winapi", ] @@ -386,12 +423,54 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs" version = "6.0.0" @@ -433,12 +512,41 @@ dependencies = [ "litrs", ] +[[package]] +name = "domain" +version = "0.12.0" +source = "git+https://github.com/soywod/domain?branch=new-srv#2a72dc6f55212d8b72b046992bc229bfdac48887" +dependencies = [ + "bumpalo", + "bytes", + "domain-macros", + "hashbrown 0.17.0", + "jiff", + "octseq", +] + +[[package]] +name = "domain-macros" +version = "0.12.0" +source = "git+https://github.com/soywod/domain?branch=new-srv#2a72dc6f55212d8b72b046992bc229bfdac48887" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -535,6 +643,25 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -620,6 +747,9 @@ name = "hashbrown" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +dependencies = [ + "allocator-api2", +] [[package]] name = "hashify" @@ -645,12 +775,14 @@ version = "1.2.0" dependencies = [ "anyhow", "assert_cmd", + "base64", "chrono", "clap", "comfy-table", - "convert_case", + "convert_case 0.11.0", "dirs", "gethostname 1.1.0", + "io-discovery", "io-email", "io-imap", "io-jmap", @@ -662,13 +794,16 @@ dependencies = [ "mail-parser", "mime_guess", "open", - "pimalaya-toolbox", + "pimalaya-cli", + "pimalaya-config", + "pimalaya-stream", "rfc2047-decoder", "secrecy", "serde", "serde_json", "shellexpand", "tempfile", + "toml", "uds_windows", "url", ] @@ -813,7 +948,7 @@ dependencies = [ "bounded-static-derive", "chrono", "rand", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -828,6 +963,39 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inquire" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756" +dependencies = [ + "bitflags", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "io-discovery" +version = "0.0.1" +dependencies = [ + "anyhow", + "base64", + "domain", + "io-http", + "log", + "pimalaya-stream", + "serde", + "serde-xml-rs", + "serde_json", + "sha2", + "subtle", + "thiserror 2.0.18", + "url", +] + [[package]] name = "io-email" version = "0.0.1" @@ -840,7 +1008,7 @@ dependencies = [ "mail-parser", "secrecy", "serde", - "thiserror", + "thiserror 2.0.18", "url", ] @@ -854,32 +1022,30 @@ dependencies = [ "log", "memchr", "secrecy", - "thiserror", + "thiserror 2.0.18", "url", ] [[package]] name = "io-imap" version = "0.0.1" -source = "git+https://github.com/pimalaya/io-imap?branch=io#573fd8bacf2e834a3f1be0c58af7cda9240b7c2d" dependencies = [ "imap-codec", "log", "secrecy", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "io-jmap" version = "0.0.1" -source = "git+https://github.com/pimalaya/io-jmap#605074f1aec3d4a30fb95d44086a6e9a4c1e3a1b" dependencies = [ "io-http", "log", "secrecy", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "url", ] @@ -891,7 +1057,7 @@ dependencies = [ "log", "mail-parser", "memchr", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -904,13 +1070,12 @@ dependencies = [ "log", "serde", "shellexpand", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "io-smtp" version = "0.0.1" -source = "git+https://github.com/pimalaya/io-smtp#4c4f4976cbb0ff544014b17f3632aaf9476fbdd4" dependencies = [ "base64", "bounded-static", @@ -918,7 +1083,7 @@ dependencies = [ "chumsky 1.0.0-alpha.8", "log", "secrecy", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -988,7 +1153,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror", + "thiserror 2.0.18", "walkdir", "windows-link", ] @@ -1124,9 +1289,9 @@ dependencies = [ [[package]] name = "mail-parser" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82a3d6522697593ba4c683e0a6ee5a40fee93bc1a525e3cc6eeb3da11fd8897" +checksum = "d8a2420e9ce11c2b0583ca97ddff7ab2398c8a613154e9b72e3bafdbf767f1d7" dependencies = [ "hashify", "serde", @@ -1169,6 +1334,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "native-tls" version = "0.2.18" @@ -1214,6 +1391,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "octseq" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "182eab3e1cd9cdc0ecf1ce3342d9844f3dc7d098f0694569bfdf327b612d69fd" +dependencies = [ + "bytes", + "serde", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1239,15 +1426,14 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.78" +version = "0.10.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" +checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", - "once_cell", "openssl-macros", "openssl-sys", ] @@ -1280,9 +1466,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.114" +version = "0.9.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" +checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" dependencies = [ "cc", "libc", @@ -1333,22 +1519,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "pimalaya-toolbox" -version = "0.0.4" -source = "git+https://github.com/pimalaya/toolbox#be0b944ad03663fec3179237f05b58b97f883e51" +name = "pimalaya-cli" +version = "0.0.1" dependencies = [ "anyhow", - "base64", "clap", "clap_complete", "clap_mangen", - "dirs", + "comfy-table", "env_logger", - "gethostname 1.1.0", "git2", - "io-imap", - "io-jmap", + "inquire", + "log", + "secrecy", + "serde", + "serde_json", + "shellexpand", + "thiserror 2.0.18", + "toml", +] + +[[package]] +name = "pimalaya-config" +version = "0.0.1" +dependencies = [ + "anyhow", + "dirs", "io-process", + "log", + "secrecy", + "serde", + "serde-toml-merge", + "shellexpand", + "thiserror 2.0.18", + "toml", +] + +[[package]] +name = "pimalaya-stream" +version = "0.0.1" +dependencies = [ + "anyhow", + "gethostname 1.1.0", "io-smtp", "log", "native-tls", @@ -1356,11 +1568,6 @@ dependencies = [ "rustls-platform-verifier", "secrecy", "serde", - "serde-toml-merge", - "serde_json", - "shellexpand", - "thiserror", - "toml", "uds_windows", "url", ] @@ -1534,7 +1741,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1594,7 +1801,7 @@ dependencies = [ "chumsky 0.12.0", "memchr", "quoted_printable", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1797,6 +2004,18 @@ dependencies = [ "toml", ] +[[package]] +name = "serde-xml-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6560602b989198ebb381e3192c86970116b3b2e13b8b8a39d6d0850e40b6b81" +dependencies = [ + "log", + "serde", + "thiserror 1.0.69", + "xml-rs", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -1839,6 +2058,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shellexpand" version = "3.1.2" @@ -1854,6 +2084,37 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd_cesu8" version = "1.1.1" @@ -1958,13 +2219,33 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1978,6 +2259,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -2029,6 +2319,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + [[package]] name = "uds_windows" version = "1.2.1" @@ -2107,6 +2403,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wait-timeout" version = "0.2.1" @@ -2478,6 +2780,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + [[package]] name = "yoke" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 34ec31be..61a0d556 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,31 +16,33 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["imap", "smtp", "rustls-ring"] +default = ["imap", "smtp", "maildir", "jmap", "rustls-ring"] -imap = ["dep:io-imap", "dep:mail-parser", "dep:rfc2047-decoder", "pimalaya-toolbox/imap", "io-email/imap"] -jmap = ["dep:io-jmap", "dep:mail-parser", "dep:serde_json", "pimalaya-toolbox/jmap", "io-email/jmap"] -smtp = ["dep:io-smtp", "dep:mail-parser", "pimalaya-toolbox/smtp", "io-email/smtp"] -maildir = ["dep:convert_case", "dep:io-maildir", "dep:mail-parser", "dep:mime_guess", "io-email/maildir"] +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", "pimalaya-stream/smtp", "io-email/smtp"] +maildir = ["dep:convert_case", "dep:io-maildir", "dep:mail-parser", "dep:mime_guess", "io-email/maildir", "io-maildir/client"] -native-tls = ["pimalaya-toolbox/native-tls"] -rustls-aws = ["pimalaya-toolbox/rustls-aws"] -rustls-ring = ["pimalaya-toolbox/rustls-ring"] +native-tls = ["pimalaya-stream/native-tls", "io-discovery/native-tls"] +rustls-aws = ["pimalaya-stream/rustls-aws", "io-discovery/rustls-aws"] +rustls-ring = ["pimalaya-stream/rustls-ring", "io-discovery/rustls-ring"] -vendored = ["pimalaya-toolbox/vendored"] +vendored = ["pimalaya-stream/vendored"] [build-dependencies] -pimalaya-toolbox = { version = "0.0.4", default-features = false, features = ["build"] } +pimalaya-cli = { version = "0.0.1", default-features = false, features = ["build"] } [dependencies] anyhow = "1" +base64 = { version = "0.22", optional = true } chrono = { version = "0.4", default-features = false } clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } comfy-table = "7" convert_case = { version = "0.11", optional = true } dirs = "6" gethostname = "1" -io-email = { version = "0.0.1", default-features = false, features = ["serde"] } +io-discovery = { version = "0.0.1", default-features = false, features = ["pacc", "autoconfig", "client"] } +io-email = { version = "0.0.1", default-features = false, features = ["serde", "client"] } io-imap = { version = "0.0.1", default-features = false, optional = true } io-jmap = { version = "0.0.1", default-features = false, optional = true } io-maildir = { version = "0.0.1", default-features = false, features = ["serde"], optional = true } @@ -51,12 +53,15 @@ mail-builder = "0.3" mail-parser = { version = "0.11", features = ["serde"], optional = true } mime_guess = { version = "2", optional = true } open = "5" -pimalaya-toolbox = { version = "0.0.4", default-features = false, features = ["config", "terminal", "secret", "sasl", "stream"] } +pimalaya-cli = { version = "0.0.1", default-features = false, features = ["terminal", "table", "prompt", "wizard", "imap", "smtp"] } +pimalaya-config = { version = "0.0.1", default-features = false, features = ["toml", "secret"] } +pimalaya-stream = { version = "0.0.1", default-features = false, features = ["std", "http"] } rfc2047-decoder = { version = "1", optional = true } secrecy = "0.10" serde = { version = "1", features = ["derive"] } serde_json = { version = "1", optional = true } shellexpand = "3.1" +toml = "0.8" url = { version = "2.2", features = ["serde"] } [target.'cfg(windows)'.dependencies] @@ -70,10 +75,14 @@ serde_json = "1" tempfile = "3" [patch.crates-io] +domain = { git = "https://github.com/soywod/domain", branch = "new-srv" } +io-discovery.path = "../io-discovery" io-email.path = "../io-email" io-http.git = "https://github.com/pimalaya/io-http" -io-imap = { git = "https://github.com/pimalaya/io-imap", branch = "io" } -io-jmap.git = "https://github.com/pimalaya/io-jmap" +io-imap.path = "../io-imap" +io-jmap.path = "../io-jmap" io-maildir.path = "../io-maildir" -io-smtp.git = "https://github.com/pimalaya/io-smtp" -pimalaya-toolbox.git = "https://github.com/pimalaya/toolbox" +io-smtp.path = "../io-smtp" +pimalaya-cli.path = "../cli" +pimalaya-config.path = "../config" +pimalaya-stream.path = "../stream" diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..75163e43 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,108 @@ +# Himalaya CLI v2 — roadmap + +Living document. Items are grouped by readiness and ordered roughly by +easiest-first within each group. Decisions land inline as `Decided:` +notes; open questions stay in section E until resolved. + +## A. Shared API — already adequate + +| Command | Status | +|---|---| +| `mailboxes list` | done | +| `envelopes list` | done (pagination: `--page` / `--page-size`, default size 25) | +| `flags add/set/delete` | done | +| `messages get` | done (`--raw` writes original RFC 5322 to stdout; `--json` emits the parsed struct) | +| `messages copy` | done (`… --from --to `) | +| `messages move` | done (`… --from --to `) | +| `messages add` | done (raw RFC 5322 in; `--mailbox ` + optional `--flag` / `--file`; IMAP APPEND, JMAP Blob/upload + Email/import, Maildir tmp-then-rename) | +| `messages compose` | done (CLI args incl. `--reply` / `--forward` with `--posting-style top\|bottom`, `--quote-headline`, `--signature`/`--signature-file`, `--send`) | +| `messages send` | done (raw RFC 5322 in; SMTP + JMAP) | +| `attachments list` | done (filename / mime / size / inline; `--include-inline` to surface inline parts) | +| `attachments download` | done (`--dir `; defaults to account/global `downloads-dir`, then platform downloads dir, then temp) | + +**Shared-command rule**: cross-backend commands always treat the IMAP +id as a UID. There is **no** `--seq` flag on the shared API — that +flag is reserved for the protocol-specific `imap` subcommands. + +## B. Shared API — follow-ups (none currently) + +`messages compose` no longer needs body quoting (shipped via +`--posting-style` + `--quote-headline` + `--signature`). `--save-to` +remains intentionally out of scope: the canonical pipeline is +`messages compose | messages add --mailbox Drafts --flag draft`, +which composes well precisely because both halves are shared +commands. MIME-part extraction stays delegated to `mml`. + + +## C. Binary surface — to add back later + +### C1. Sendmail / command backend + +- Generic "exec a command, write the message to its stdin" backend. + Replaces both v1's `sendmail` variant and any future `mailcmd` + integrations. +- Config: `command.send = "/usr/sbin/sendmail -t"` (or similar). +- Wires only into `messages send`. + +### C2. Notmuch backend + +- Local index over Maildir. Adds `notmuch envelopes search` (real + query support — Notmuch's reason to exist) and reuses the maildir + arms for everything else. + +### C3. `accounts` subcommand + wizard + +- **Decided (E6)**: top-level verb is `accounts configure`. +- `accounts list` (read TOML, dump JSON). +- `accounts configure []` → wizard. Depends on the new + `pimalaya/inquire` lib (crossterm-based, extracted from old + `pimalaya-tui`) — himalaya hosts the `accounts configure` entry + point, the lib provides the prompts. +- Possibly `accounts doctor` later (connectivity check). + +### C4. Keyring support (opt-in) + +- Cargo feature `keyring`, off by default. When on, secret values in + TOML can be `{ keyring = "service:account" }`. +- Decoupled from auth itself — works for IMAP password, SMTP password, + JMAP bearer, etc. + +## D. Out of scope (delegated) + +| Concern | Lives in | +|---|---| +| OAuth2 token acquisition / refresh | `pimalaya/ortie` (CLI) | +| MML composition, signing, encryption | `pimalaya/mml` (CLI) | +| HTML rendering | downstream tool / TUI | +| Search query DSL across backends | per-protocol commands only | +| Cross-backend sync | future `pimalaya/sirup` (or similar) | + +Dropped permanently from v2 binary: `template *`, `attachment download` +(use `mml` for MIME part extraction), `messages delete` (use a flag or +move-to-trash), cross-backend copy, mailto handler. + +## E. Resolved design decisions + +1. **Pagination default size** — 25, matching v1. (E1) +2. **Compose shape** — single `messages compose` command with + mutually-exclusive `--reply ` / `--forward ` flags. (E2) +3. **Compose vs send** — `compose --send` is supported. No interactive + editor → no reason to force a `compose | send` pipe. (E3) +4. **Copy/move arg shape** — `… --from --to `, + with `--from` defaulting to `Inbox` and `--to` mandatory. (E4) +5. **`messages get` mode flags** — only `--raw`. JSON output is the + global `--json` flag's job. No `--attachment-dir`, no `--part`. (E5) +6. **Wizard placement** — `accounts configure`. (E6) +7. **Backend dispatch** — none. The shared command runs unless the + user explicitly types the protocol verb (`himalaya imap …`, + `himalaya jmap …`, etc.), in which case the protocol-specific CLI + takes over. (E7) + +## F. Suggested ordering + +1. **B1** compose body-quoting + `--save-to` — finish what's missing. +2. **C1** command/sendmail backend — drops in once the SMTP arm is + stable. +3. **C3 + C4** account wizard + keyring — both gated on the inquire + lib being extracted. +4. **C2** notmuch — last, depends on having maildir arms solid. diff --git a/build.rs b/build.rs index 32ae3009..83d17cbc 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,4 @@ -use pimalaya_toolbox::build::{features_env, git_envs, target_envs}; +use pimalaya_cli::build::{features_env, git_envs, target_envs}; fn main() { features_env(include_str!("./Cargo.toml")); diff --git a/src/attachments/command.rs b/src/attachments/cli.rs similarity index 95% rename from src/attachments/command.rs rename to src/attachments/cli.rs index dc356507..5e6b0216 100644 --- a/src/attachments/command.rs +++ b/src/attachments/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::{ attachments::{download::AttachmentsDownloadCommand, list::AttachmentsListCommand}, diff --git a/src/attachments/download.rs b/src/attachments/download.rs index 8a1486e3..2bf0e01c 100644 --- a/src/attachments/download.rs +++ b/src/attachments/download.rs @@ -6,7 +6,7 @@ use std::{ use anyhow::{bail, Result}; use clap::Parser; use mail_parser::{MessageParser, MimeHeaders}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::{ account::Account, diff --git a/src/attachments/list.rs b/src/attachments/list.rs index f31a0e6d..0ac65728 100644 --- a/src/attachments/list.rs +++ b/src/attachments/list.rs @@ -1,7 +1,7 @@ use anyhow::{bail, Result}; use clap::Parser; use mail_parser::{MessageParser, MessagePart, MimeHeaders}; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::{ account::Account, diff --git a/src/attachments/mod.rs b/src/attachments/mod.rs index 136dea6b..1cff55d2 100644 --- a/src/attachments/mod.rs +++ b/src/attachments/mod.rs @@ -1,4 +1,4 @@ -pub mod command; +pub mod cli; pub mod download; pub mod list; pub mod table; diff --git a/src/cli.rs b/src/cli.rs index 52562942..0f7915e2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,31 +2,28 @@ use std::{fmt, path::PathBuf, str::FromStr}; use anyhow::{bail, Error, Result}; use clap::{CommandFactory, Parser, Subcommand}; -use pimalaya_toolbox::{ - config::TomlConfig, - long_version, - terminal::{ - clap::{ - args::{AccountFlag, JsonFlag, LogFlags}, - commands::{CompletionCommand, ManualCommand}, - parsers::path_parser, - }, - printer::Printer, +use pimalaya_cli::{ + clap::{ + args::{AccountFlag, JsonFlag, LogFlags}, + commands::{CompletionCommand, ManualCommand}, + parsers::path_parser, }, + long_version, + printer::Printer, }; +use pimalaya_config::toml::TomlConfig; #[cfg(feature = "imap")] -use crate::imap::command::ImapCommand; +use crate::imap::cli::ImapCommand; #[cfg(feature = "jmap")] -use crate::jmap::command::JmapCommand; +use crate::jmap::cli::JmapCommand; #[cfg(feature = "maildir")] -use crate::maildir::command::MaildirCommand; +use crate::maildir::cli::MaildirCommand; #[cfg(feature = "smtp")] -use crate::smtp::command::SmtpCommand; +use crate::smtp::cli::SmtpCommand; use crate::{ - account::Account, config::Config, envelopes::command::EnvelopesCommand, - flags::command::FlagsCommand, mailboxes::command::MailboxesCommand, - messages::command::MessagesCommand, + account::Account, config::Config, envelopes::cli::EnvelopesCommand, flags::cli::FlagsCommand, + mailboxes::cli::MailboxesCommand, messages::cli::MessagesCommand, }; #[derive(Parser, Debug)] @@ -158,7 +155,7 @@ pub enum BackendCommand { Messages(MessagesCommand), #[cfg(any(feature = "imap", feature = "jmap", feature = "maildir"))] #[command(subcommand)] - Attachments(crate::attachments::command::AttachmentsCommand), + Attachments(crate::attachments::cli::AttachmentsCommand), #[cfg(feature = "imap")] #[command(subcommand)] @@ -187,35 +184,35 @@ impl BackendCommand { Self::Completions(cmd) => cmd.execute(printer, HimalayaCli::command()), Self::Mailboxes(cmd) => { - let config = Config::from_paths_or_default(config_paths)?; + let config = load_or_wizard(config_paths)?; let (_, account_config) = config.get_account(account_name)?; cmd.execute(printer, config, account_config, backend) } Self::Envelopes(cmd) => { - let config = Config::from_paths_or_default(config_paths)?; + let config = load_or_wizard(config_paths)?; let (_, account_config) = config.get_account(account_name)?; cmd.execute(printer, config, account_config, backend) } Self::Flags(cmd) => { - let config = Config::from_paths_or_default(config_paths)?; + let config = load_or_wizard(config_paths)?; let (_, account_config) = config.get_account(account_name)?; cmd.execute(printer, config, account_config, backend) } Self::Messages(cmd) => { - let config = Config::from_paths_or_default(config_paths)?; + let config = load_or_wizard(config_paths)?; let (_, account_config) = config.get_account(account_name)?; cmd.execute(printer, config, account_config, backend) } #[cfg(any(feature = "imap", feature = "jmap", feature = "maildir"))] Self::Attachments(cmd) => { - let config = Config::from_paths_or_default(config_paths)?; + let config = load_or_wizard(config_paths)?; let (_, account_config) = config.get_account(account_name)?; cmd.execute(printer, config, account_config, backend) } #[cfg(feature = "imap")] Self::Imap(cmd) => { - let config = Config::from_paths_or_default(config_paths)?; + let config = load_or_wizard(config_paths)?; let (account_name, mut account_config) = config.get_account(account_name)?; let Some(imap_config) = account_config.imap.take() else { @@ -228,7 +225,7 @@ impl BackendCommand { } #[cfg(feature = "jmap")] Self::Jmap(cmd) => { - let config = Config::from_paths_or_default(config_paths)?; + let config = load_or_wizard(config_paths)?; let (account_name, mut account_config) = config.get_account(account_name)?; let Some(jmap_config) = account_config.jmap.take() else { @@ -241,7 +238,7 @@ impl BackendCommand { } #[cfg(feature = "maildir")] Self::Maildir(cmd) => { - let config = Config::from_paths_or_default(config_paths)?; + let config = load_or_wizard(config_paths)?; let (account_name, mut account_config) = config.get_account(account_name)?; let Some(maildir_config) = account_config.maildir.take() else { @@ -254,7 +251,7 @@ impl BackendCommand { } #[cfg(feature = "smtp")] Self::Smtp(cmd) => { - let config = Config::from_paths_or_default(config_paths)?; + let config = load_or_wizard(config_paths)?; let (account_name, mut account_config) = config.get_account(account_name)?; let Some(smtp_config) = account_config.smtp.take() else { @@ -268,3 +265,13 @@ impl BackendCommand { } } } + +/// Loads `Config` from `paths`, or runs the wizard if no config file +/// is found. Centralises the `Result>` → `Config` +/// adaptation so call sites stay readable. +fn load_or_wizard(paths: &[PathBuf]) -> Result { + match Config::from_paths_or_default(paths)? { + Some(config) => Ok(config), + None => crate::wizard::run_or_exit(&Config::target_path(paths)?), + } +} diff --git a/src/config.rs b/src/config.rs index 15bb5579..84e548c9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,19 +1,22 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::{collections::HashMap, fs, path::Path, path::PathBuf}; +use anyhow::{Context, Result}; use comfy_table::ContentArrangement; -use pimalaya_toolbox::{ - config::{shell_expanded_string, TomlConfig}, - sasl::{Sasl, SaslAnonymous, SaslLogin, SaslMechanism, SaslPlain}, +use pimalaya_config::{ secret::{Secret, SecretError}, - stream::{Rustls, RustlsCrypto, Tls, TlsProvider}, + toml::{shell_expanded_string, TomlConfig}, }; -use serde::Deserialize; +use pimalaya_stream::{ + sasl::{Sasl, SaslAnonymous, SaslLogin, SaslMechanism, SaslPlain}, + tls::{Rustls, RustlsCrypto, Tls, TlsProvider}, +}; +use serde::{Deserialize, Serialize}; use url::Url; /// Global configuration. /// /// Represents the whole TOML user's configuration file. -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct Config { pub downloads_dir: Option, @@ -43,8 +46,28 @@ impl TomlConfig for Config { } } +impl Config { + /// Serializes `self` to TOML and writes it to `path`, creating + /// any missing parent directories. Used by the wizard to persist + /// a freshly-built configuration. + pub fn write(&self, path: &Path) -> Result<()> { + let toml = toml::to_string_pretty(self).context("Serialize TOML config error")?; + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).with_context(|| { + format!("Create TOML config parent `{}` error", parent.display()) + })?; + } + + fs::write(path, toml) + .with_context(|| format!("Write TOML config `{}` error", path.display()))?; + + Ok(()) + } +} + /// Account configuration. -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct AccountConfig { #[serde(default)] @@ -64,7 +87,7 @@ pub struct AccountConfig { pub smtp: Option, } -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum TableArrangementConfig { #[default] @@ -85,7 +108,7 @@ impl From for ContentArrangement { /// IMAP configuration. #[allow(unused)] -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct ImapConfig { pub url: Url, @@ -99,7 +122,7 @@ pub struct ImapConfig { /// Maildir configuration. #[allow(unused)] -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct MaildirConfig { pub root: PathBuf, @@ -107,7 +130,7 @@ pub struct MaildirConfig { /// SMTP configuration. #[allow(unused)] -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct SmtpConfig { pub url: Url, @@ -120,7 +143,7 @@ pub struct SmtpConfig { } /// SSL/TLS configuration. -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct TlsConfig { pub provider: Option, @@ -130,7 +153,7 @@ pub struct TlsConfig { } /// SSL/TLS provider configuration. -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum TlsProviderConfig { Rustls, @@ -138,14 +161,14 @@ pub enum TlsProviderConfig { } /// Rustls configuration. -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct RustlsConfig { pub crypto: Option, } /// Rustls crypto provider configuration. -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum RustlsCryptoConfig { Aws, @@ -173,7 +196,7 @@ impl TryFrom for Tls { } /// SASL configuration. -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct SaslConfig { pub mechanism: Option, @@ -183,7 +206,7 @@ pub struct SaslConfig { } /// SASL mechanism configuration. -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub enum SaslMechanismConfig { Login, @@ -193,7 +216,7 @@ pub enum SaslMechanismConfig { } /// SASL LOGIN configuration. -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct SaslLoginConfig { #[serde(deserialize_with = "shell_expanded_string")] @@ -202,7 +225,7 @@ pub struct SaslLoginConfig { } /// SASL PLAIN configuration. -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct SaslPlainConfig { pub authzid: Option, @@ -212,7 +235,7 @@ pub struct SaslPlainConfig { } /// SASL ANONYMOUS configuration. -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct SaslAnonymousConfig { pub message: Option, @@ -254,7 +277,7 @@ impl TryFrom for Sasl { } /// JMAP configuration. -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct JmapConfig { /// The JMAP server address. @@ -286,7 +309,7 @@ pub struct JmapConfig { /// JMAP authentication configuration. // https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum JmapAuthConfig { Header(Secret), @@ -303,8 +326,8 @@ pub enum JmapAuthConfig { } #[cfg(feature = "jmap")] -impl TryFrom for pimalaya_toolbox::stream::jmap::JmapAuth { - type Error = pimalaya_toolbox::secret::SecretError; +impl TryFrom for crate::jmap::session::JmapAuth { + type Error = pimalaya_config::secret::SecretError; fn try_from(config: JmapAuthConfig) -> Result { match config { diff --git a/src/email_client.rs b/src/email_client.rs new file mode 100644 index 00000000..1baacd18 --- /dev/null +++ b/src/email_client.rs @@ -0,0 +1,119 @@ +//! Builder for the unified [`io_email::client::EmailClient`] used by +//! cross-protocol shared subcommands (`mailboxes`, `envelopes`, +//! `flags`, `messages`). +//! +//! The legacy per-backend dispatch — three nearly-identical +//! `if backend.allows_X() { … resume loop … }` blocks per command — +//! is replaced by a single call to [`build`] that returns a fully +//! authenticated [`EmailContext`]. The shared command then calls one +//! [`EmailClient`] method and renders the result. +//! +//! Construction is still backend-asymmetric (IMAP needs TLS + SASL, +//! JMAP needs an HTTP credential, Maildir just needs a root path), +//! and that asymmetry is collapsed here. We delegate to the existing +//! transitional [`ImapSession`] / [`JmapSession`] helpers for the +//! handshake/auth flow, then bridge the resulting `(stream, context)` +//! pairs into [`io_imap::client::ImapClient`] / [`io_jmap::client::JmapClient`] +//! via their `from_parts` constructors. +//! +//! [`ImapSession`]: crate::imap::session::ImapSession +//! [`JmapSession`]: crate::jmap::session::JmapSession + +use std::path::PathBuf; + +use anyhow::{bail, Result}; +use comfy_table::ContentArrangement; +use io_email::client::EmailClient; + +use crate::{ + account::Account, + cli::BackendArg, + config::{AccountConfig, Config}, +}; + +/// Bundle handed to shared commands: a fully-built [`EmailClient`] +/// plus the account-level rendering settings the per-backend +/// dispatchers used to extract independently. +pub struct EmailContext { + pub client: EmailClient, + #[allow(dead_code)] + pub downloads_dir: PathBuf, + pub table_preset: String, + pub table_arrangement: ContentArrangement, +} + +/// Builds an [`EmailContext`] from `(config, account_config, backend)`. +/// +/// Tries each backend in `imap → jmap → maildir` order, picking the +/// first one whose config block is present and whose [`BackendArg`] +/// filter allows it. Bails when nothing matches. SMTP is omitted on +/// purpose: none of the shared read-side operations have an SMTP +/// implementation. +pub fn build( + config: Config, + mut account_config: AccountConfig, + backend: BackendArg, +) -> Result { + #[cfg(feature = "imap")] + if backend.allows_imap() { + if let Some(imap_config) = account_config.imap.take() { + use crate::imap::session::ImapSession; + use io_imap::client::ImapClient; + + let account = Account::new(config, account_config, imap_config)?; + let session = ImapSession::new( + account.backend.url.clone(), + account.backend.tls.clone().try_into()?, + account.backend.starttls, + account.backend.sasl.clone().try_into()?, + )?; + let client = ImapClient::from_parts(session.stream, session.context); + return Ok(EmailContext { + client: client.into(), + downloads_dir: account.downloads_dir, + table_preset: account.table_preset, + table_arrangement: account.table_arrangement, + }); + } + } + + #[cfg(feature = "jmap")] + if backend.allows_jmap() { + if let Some(jmap_config) = account_config.jmap.take() { + use crate::jmap::session::JmapSession; + use io_jmap::client::JmapClient; + + let account = Account::new(config, account_config, jmap_config)?; + let session = JmapSession::new( + account.backend.server.clone(), + account.backend.tls.clone().try_into()?, + account.backend.auth.clone().try_into()?, + )?; + let client = JmapClient::from_parts(session.stream, session.http_auth, session.session); + return Ok(EmailContext { + client: client.into(), + downloads_dir: account.downloads_dir, + table_preset: account.table_preset, + table_arrangement: account.table_arrangement, + }); + } + } + + #[cfg(feature = "maildir")] + if backend.allows_maildir() { + if let Some(maildir_config) = account_config.maildir.take() { + use io_maildir::client::MaildirClient; + + let account = Account::new(config, account_config, maildir_config)?; + let client = MaildirClient::new(account.backend.root.clone()); + return Ok(EmailContext { + client: client.into(), + downloads_dir: account.downloads_dir, + table_preset: account.table_preset, + table_arrangement: account.table_arrangement, + }); + } + } + + bail!("no backend matching `{backend}` is configured for this account") +} diff --git a/src/envelopes/command.rs b/src/envelopes/cli.rs similarity index 94% rename from src/envelopes/command.rs rename to src/envelopes/cli.rs index 8fed85e0..886155a5 100644 --- a/src/envelopes/command.rs +++ b/src/envelopes/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::{ cli::BackendArg, diff --git a/src/envelopes/list.rs b/src/envelopes/list.rs index 8548bcf9..5f75ec95 100644 --- a/src/envelopes/list.rs +++ b/src/envelopes/list.rs @@ -1,22 +1,14 @@ -#[cfg(feature = "maildir")] -use std::collections::{BTreeMap, BTreeSet}; -#[cfg(any(feature = "imap", feature = "jmap"))] -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::{ - account::Account, cli::BackendArg, config::{AccountConfig, Config}, + email_client::build, envelopes::table::EnvelopesTable, }; -#[cfg(any(feature = "imap", feature = "jmap"))] -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// List envelopes for the active account, regardless of the underlying /// backend (IMAP, JMAP or Maildir). #[derive(Debug, Parser)] @@ -50,172 +42,18 @@ impl EnvelopesListCommand { self, printer: &mut impl Printer, config: Config, - mut account_config: AccountConfig, + account_config: AccountConfig, backend: BackendArg, ) -> Result<()> { - #[cfg(feature = "imap")] - if backend.allows_imap() { - if let Some(imap_config) = account_config.imap.take() { - use io_email::imap::envelope_list::{EnvelopeList, EnvelopeListResult}; - use io_imap::types::mailbox::Mailbox; - use pimalaya_toolbox::stream::imap::ImapSession; + let mut ctx = build(config, account_config, backend)?; + let envelopes = + ctx.client + .list_envelopes(&self.mailbox, Some(self.page), Some(self.page_size))?; - let account = Account::new(config, account_config, imap_config)?; - let mut session = ImapSession::new( - account.backend.url.clone(), - account.backend.tls.clone().try_into()?, - account.backend.starttls, - account.backend.sasl.clone().try_into()?, - )?; - - let mailbox: Mailbox<'static> = self.mailbox.clone().try_into()?; - let mut coroutine = EnvelopeList::new( - session.context, - mailbox, - Some(self.page), - Some(self.page_size), - ); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let envelopes = loop { - match coroutine.resume(arg.take()) { - EnvelopeListResult::Ok(envelopes) => break envelopes, - EnvelopeListResult::WantsRead => { - let n = session.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - EnvelopeListResult::WantsWrite(bytes) => { - session.stream.write_all(&bytes)?; - } - EnvelopeListResult::Err(err) => bail!("{err}"), - } - }; - - return printer.out(EnvelopesTable { - preset: account.table_preset, - arrangement: account.table_arrangement, - envelopes, - }); - } - } - - #[cfg(feature = "jmap")] - if backend.allows_jmap() { - if let Some(jmap_config) = account_config.jmap.take() { - use io_email::jmap::envelope_list::{EnvelopeList, EnvelopeListResult}; - use pimalaya_toolbox::stream::jmap::JmapSession; - - let account = Account::new(config, account_config, jmap_config)?; - let mut session = JmapSession::new( - account.backend.server.clone(), - account.backend.tls.clone().try_into()?, - account.backend.auth.clone().try_into()?, - )?; - let mut coroutine = EnvelopeList::new( - &session.session, - &session.http_auth, - Some(self.page), - Some(self.page_size), - )?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let envelopes = loop { - match coroutine.resume(arg.take()) { - EnvelopeListResult::Ok(envelopes) => break envelopes, - EnvelopeListResult::WantsRead => { - let n = session.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - EnvelopeListResult::WantsWrite(bytes) => { - session.stream.write_all(&bytes)?; - } - EnvelopeListResult::Err(err) => bail!("{err}"), - } - }; - - return printer.out(EnvelopesTable { - preset: account.table_preset, - arrangement: account.table_arrangement, - envelopes, - }); - } - } - - #[cfg(feature = "maildir")] - if backend.allows_maildir() { - if let Some(maildir_config) = account_config.maildir.take() { - use io_email::maildir::envelope_list::{ - EnvelopeList, EnvelopeListArg, EnvelopeListResult, - }; - use io_maildir::maildir::Maildir; - - let account = Account::new(config, account_config, maildir_config)?; - let path = account.backend.root.join(&self.mailbox); - let maildir = Maildir::try_from(path)?; - - let mut coroutine = - EnvelopeList::new(maildir, Some(self.page), Some(self.page_size)); - let mut arg: Option = None; - - let envelopes = loop { - match coroutine.resume(arg.take()) { - EnvelopeListResult::Ok(envelopes) => break envelopes, - EnvelopeListResult::WantsDirRead(paths) => { - arg = Some(EnvelopeListArg::DirRead(read_dirs(&paths)?)); - } - EnvelopeListResult::WantsFileRead(paths) => { - arg = Some(EnvelopeListArg::FileRead(read_files(&paths)?)); - } - EnvelopeListResult::Err(err) => bail!("{err}"), - } - }; - - return printer.out(EnvelopesTable { - preset: account.table_preset, - arrangement: account.table_arrangement, - envelopes, - }); - } - } - - bail!("no backend matching `{backend}` is configured for this account") + printer.out(EnvelopesTable { + preset: ctx.table_preset, + arrangement: ctx.table_arrangement, + envelopes, + }) } } - -#[cfg(feature = "maildir")] -fn read_dirs(paths: &BTreeSet) -> Result>> { - use std::fs; - - let mut out = BTreeMap::new(); - - for path in paths { - let mut entries = BTreeSet::new(); - - for entry in fs::read_dir(path)? { - let entry = entry?; - - if let Some(s) = entry.path().to_str() { - entries.insert(s.to_owned()); - } - } - - out.insert(path.clone(), entries); - } - - Ok(out) -} - -#[cfg(feature = "maildir")] -fn read_files(paths: &BTreeSet) -> Result>> { - use std::fs; - - let mut out = BTreeMap::new(); - - for path in paths { - out.insert(path.clone(), fs::read(path)?); - } - - Ok(out) -} diff --git a/src/envelopes/mod.rs b/src/envelopes/mod.rs index 8ca0c9cd..0cf8a1a4 100644 --- a/src/envelopes/mod.rs +++ b/src/envelopes/mod.rs @@ -1,3 +1,3 @@ -pub mod command; +pub mod cli; pub mod list; pub mod table; diff --git a/src/flags/add.rs b/src/flags/add.rs index b852fbe3..7b8f99eb 100644 --- a/src/flags/add.rs +++ b/src/flags/add.rs @@ -1,20 +1,14 @@ -#[cfg(any(feature = "imap", feature = "jmap"))] -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::{ - account::Account, cli::BackendArg, config::{AccountConfig, Config}, + email_client::build, flags::arg::{FlagsArg, MailboxFlag, MessageIdsArg}, }; -#[cfg(any(feature = "imap", feature = "jmap"))] -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Add flag(s) to message(s) for the active account. #[derive(Debug, Parser)] pub struct FlagsAddCommand { @@ -31,164 +25,16 @@ impl FlagsAddCommand { self, printer: &mut impl Printer, config: Config, - mut account_config: AccountConfig, + account_config: AccountConfig, backend: BackendArg, ) -> Result<()> { - #[cfg(feature = "imap")] - if backend.allows_imap() { - if let Some(imap_config) = account_config.imap.take() { - use io_email::imap::flag_add::{FlagAdd, FlagAddResult}; - use io_imap::types::{mailbox::Mailbox, sequence::SequenceSet}; - use pimalaya_toolbox::stream::imap::ImapSession; + let mut ctx = build(config, account_config, backend)?; - let account = Account::new(config, account_config, imap_config)?; - let mut session = ImapSession::new( - account.backend.url.clone(), - account.backend.tls.clone().try_into()?, - account.backend.starttls, - account.backend.sasl.clone().try_into()?, - )?; + let ids: Vec<&str> = self.ids.inner.iter().map(String::as_str).collect(); + let flags: Vec = self.flags.inner.iter().map(Into::into).collect(); - let mailbox: Mailbox<'static> = self.mailbox.inner.clone().try_into()?; - let sequence_set: SequenceSet = self.ids.inner.join(",").as_str().try_into()?; - let imap_flags = self.flags.inner.iter().map(|f| f.imap()).collect(); - let mut coroutine = - FlagAdd::new(session.context, mailbox, sequence_set, imap_flags, true); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; + ctx.client.add_flags(&self.mailbox.inner, &ids, &flags)?; - loop { - match coroutine.resume(arg.take()) { - FlagAddResult::Ok => break, - FlagAddResult::WantsRead => { - let n = session.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - FlagAddResult::WantsWrite(bytes) => { - session.stream.write_all(&bytes)?; - } - FlagAddResult::Err(err) => bail!("{err}"), - } - } - - return printer.out(Message::new("Flag(s) successfully added")); - } - } - - #[cfg(feature = "jmap")] - if backend.allows_jmap() { - if let Some(jmap_config) = account_config.jmap.take() { - use io_email::jmap::flag_add::{FlagAdd, FlagAddResult}; - use pimalaya_toolbox::stream::jmap::JmapSession; - - let account = Account::new(config, account_config, jmap_config)?; - let mut session = JmapSession::new( - account.backend.server.clone(), - account.backend.tls.clone().try_into()?, - account.backend.auth.clone().try_into()?, - )?; - - let keywords: Vec = self - .flags - .inner - .iter() - .map(|f| f.jmap().to_owned()) - .collect(); - let mut coroutine = FlagAdd::new( - &session.session, - &session.http_auth, - self.ids.inner.iter().cloned(), - keywords, - )?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - FlagAddResult::Ok => break, - FlagAddResult::WantsRead => { - let n = session.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - FlagAddResult::WantsWrite(bytes) => { - session.stream.write_all(&bytes)?; - } - FlagAddResult::Err(err) => bail!("{err}"), - } - } - - return printer.out(Message::new("Flag(s) successfully added")); - } - } - - #[cfg(feature = "maildir")] - if backend.allows_maildir() { - if let Some(maildir_config) = account_config.maildir.take() { - use io_email::maildir::flag_add::{FlagAdd, FlagAddArg, FlagAddResult}; - use io_maildir::{ - flag::{Flag, Flags}, - maildir::Maildir, - }; - - use crate::maildir::runtime; - - let account = Account::new(config, account_config, maildir_config)?; - let path = account.backend.root.join(&self.mailbox.inner); - let maildir = Maildir::try_from(path)?; - let maildir_flags: Flags = self.flags.inner.iter().map(|f| Flag::from(f)).collect(); - - for id in &self.ids.inner { - let mut coroutine = - FlagAdd::new(maildir.clone(), id.as_str(), maildir_flags.clone()); - let mut arg: Option = None; - - loop { - match coroutine.resume(arg.take()) { - FlagAddResult::Ok => break, - FlagAddResult::WantsDirRead(paths) => { - arg = Some(FlagAddArg::DirRead(read_dirs(&paths)?)); - } - FlagAddResult::WantsRename(pairs) => { - runtime::rename(pairs)?; - arg = Some(FlagAddArg::Rename); - } - FlagAddResult::Err(err) => bail!("{err}"), - } - } - } - - return printer.out(Message::new("Flag(s) successfully added")); - } - } - - bail!("no backend matching `{backend}` is configured for this account") + printer.out(Message::new("Flag(s) successfully added")) } } - -#[cfg(feature = "maildir")] -fn read_dirs( - paths: &std::collections::BTreeSet, -) -> Result>> { - use std::{ - collections::{BTreeMap, BTreeSet}, - fs, - }; - - let mut out = BTreeMap::new(); - - for path in paths { - let mut entries = BTreeSet::new(); - - for entry in fs::read_dir(path)? { - let entry = entry?; - - if let Some(s) = entry.path().to_str() { - entries.insert(s.to_owned()); - } - } - - out.insert(path.clone(), entries); - } - - Ok(out) -} diff --git a/src/flags/arg.rs b/src/flags/arg.rs index d3252b16..4bdab0db 100644 --- a/src/flags/arg.rs +++ b/src/flags/arg.rs @@ -53,6 +53,20 @@ impl From<&FlagArg> for io_maildir::flag::Flag { } } +impl From<&FlagArg> for io_email::flag::Flag { + fn from(flag: &FlagArg) -> Self { + use io_email::flag::Flag; + + match flag { + FlagArg::Seen => Flag::Seen, + FlagArg::Answered => Flag::Answered, + FlagArg::Flagged => Flag::Flagged, + FlagArg::Deleted => Flag::Deleted, + FlagArg::Draft => Flag::Draft, + } + } +} + #[derive(Debug, Parser)] pub struct MessageIdsArg { /// Identifier(s) of message(s) (IMAP UID, JMAP email ID, Maildir filename id). diff --git a/src/flags/command.rs b/src/flags/cli.rs similarity index 95% rename from src/flags/command.rs rename to src/flags/cli.rs index b9201fde..2c929dd3 100644 --- a/src/flags/command.rs +++ b/src/flags/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::{ cli::BackendArg, diff --git a/src/flags/delete.rs b/src/flags/delete.rs index 4c41e9df..9ed13359 100644 --- a/src/flags/delete.rs +++ b/src/flags/delete.rs @@ -1,20 +1,14 @@ -#[cfg(any(feature = "imap", feature = "jmap"))] -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::{ - account::Account, cli::BackendArg, config::{AccountConfig, Config}, + email_client::build, flags::arg::{FlagsArg, MailboxFlag, MessageIdsArg}, }; -#[cfg(any(feature = "imap", feature = "jmap"))] -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Remove flag(s) from message(s) for the active account. #[derive(Debug, Parser)] pub struct FlagsDeleteCommand { @@ -31,164 +25,16 @@ impl FlagsDeleteCommand { self, printer: &mut impl Printer, config: Config, - mut account_config: AccountConfig, + account_config: AccountConfig, backend: BackendArg, ) -> Result<()> { - #[cfg(feature = "imap")] - if backend.allows_imap() { - if let Some(imap_config) = account_config.imap.take() { - use io_email::imap::flag_delete::{FlagDelete, FlagDeleteResult}; - use io_imap::types::{mailbox::Mailbox, sequence::SequenceSet}; - use pimalaya_toolbox::stream::imap::ImapSession; + let mut ctx = build(config, account_config, backend)?; - let account = Account::new(config, account_config, imap_config)?; - let mut session = ImapSession::new( - account.backend.url.clone(), - account.backend.tls.clone().try_into()?, - account.backend.starttls, - account.backend.sasl.clone().try_into()?, - )?; + let ids: Vec<&str> = self.ids.inner.iter().map(String::as_str).collect(); + let flags: Vec = self.flags.inner.iter().map(Into::into).collect(); - let mailbox: Mailbox<'static> = self.mailbox.inner.clone().try_into()?; - let sequence_set: SequenceSet = self.ids.inner.join(",").as_str().try_into()?; - let imap_flags = self.flags.inner.iter().map(|f| f.imap()).collect(); - let mut coroutine = - FlagDelete::new(session.context, mailbox, sequence_set, imap_flags, true); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; + ctx.client.delete_flags(&self.mailbox.inner, &ids, &flags)?; - loop { - match coroutine.resume(arg.take()) { - FlagDeleteResult::Ok => break, - FlagDeleteResult::WantsRead => { - let n = session.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - FlagDeleteResult::WantsWrite(bytes) => { - session.stream.write_all(&bytes)?; - } - FlagDeleteResult::Err(err) => bail!("{err}"), - } - } - - return printer.out(Message::new("Flag(s) successfully removed")); - } - } - - #[cfg(feature = "jmap")] - if backend.allows_jmap() { - if let Some(jmap_config) = account_config.jmap.take() { - use io_email::jmap::flag_delete::{FlagDelete, FlagDeleteResult}; - use pimalaya_toolbox::stream::jmap::JmapSession; - - let account = Account::new(config, account_config, jmap_config)?; - let mut session = JmapSession::new( - account.backend.server.clone(), - account.backend.tls.clone().try_into()?, - account.backend.auth.clone().try_into()?, - )?; - - let keywords: Vec = self - .flags - .inner - .iter() - .map(|f| f.jmap().to_owned()) - .collect(); - let mut coroutine = FlagDelete::new( - &session.session, - &session.http_auth, - self.ids.inner.iter().cloned(), - keywords, - )?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - FlagDeleteResult::Ok => break, - FlagDeleteResult::WantsRead => { - let n = session.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - FlagDeleteResult::WantsWrite(bytes) => { - session.stream.write_all(&bytes)?; - } - FlagDeleteResult::Err(err) => bail!("{err}"), - } - } - - return printer.out(Message::new("Flag(s) successfully removed")); - } - } - - #[cfg(feature = "maildir")] - if backend.allows_maildir() { - if let Some(maildir_config) = account_config.maildir.take() { - use io_email::maildir::flag_delete::{FlagDelete, FlagDeleteArg, FlagDeleteResult}; - use io_maildir::{ - flag::{Flag, Flags}, - maildir::Maildir, - }; - - use crate::maildir::runtime; - - let account = Account::new(config, account_config, maildir_config)?; - let path = account.backend.root.join(&self.mailbox.inner); - let maildir = Maildir::try_from(path)?; - let maildir_flags: Flags = self.flags.inner.iter().map(|f| Flag::from(f)).collect(); - - for id in &self.ids.inner { - let mut coroutine = - FlagDelete::new(maildir.clone(), id.as_str(), maildir_flags.clone()); - let mut arg: Option = None; - - loop { - match coroutine.resume(arg.take()) { - FlagDeleteResult::Ok => break, - FlagDeleteResult::WantsDirRead(paths) => { - arg = Some(FlagDeleteArg::DirRead(read_dirs(&paths)?)); - } - FlagDeleteResult::WantsRename(pairs) => { - runtime::rename(pairs)?; - arg = Some(FlagDeleteArg::Rename); - } - FlagDeleteResult::Err(err) => bail!("{err}"), - } - } - } - - return printer.out(Message::new("Flag(s) successfully removed")); - } - } - - bail!("no backend matching `{backend}` is configured for this account") + printer.out(Message::new("Flag(s) successfully removed")) } } - -#[cfg(feature = "maildir")] -fn read_dirs( - paths: &std::collections::BTreeSet, -) -> Result>> { - use std::{ - collections::{BTreeMap, BTreeSet}, - fs, - }; - - let mut out = BTreeMap::new(); - - for path in paths { - let mut entries = BTreeSet::new(); - - for entry in fs::read_dir(path)? { - let entry = entry?; - - if let Some(s) = entry.path().to_str() { - entries.insert(s.to_owned()); - } - } - - out.insert(path.clone(), entries); - } - - Ok(out) -} diff --git a/src/flags/mod.rs b/src/flags/mod.rs index c11a643e..e4941f3c 100644 --- a/src/flags/mod.rs +++ b/src/flags/mod.rs @@ -1,5 +1,5 @@ pub mod add; pub mod arg; -pub mod command; +pub mod cli; pub mod delete; pub mod set; diff --git a/src/flags/set.rs b/src/flags/set.rs index 8292ef8b..225a0617 100644 --- a/src/flags/set.rs +++ b/src/flags/set.rs @@ -1,21 +1,15 @@ -#[cfg(any(feature = "imap", feature = "jmap"))] -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::{ - account::Account, cli::BackendArg, config::{AccountConfig, Config}, + email_client::build, flags::arg::{FlagsArg, MailboxFlag, MessageIdsArg}, }; -#[cfg(any(feature = "imap", feature = "jmap"))] -const READ_BUFFER_SIZE: usize = 16 * 1024; - -/// Replace flag(s) on message(s) for the active account. +/// Replace the flags of message(s) with the given set. #[derive(Debug, Parser)] pub struct FlagsSetCommand { #[command(flatten)] @@ -31,164 +25,16 @@ impl FlagsSetCommand { self, printer: &mut impl Printer, config: Config, - mut account_config: AccountConfig, + account_config: AccountConfig, backend: BackendArg, ) -> Result<()> { - #[cfg(feature = "imap")] - if backend.allows_imap() { - if let Some(imap_config) = account_config.imap.take() { - use io_email::imap::flag_set::{FlagSet, FlagSetResult}; - use io_imap::types::{mailbox::Mailbox, sequence::SequenceSet}; - use pimalaya_toolbox::stream::imap::ImapSession; + let mut ctx = build(config, account_config, backend)?; - let account = Account::new(config, account_config, imap_config)?; - let mut session = ImapSession::new( - account.backend.url.clone(), - account.backend.tls.clone().try_into()?, - account.backend.starttls, - account.backend.sasl.clone().try_into()?, - )?; + let ids: Vec<&str> = self.ids.inner.iter().map(String::as_str).collect(); + let flags: Vec = self.flags.inner.iter().map(Into::into).collect(); - let mailbox: Mailbox<'static> = self.mailbox.inner.clone().try_into()?; - let sequence_set: SequenceSet = self.ids.inner.join(",").as_str().try_into()?; - let imap_flags = self.flags.inner.iter().map(|f| f.imap()).collect(); - let mut coroutine = - FlagSet::new(session.context, mailbox, sequence_set, imap_flags, true); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; + ctx.client.set_flags(&self.mailbox.inner, &ids, &flags)?; - loop { - match coroutine.resume(arg.take()) { - FlagSetResult::Ok => break, - FlagSetResult::WantsRead => { - let n = session.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - FlagSetResult::WantsWrite(bytes) => { - session.stream.write_all(&bytes)?; - } - FlagSetResult::Err(err) => bail!("{err}"), - } - } - - return printer.out(Message::new("Flag(s) successfully set")); - } - } - - #[cfg(feature = "jmap")] - if backend.allows_jmap() { - if let Some(jmap_config) = account_config.jmap.take() { - use io_email::jmap::flag_set::{FlagSet, FlagSetResult}; - use pimalaya_toolbox::stream::jmap::JmapSession; - - let account = Account::new(config, account_config, jmap_config)?; - let mut session = JmapSession::new( - account.backend.server.clone(), - account.backend.tls.clone().try_into()?, - account.backend.auth.clone().try_into()?, - )?; - - let keywords: Vec = self - .flags - .inner - .iter() - .map(|f| f.jmap().to_owned()) - .collect(); - let mut coroutine = FlagSet::new( - &session.session, - &session.http_auth, - self.ids.inner.iter().cloned(), - keywords, - )?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - FlagSetResult::Ok => break, - FlagSetResult::WantsRead => { - let n = session.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - FlagSetResult::WantsWrite(bytes) => { - session.stream.write_all(&bytes)?; - } - FlagSetResult::Err(err) => bail!("{err}"), - } - } - - return printer.out(Message::new("Flag(s) successfully set")); - } - } - - #[cfg(feature = "maildir")] - if backend.allows_maildir() { - if let Some(maildir_config) = account_config.maildir.take() { - use io_email::maildir::flag_set::{FlagSet, FlagSetArg, FlagSetResult}; - use io_maildir::{ - flag::{Flag, Flags}, - maildir::Maildir, - }; - - use crate::maildir::runtime; - - let account = Account::new(config, account_config, maildir_config)?; - let path = account.backend.root.join(&self.mailbox.inner); - let maildir = Maildir::try_from(path)?; - let maildir_flags: Flags = self.flags.inner.iter().map(|f| Flag::from(f)).collect(); - - for id in &self.ids.inner { - let mut coroutine = - FlagSet::new(maildir.clone(), id.as_str(), maildir_flags.clone()); - let mut arg: Option = None; - - loop { - match coroutine.resume(arg.take()) { - FlagSetResult::Ok => break, - FlagSetResult::WantsDirRead(paths) => { - arg = Some(FlagSetArg::DirRead(read_dirs(&paths)?)); - } - FlagSetResult::WantsRename(pairs) => { - runtime::rename(pairs)?; - arg = Some(FlagSetArg::Rename); - } - FlagSetResult::Err(err) => bail!("{err}"), - } - } - } - - return printer.out(Message::new("Flag(s) successfully set")); - } - } - - bail!("no backend matching `{backend}` is configured for this account") + printer.out(Message::new("Flag(s) successfully set")) } } - -#[cfg(feature = "maildir")] -fn read_dirs( - paths: &std::collections::BTreeSet, -) -> Result>> { - use std::{ - collections::{BTreeMap, BTreeSet}, - fs, - }; - - let mut out = BTreeMap::new(); - - for path in paths { - let mut entries = BTreeSet::new(); - - for entry in fs::read_dir(path)? { - let entry = entry?; - - if let Some(s) = entry.path().to_str() { - entries.insert(s.to_owned()); - } - } - - out.insert(path.clone(), entries); - } - - Ok(out) -} diff --git a/src/imap/account.rs b/src/imap/account.rs index 8ea00605..c99e9472 100644 --- a/src/imap/account.rs +++ b/src/imap/account.rs @@ -1,17 +1,21 @@ use anyhow::Result; -use pimalaya_toolbox::stream::imap::ImapSession; +use io_imap::client::ImapClient; -use crate::{account::Account, config::ImapConfig}; +use crate::{account::Account, config::ImapConfig, imap::session::ImapSession}; pub type ImapAccount = Account; impl ImapAccount { - pub fn new_imap_session(&self) -> Result { - ImapSession::new( + /// Opens the IMAP connection (TCP/TLS/STARTTLS, greeting, SASL), + /// then hands the established stream and context off to a fresh + /// [`ImapClient`]. + pub fn new_imap_client(&self) -> Result { + let session = ImapSession::new( self.backend.url.clone(), self.backend.tls.clone().try_into()?, self.backend.starttls, self.backend.sasl.clone().try_into()?, - ) + )?; + Ok(ImapClient::from_parts(session.stream, session.context)) } } diff --git a/src/imap/command.rs b/src/imap/cli.rs similarity index 82% rename from src/imap/command.rs rename to src/imap/cli.rs index 52fdb1f8..67ff2dae 100644 --- a/src/imap/command.rs +++ b/src/imap/cli.rs @@ -1,10 +1,10 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::imap::{ - account::ImapAccount, envelope::command::ImapEnvelopeCommand, flag::command::ImapFlagCommand, - id::ImapIdCommand, mailbox::command::ImapMailboxCommand, message::command::ImapMessageCommand, + account::ImapAccount, envelope::cli::ImapEnvelopeCommand, flag::cli::ImapFlagCommand, + id::ImapIdCommand, mailbox::cli::ImapMailboxCommand, message::cli::ImapMessageCommand, }; /// IMAP CLI (requires the `imap` cargo feature). diff --git a/src/imap/envelope/command.rs b/src/imap/envelope/cli.rs similarity index 96% rename from src/imap/envelope/command.rs rename to src/imap/envelope/cli.rs index 95c81d8a..e4182038 100644 --- a/src/imap/envelope/command.rs +++ b/src/imap/envelope/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::imap::{ account::ImapAccount, diff --git a/src/imap/envelope/get.rs b/src/imap/envelope/get.rs index 3b7f7190..fae18c13 100644 --- a/src/imap/envelope/get.rs +++ b/src/imap/envelope/get.rs @@ -1,20 +1,13 @@ -use std::{ - fmt, - io::{Read, Write}, - num::NonZeroU32, -}; +use std::fmt; use anyhow::{bail, Result}; use clap::Parser; use comfy_table::{Cell, Row, Table}; -use io_imap::{ - rfc3501::{fetch::*, select::*}, - types::{ - core::Vec1, - fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, - }, +use io_imap::types::{ + core::Vec1, + fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, }; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::imap::{ @@ -23,8 +16,6 @@ use crate::imap::{ mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag}, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Get a single IMAP envelope. /// /// This command displays detailed envelope information for a specific @@ -47,54 +38,25 @@ pub struct ImapEnvelopeGetCommand { impl ImapEnvelopeGetCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - if !self.mailbox_no_select.inner { - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - imap.context = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, .. } => break context, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; + client.select(mailbox)?; } - let Some(id) = NonZeroU32::new(self.id) else { + if self.id == 0 { bail!("ID must be non-zero"); - }; + } let item_names = MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::Envelope]); - let mut coroutine = ImapMessageFetchFirst::new(imap.context, id, item_names, !self.seq); - let mut arg: Option<&[u8]> = None; + let sequence_set = self.id.to_string().parse()?; + let mut data = client.fetch(sequence_set, item_names, !self.seq)?; - let items = loop { - match coroutine.resume(arg.take()) { - ImapMessageFetchFirstResult::Ok { items, .. } => break items, - ImapMessageFetchFirstResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageFetchFirstResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageFetchFirstResult::Err { err, .. } => bail!("{err}"), - } + let Some((_, items)) = data.pop_first() else { + bail!("No envelope returned for ID {}", self.id); }; let table = EnvelopeTable { diff --git a/src/imap/envelope/list.rs b/src/imap/envelope/list.rs index a5b4fb2d..8c866d8b 100644 --- a/src/imap/envelope/list.rs +++ b/src/imap/envelope/list.rs @@ -1,25 +1,17 @@ -use std::{ - collections::BTreeMap, - fmt, - io::{Read, Write}, - num::NonZeroU32, -}; +use std::{collections::BTreeMap, fmt, num::NonZeroU32}; use anyhow::{bail, Result}; use clap::Parser; use comfy_table::{Cell, ContentArrangement, Row, Table}; -use io_imap::{ - rfc3501::{fetch::*, select::*, status::*}, - types::{ - core::Vec1, - envelope::Address, - fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, - sequence::{SeqOrUid, Sequence, SequenceSet}, - status::{StatusDataItem, StatusDataItemName}, - }, +use io_imap::types::{ + core::Vec1, + envelope::Address, + fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, + sequence::{SeqOrUid, Sequence, SequenceSet}, + status::{StatusDataItem, StatusDataItemName}, }; use log::debug; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use rfc2047_decoder::{Decoder, RecoverStrategy}; use serde::Serialize; @@ -28,8 +20,6 @@ use crate::imap::{ mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag}, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// List IMAP envelopes from the given mailbox. /// /// This command displays envelopes for messages in the specified @@ -61,57 +51,17 @@ pub struct ImapEnvelopeListCommand { impl ImapEnvelopeListCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let exists = if self.mailbox_no_select.inner { - let mut coroutine = - ImapMailboxStatus::new(imap.context, mailbox, &[StatusDataItemName::Messages]); - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMailboxStatusResult::Ok { context, items } => { - imap.context = context; - break items.into_iter().find_map(|i| match i { - StatusDataItem::Messages(exists) => Some(exists), - _ => None, - }); - } - ImapMailboxStatusResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxStatusResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxStatusResult::Err { err, .. } => bail!("{err}"), - } - } + let items = client.status(mailbox, &[StatusDataItemName::Messages])?; + items.into_iter().find_map(|i| match i { + StatusDataItem::Messages(exists) => Some(exists), + _ => None, + }) } else { - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, data } => { - imap.context = context; - break data.exists; - } - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - } + client.select(mailbox)?.exists }; let mut has_sequence = false; @@ -131,28 +81,7 @@ impl ImapEnvelopeListCommand { MessageDataItemName::Envelope, ]); - let mut coroutine = ImapMessageFetch::new( - imap.context, - sequence_set, - item_names, - !self.sequence && has_sequence, - ); - let mut arg: Option<&[u8]> = None; - - let data = loop { - match coroutine.resume(arg.take()) { - ImapMessageFetchResult::Ok { data, .. } => break data, - ImapMessageFetchResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageFetchResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageFetchResult::Err { err, .. } => bail!("{err}"), - } - }; + let data = client.fetch(sequence_set, item_names, !self.sequence && has_sequence)?; let table = EnvelopesTable { preset: account.table_preset, diff --git a/src/imap/envelope/mod.rs b/src/imap/envelope/mod.rs index 016a6859..62247284 100644 --- a/src/imap/envelope/mod.rs +++ b/src/imap/envelope/mod.rs @@ -1,4 +1,4 @@ -pub mod command; +pub mod cli; pub mod get; pub mod list; pub mod search; diff --git a/src/imap/envelope/search.rs b/src/imap/envelope/search.rs index 34bd906a..159d2d12 100644 --- a/src/imap/envelope/search.rs +++ b/src/imap/envelope/search.rs @@ -1,20 +1,14 @@ -use std::{ - fmt, - io::{Read, Write}, -}; +use std::fmt; use anyhow::{anyhow, bail, Result}; use clap::Parser; use comfy_table::{Cell, ContentArrangement, Row, Table}; -use io_imap::{ - rfc3501::{search::*, select::*}, - types::{ - core::{AString, Vec1}, - datetime::NaiveDate, - search::SearchKey, - }, +use io_imap::types::{ + core::{AString, Vec1}, + datetime::NaiveDate, + search::SearchKey, }; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::imap::{ @@ -22,8 +16,6 @@ use crate::imap::{ mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag}, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Search IMAP messages by criteria. /// /// This command searches for messages matching the given criteria and @@ -67,50 +59,15 @@ pub struct ImapEnvelopeSearchCommand { impl ImapEnvelopeSearchCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - if !self.mailbox_no_select.inner { - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - imap.context = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, .. } => break context, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; + client.select(mailbox)?; } let criteria = parse_query(&self.query)?; - - let mut coroutine = ImapMessageSearch::new(imap.context, criteria, !self.seq); - let mut arg: Option<&[u8]> = None; - - let ids = loop { - match coroutine.resume(arg.take()) { - ImapMessageSearchResult::Ok { ids, .. } => break ids, - ImapMessageSearchResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageSearchResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageSearchResult::Err { err, .. } => bail!("{err}"), - } - }; + let ids = client.search(criteria, !self.seq)?; let table = SearchTable { preset: account.table_preset, diff --git a/src/imap/envelope/sort.rs b/src/imap/envelope/sort.rs index 0edc45cb..4bb27381 100644 --- a/src/imap/envelope/sort.rs +++ b/src/imap/envelope/sort.rs @@ -1,28 +1,19 @@ -use std::{ - fmt, - io::{Read, Write}, -}; +use std::fmt; use anyhow::{bail, Result}; use clap::Parser; use comfy_table::{presets, Cell, ContentArrangement, Row, Table}; -use io_imap::{ - rfc3501::select::*, - rfc5256::sort::*, - types::{ - core::Vec1, - extensions::sort::{SortCriterion, SortKey}, - }, +use io_imap::types::{ + core::Vec1, + extensions::sort::{SortCriterion, SortKey}, }; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::imap::{ account::ImapAccount, envelope::search::parse_query, mailbox::arg::MailboxNameOptionalArg, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Sort messages by criteria. /// /// This command searches for messages matching the given query and @@ -61,59 +52,19 @@ pub struct ImapEnvelopeSortCommand { impl ImapEnvelopeSortCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; + client.select(mailbox)?; - // SELECT mailbox - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - let context = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, .. } => break context, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; - - // Parse sort criteria let sort_key = parse_sort_key(&self.sort)?; let sort_criteria = Vec1::unvalidated(vec![SortCriterion { reverse: self.reverse, key: sort_key, }]); - - // Parse search criteria let search_criteria = parse_query(&self.query)?; - // SORT - let mut coroutine = - ImapMailboxSort::new(context, sort_criteria, search_criteria, !self.seq); - let mut arg: Option<&[u8]> = None; - - let ids = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSortResult::Ok { ids, .. } => break ids, - ImapMailboxSortResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSortResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSortResult::Err { err, .. } => bail!("{err}"), - } - }; + let ids = client.sort(sort_criteria, search_criteria, !self.seq)?; let table = SortResultsTable::new(ids, !self.seq); diff --git a/src/imap/envelope/thread.rs b/src/imap/envelope/thread.rs index 5495bbc3..6352f2d7 100644 --- a/src/imap/envelope/thread.rs +++ b/src/imap/envelope/thread.rs @@ -1,22 +1,16 @@ -use std::{ - collections::HashMap, - fmt, - io::{Read, Write}, - num::NonZeroU32, -}; +use std::{collections::HashMap, fmt, num::NonZeroU32}; use anyhow::{bail, Result}; use clap::Parser; use io_imap::{ - rfc3501::{fetch::*, select::*}, - rfc5256::thread::*, + client::ImapClient, types::{ extensions::thread::{Thread, ThreadingAlgorithm}, fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, sequence::SequenceSet, }, }; -use pimalaya_toolbox::{stream::imap::ImapSession, terminal::printer::Printer}; +use pimalaya_cli::printer::Printer; use serde::{ser::SerializeStruct, Serialize, Serializer}; use crate::imap::{ @@ -25,8 +19,6 @@ use crate::imap::{ mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag}, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Thread IMAP messages by algorithm. /// /// This command groups messages into conversation threads using the @@ -57,64 +49,21 @@ pub struct ImapEnvelopeThreadCommand { impl ImapEnvelopeThreadCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - if !self.mailbox_no_select.inner { - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - imap.context = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, .. } => break context, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; + client.select(mailbox)?; } let algorithm = parse_algorithm(&self.algorithm)?; let search_criteria = parse_query(&self.query)?; - let mut coroutine = - ImapMessageThread::new(imap.context, algorithm, search_criteria, !self.seq); - let mut arg: Option<&[u8]> = None; + let threads = client.thread(algorithm, search_criteria, !self.seq)?; - let threads = loop { - match coroutine.resume(arg.take()) { - ImapMessageThreadResult::Ok { - context, threads, .. - } => { - imap.context = context; - break threads; - } - ImapMessageThreadResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageThreadResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageThreadResult::Err { err, .. } => bail!("{err}"), - } - }; - - // Collect all message IDs from threads to fetch subjects let all_ids = collect_thread_ids(&threads); - - // Fetch subjects for all messages in threads let subjects = if !all_ids.is_empty() { - fetch_subjects(imap, &all_ids, !self.seq)? + fetch_subjects(&mut client, &all_ids, !self.seq)? } else { HashMap::new() }; @@ -162,7 +111,7 @@ fn collect_thread_ids_recursive(thread: &Thread, ids: &mut Vec) { } fn fetch_subjects( - mut imap: ImapSession, + client: &mut ImapClient, ids: &[NonZeroU32], uid: bool, ) -> Result> { @@ -170,7 +119,6 @@ fn fetch_subjects( return Ok(HashMap::new()); } - // Build sequence set from IDs let seq_set_str = ids .iter() .map(|id| id.to_string()) @@ -183,24 +131,7 @@ fn fetch_subjects( MessageDataItemName::Uid, ]); - let mut coroutine = ImapMessageFetch::new(imap.context, sequence_set, item_names, uid); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let data = loop { - match coroutine.resume(arg.take()) { - ImapMessageFetchResult::Ok { data, .. } => break data, - ImapMessageFetchResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageFetchResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageFetchResult::Err { err, .. } => bail!("{err}"), - } - }; + let data = client.fetch(sequence_set, item_names, uid)?; let mut subjects: HashMap = HashMap::new(); diff --git a/src/imap/flag/add.rs b/src/imap/flag/add.rs index 9803c369..3751d9c3 100644 --- a/src/imap/flag/add.rs +++ b/src/imap/flag/add.rs @@ -1,23 +1,16 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::{ - rfc3501::{select::*, store::*}, - types::{ - flag::{Flag, StoreType}, - IntoStatic, - }, +use io_imap::types::{ + flag::{Flag, StoreType}, + IntoStatic, }; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{ account::ImapAccount, mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag}, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Add IMAP flag(s) to message(s). /// /// This command adds the given flags to messages identified by the @@ -43,29 +36,11 @@ pub struct ImapFlagAddCommand { impl ImapFlagAddCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - if !self.mailbox_no_select.inner { - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - imap.context = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, .. } => break context, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; + client.select(mailbox)?; } let sequence_set = self.sequence_set.as_str().try_into()?; @@ -75,29 +50,7 @@ impl ImapFlagAddCommand { .map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static())) .collect::>()?; - let mut coroutine = ImapMessageStoreSilent::new( - imap.context, - sequence_set, - StoreType::Add, - flags, - !self.seq, - ); - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMessageStoreSilentResult::Ok(_) => break, - ImapMessageStoreSilentResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageStoreSilentResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageStoreSilentResult::Err { err, .. } => bail!("{err}"), - } - } + client.store(sequence_set, StoreType::Add, flags, !self.seq)?; printer.out(Message::new("Flag(s) successfully added")) } diff --git a/src/imap/flag/command.rs b/src/imap/flag/cli.rs similarity index 94% rename from src/imap/flag/command.rs rename to src/imap/flag/cli.rs index 571680af..5201f3ed 100644 --- a/src/imap/flag/command.rs +++ b/src/imap/flag/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::imap::{ account::ImapAccount, diff --git a/src/imap/flag/list.rs b/src/imap/flag/list.rs index 1e409049..ab9168d4 100644 --- a/src/imap/flag/list.rs +++ b/src/imap/flag/list.rs @@ -1,23 +1,14 @@ -use std::{ - collections::BTreeMap, - fmt, - io::{Read, Write}, -}; +use std::{collections::BTreeMap, fmt}; -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; use comfy_table::{Cell, ContentArrangement, Row, Table}; -use io_imap::{ - rfc3501::select::*, - types::flag::{Flag, FlagPerm}, -}; -use pimalaya_toolbox::terminal::printer::Printer; +use io_imap::types::flag::{Flag, FlagPerm}; +use pimalaya_cli::printer::Printer; use serde::{Serialize, Serializer}; use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// List available IMAP flags for the given mailbox. /// /// This command displays the flags and permanent flags that are @@ -31,32 +22,12 @@ pub struct ImapFlagListCommand { impl ImapFlagListCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let (flags, permanent_flags) = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { data, .. } => { - break ( - data.flags.unwrap_or_default(), - data.permanent_flags.unwrap_or_default(), - ) - } - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; + let data = client.select(mailbox)?; + let flags = data.flags.unwrap_or_default(); + let permanent_flags = data.permanent_flags.unwrap_or_default(); let table = FlagsTable { preset: account.table_preset, diff --git a/src/imap/flag/mod.rs b/src/imap/flag/mod.rs index 1fcd0bcf..193a2726 100644 --- a/src/imap/flag/mod.rs +++ b/src/imap/flag/mod.rs @@ -1,5 +1,5 @@ pub mod add; -pub mod command; +pub mod cli; pub mod list; pub mod remove; pub mod set; diff --git a/src/imap/flag/remove.rs b/src/imap/flag/remove.rs index abfe9ab6..b4d6d54a 100644 --- a/src/imap/flag/remove.rs +++ b/src/imap/flag/remove.rs @@ -1,23 +1,16 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::{ - rfc3501::{select::*, store::*}, - types::{ - flag::{Flag, StoreType}, - IntoStatic, - }, +use io_imap::types::{ + flag::{Flag, StoreType}, + IntoStatic, }; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{ account::ImapAccount, mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag}, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Remove IMAP flag(s) from message(s). /// /// This command removes the specified flag(s) from message(s) @@ -43,29 +36,11 @@ pub struct ImapFlagRemoveCommand { impl ImapFlagRemoveCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - if !self.mailbox_no_select.inner { - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - imap.context = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, .. } => break context, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; + client.select(mailbox)?; } let sequence_set = self.sequence_set.as_str().try_into()?; @@ -75,29 +50,7 @@ impl ImapFlagRemoveCommand { .map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static())) .collect::>()?; - let mut coroutine = ImapMessageStoreSilent::new( - imap.context, - sequence_set, - StoreType::Remove, - flags, - !self.seq, - ); - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMessageStoreSilentResult::Ok(_) => break, - ImapMessageStoreSilentResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageStoreSilentResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageStoreSilentResult::Err { err, .. } => bail!("{err}"), - } - } + client.store(sequence_set, StoreType::Remove, flags, !self.seq)?; printer.out(Message::new("Flag(s) successfully removed")) } diff --git a/src/imap/flag/set.rs b/src/imap/flag/set.rs index 12472e43..f5a92709 100644 --- a/src/imap/flag/set.rs +++ b/src/imap/flag/set.rs @@ -1,23 +1,16 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::{ - rfc3501::{select::*, store::*}, - types::{ - flag::{Flag, StoreType}, - IntoStatic, - }, +use io_imap::types::{ + flag::{Flag, StoreType}, + IntoStatic, }; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{ account::ImapAccount, mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag}, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Set IMAP flag(s) on message(s), replacing any existing flags. /// /// This command replaces all existing flags on messages identified by @@ -43,29 +36,11 @@ pub struct ImapFlagSetCommand { impl ImapFlagSetCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - if !self.mailbox_no_select.inner { - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - imap.context = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, .. } => break context, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; + client.select(mailbox)?; } let sequence_set = self.sequence_set.as_str().try_into()?; @@ -75,29 +50,7 @@ impl ImapFlagSetCommand { .map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static())) .collect::>()?; - let mut coroutine = ImapMessageStoreSilent::new( - imap.context, - sequence_set, - StoreType::Replace, - flags, - !self.seq, - ); - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMessageStoreSilentResult::Ok(_) => break, - ImapMessageStoreSilentResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageStoreSilentResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageStoreSilentResult::Err { err, .. } => bail!("{err}"), - } - } + client.store(sequence_set, StoreType::Replace, flags, !self.seq)?; printer.out(Message::new("Flag(s) successfully replaced")) } diff --git a/src/imap/id.rs b/src/imap/id.rs index 7ed27c04..34378d87 100644 --- a/src/imap/id.rs +++ b/src/imap/id.rs @@ -1,26 +1,17 @@ -use std::{ - collections::HashMap, - fmt, - io::{Read, Write}, -}; +use std::{collections::HashMap, fmt}; -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; use comfy_table::{Cell, Row, Table}; -use io_imap::{ - rfc2971::id::*, - types::{ - core::{IString, NString}, - IntoStatic, - }, +use io_imap::types::{ + core::{IString, NString}, + IntoStatic, }; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::imap::account::ImapAccount; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Get information about the IMAP server. /// /// This command allows you to exchange parameters with the IMAP @@ -37,7 +28,7 @@ pub struct ImapIdCommand { impl ImapIdCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mut params = HashMap::new(); params.extend([ @@ -63,24 +54,7 @@ impl ImapIdCommand { params.extend(more); } - let mut coroutine = ImapServerId::new(imap.context, Some(params.into_iter().collect())); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let params = loop { - match coroutine.resume(arg.take()) { - ImapServerIdResult::Ok { server_id, .. } => break server_id, - ImapServerIdResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapServerIdResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapServerIdResult::Err { err, .. } => bail!("{err}"), - } - }; + let params = client.id(Some(params.into_iter().collect()))?; let table = ServerIdTable { preset: account.table_preset, diff --git a/src/imap/mailbox/command.rs b/src/imap/mailbox/cli.rs similarity index 97% rename from src/imap/mailbox/command.rs rename to src/imap/mailbox/cli.rs index 3498caae..f1df4c11 100644 --- a/src/imap/mailbox/command.rs +++ b/src/imap/mailbox/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::imap::{ account::ImapAccount, diff --git a/src/imap/mailbox/close.rs b/src/imap/mailbox/close.rs index 581d4812..aa01ea99 100644 --- a/src/imap/mailbox/close.rs +++ b/src/imap/mailbox/close.rs @@ -1,14 +1,9 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::rfc3501::close::*; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::account::ImapAccount; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Close the current, selected mailbox. /// /// This command also expunges the current mailbox and returns to the @@ -23,27 +18,8 @@ pub struct ImapMailboxCloseCommand; impl ImapMailboxCloseCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; - - let mut close_coroutine = ImapMailboxClose::new(imap.context); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - loop { - match close_coroutine.resume(arg.take()) { - ImapMailboxCloseResult::Ok(_) => break, - ImapMailboxCloseResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxCloseResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxCloseResult::Err { err, .. } => bail!("{err}"), - } - } - + let mut client = account.new_imap_client()?; + client.close()?; printer.out(Message::new("Mailbox successfully closed")) } } diff --git a/src/imap/mailbox/create.rs b/src/imap/mailbox/create.rs index 665b7be8..a3b11d3d 100644 --- a/src/imap/mailbox/create.rs +++ b/src/imap/mailbox/create.rs @@ -1,14 +1,9 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::rfc3501::create::*; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Create the given mailbox. /// /// This command allows you to create a new mailbox using the given @@ -21,29 +16,9 @@ pub struct ImapMailboxCreateCommand { impl ImapMailboxCreateCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; - + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - - let mut coroutine = ImapMailboxCreate::new(imap.context, mailbox); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMailboxCreateResult::Ok(_) => break, - ImapMailboxCreateResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxCreateResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxCreateResult::Err { err, .. } => bail!("{err}"), - } - } - + client.create(mailbox)?; printer.out(Message::new("Mailbox successfully created")) } } diff --git a/src/imap/mailbox/delete.rs b/src/imap/mailbox/delete.rs index ef5fb6aa..e4e34cdc 100644 --- a/src/imap/mailbox/delete.rs +++ b/src/imap/mailbox/delete.rs @@ -1,14 +1,9 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::rfc3501::delete::*; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Delete the given mailbox. /// /// All emails from the given mailbox are definitely deleted. The @@ -21,28 +16,9 @@ pub struct ImapMailboxDeleteCommand { impl ImapMailboxDeleteCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - - let mut coroutine = ImapMailboxDelete::new(imap.context, mailbox); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMailboxDeleteResult::Ok(_) => break, - ImapMailboxDeleteResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxDeleteResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxDeleteResult::Err { err, .. } => bail!("{err}"), - } - } - + client.delete(mailbox)?; printer.out(Message::new("Mailbox successfully deleted")) } } diff --git a/src/imap/mailbox/expunge.rs b/src/imap/mailbox/expunge.rs index 280658d2..af2afe9a 100644 --- a/src/imap/mailbox/expunge.rs +++ b/src/imap/mailbox/expunge.rs @@ -1,17 +1,12 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::rfc3501::{expunge::*, select::*}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{ account::ImapAccount, mailbox::arg::{MailboxNameArg, MailboxNoSelectFlag}, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Expunge the given mailbox. /// /// All envelopes with the \Deleted flag will be definitely removed @@ -26,48 +21,14 @@ pub struct ImapMailboxExpungeCommand { impl ImapMailboxExpungeCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - if !self.mailbox_no_select.inner { - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - imap.context = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, .. } => break context, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; + client.select(mailbox)?; } - let mut coroutine = ImapMailboxExpunge::new(imap.context); - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMailboxExpungeResult::Ok { .. } => break, - ImapMailboxExpungeResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxExpungeResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxExpungeResult::Err { err, .. } => bail!("{err}"), - } - } + client.expunge()?; printer.out(Message::new("Mailbox successfully expunged")) } diff --git a/src/imap/mailbox/list.rs b/src/imap/mailbox/list.rs index 36a9a88d..c3a5b3e6 100644 --- a/src/imap/mailbox/list.rs +++ b/src/imap/mailbox/list.rs @@ -1,22 +1,14 @@ -use std::{ - fmt, - io::{Read, Write}, -}; +use std::fmt; -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; use comfy_table::{Cell, Row, Table}; -use io_imap::{ - rfc3501::{list::*, lsub::*}, - types::{core::QuotedChar, flag::FlagNameAttribute, mailbox::Mailbox}, -}; -use pimalaya_toolbox::terminal::printer::Printer; +use io_imap::types::{core::QuotedChar, flag::FlagNameAttribute, mailbox::Mailbox}; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::imap::account::ImapAccount; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// List, search and filter mailboxes. /// /// This command allows you to list mailboxes from your IMAP account. @@ -39,48 +31,14 @@ pub struct ImapMailboxListCommand { impl ImapMailboxListCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let reference = self.reference.try_into()?; let pattern = self.pattern.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mailboxes = if self.all { - let mut coroutine = ImapMailboxList::new(imap.context, reference, pattern); - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMailboxListResult::Ok { mailboxes, .. } => break mailboxes, - ImapMailboxListResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxListResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxListResult::Err { err, .. } => bail!("{err}"), - } - } + client.list(reference, pattern)? } else { - let mut coroutine = ImapMailboxLsub::new(imap.context, reference, pattern); - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMailboxLsubResult::Ok { mailboxes, .. } => break mailboxes, - ImapMailboxLsubResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxLsubResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxLsubResult::Err { err, .. } => bail!("{err}"), - } - } + client.lsub(reference, pattern)? }; let table = MailboxesTable { diff --git a/src/imap/mailbox/mod.rs b/src/imap/mailbox/mod.rs index 2fe8fea0..7ccbf20f 100644 --- a/src/imap/mailbox/mod.rs +++ b/src/imap/mailbox/mod.rs @@ -1,6 +1,6 @@ pub mod arg; +pub mod cli; pub mod close; -pub mod command; pub mod create; pub mod delete; pub mod expunge; diff --git a/src/imap/mailbox/purge.rs b/src/imap/mailbox/purge.rs index 231a8791..330bc0cf 100644 --- a/src/imap/mailbox/purge.rs +++ b/src/imap/mailbox/purge.rs @@ -1,20 +1,13 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::{ - rfc3501::{expunge::*, select::*, store::*}, - types::flag::{Flag, StoreType}, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_imap::types::flag::{Flag, StoreType}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{ account::ImapAccount, mailbox::arg::{MailboxNameArg, MailboxNoSelectFlag}, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Shortcut for marking as deleted all envelopes then expunging the /// given mailbox. /// @@ -30,72 +23,20 @@ pub struct ImapMailboxPurgeCommand { impl ImapMailboxPurgeCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - if !self.mailbox_no_select.inner { - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - imap.context = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, .. } => break context, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; + client.select(mailbox)?; } - let mut coroutine = ImapMessageStoreSilent::new( - imap.context, + client.store( "1:*".try_into()?, StoreType::Add, vec![Flag::Deleted], false, - ); - let mut arg: Option<&[u8]> = None; - - imap.context = loop { - match coroutine.resume(arg.take()) { - ImapMessageStoreSilentResult::Ok(context) => break context, - ImapMessageStoreSilentResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageStoreSilentResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageStoreSilentResult::Err { err, .. } => bail!("{err}"), - } - }; - - let mut coroutine = ImapMailboxExpunge::new(imap.context); - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMailboxExpungeResult::Ok { .. } => break, - ImapMailboxExpungeResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxExpungeResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxExpungeResult::Err { err, .. } => bail!("{err}"), - } - } + )?; + client.expunge()?; printer.out(Message::new("Mailbox successfully purged")) } diff --git a/src/imap/mailbox/rename.rs b/src/imap/mailbox/rename.rs index 85df5793..09220529 100644 --- a/src/imap/mailbox/rename.rs +++ b/src/imap/mailbox/rename.rs @@ -1,17 +1,12 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::rfc3501::rename::*; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{ account::ImapAccount, mailbox::arg::{MailboxNameArg, TargetMailboxNameArg}, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Rename the given mailbox. /// /// This command renames an existing mailbox to a new name. @@ -25,29 +20,10 @@ pub struct ImapMailboxRenameCommand { impl ImapMailboxRenameCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let from = self.mailbox_source_name.inner.try_into()?; let to = self.mailbox_dest_name.inner.try_into()?; - - let mut coroutine = ImapMailboxRename::new(imap.context, from, to); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMailboxRenameResult::Ok(_) => break, - ImapMailboxRenameResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxRenameResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxRenameResult::Err { err, .. } => bail!("{err}"), - } - } - + client.rename(from, to)?; printer.out(Message::new("Mailbox successfully renamed")) } } diff --git a/src/imap/mailbox/select.rs b/src/imap/mailbox/select.rs index 8fc12ba0..1afe243d 100644 --- a/src/imap/mailbox/select.rs +++ b/src/imap/mailbox/select.rs @@ -1,14 +1,9 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::rfc3501::select::*; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Select the given mailbox. /// /// This command permanently removes all messages with the \Deleted @@ -25,28 +20,9 @@ pub struct ImapMailboxSelectCommand { impl ImapMailboxSelectCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { .. } => break, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - } - + client.select(mailbox)?; printer.out(Message::new("Mailbox successfully selected")) } } diff --git a/src/imap/mailbox/status.rs b/src/imap/mailbox/status.rs index 56cc665e..131b5339 100644 --- a/src/imap/mailbox/status.rs +++ b/src/imap/mailbox/status.rs @@ -1,22 +1,14 @@ -use std::{ - fmt, - io::{Read, Write}, -}; +use std::fmt; -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; use comfy_table::{Cell, Row, Table}; -use io_imap::{ - rfc3501::status::*, - types::status::{StatusDataItem, StatusDataItemName}, -}; -use pimalaya_toolbox::terminal::printer::Printer; +use io_imap::types::status::{StatusDataItem, StatusDataItemName}; +use pimalaya_cli::printer::Printer; use serde::{Serialize, Serializer}; use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Get the status of the given mailbox. /// /// This command displays status information about a mailbox, @@ -29,7 +21,7 @@ pub struct ImapMailboxStatusCommand { impl ImapMailboxStatusCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; let item_names = vec![ StatusDataItemName::Messages, @@ -39,24 +31,7 @@ impl ImapMailboxStatusCommand { StatusDataItemName::UidValidity, ]; - let mut coroutine = ImapMailboxStatus::new(imap.context, mailbox, item_names); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let items = loop { - match coroutine.resume(arg.take()) { - ImapMailboxStatusResult::Ok { items, .. } => break items, - ImapMailboxStatusResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxStatusResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxStatusResult::Err { err, .. } => bail!("{err}"), - } - }; + let items = client.status(mailbox, item_names)?; let table = MailboxStatusTable { preset: account.table_preset, diff --git a/src/imap/mailbox/subscribe.rs b/src/imap/mailbox/subscribe.rs index efa21f4f..bad78962 100644 --- a/src/imap/mailbox/subscribe.rs +++ b/src/imap/mailbox/subscribe.rs @@ -1,14 +1,9 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::rfc3501::subscribe::*; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Subscribe to the given mailbox. /// /// This command subscribes to a mailbox, making it appear in the @@ -21,28 +16,9 @@ pub struct ImapMailboxSubscribeCommand { impl ImapMailboxSubscribeCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - - let mut coroutine = ImapMailboxSubscribe::new(imap.context, mailbox); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMailboxSubscribeResult::Ok(_) => break, - ImapMailboxSubscribeResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSubscribeResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSubscribeResult::Err { err, .. } => bail!("{err}"), - } - } - + client.subscribe(mailbox)?; printer.out(Message::new("Mailbox successfully subscribed")) } } diff --git a/src/imap/mailbox/unselect.rs b/src/imap/mailbox/unselect.rs index 1d4c2ebf..8a3f51b7 100644 --- a/src/imap/mailbox/unselect.rs +++ b/src/imap/mailbox/unselect.rs @@ -1,14 +1,9 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::rfc3691::unselect::*; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::account::ImapAccount; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Unselect a current, selected mailbox. /// /// Unlike CLOSE, UNSELECT does not expunge deleted messages. @@ -22,27 +17,8 @@ pub struct ImapMailboxUnselectCommand; impl ImapMailboxUnselectCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; - - let mut coroutine = ImapMailboxUnselect::new(imap.context); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMailboxUnselectResult::Ok(_) => break, - ImapMailboxUnselectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxUnselectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxUnselectResult::Err { err, .. } => bail!("{err}"), - } - } - + let mut client = account.new_imap_client()?; + client.unselect()?; printer.out(Message::new("Mailbox successfully unselected")) } } diff --git a/src/imap/mailbox/unsubscribe.rs b/src/imap/mailbox/unsubscribe.rs index 67e727e3..bacd5a2e 100644 --- a/src/imap/mailbox/unsubscribe.rs +++ b/src/imap/mailbox/unsubscribe.rs @@ -1,14 +1,9 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::rfc3501::unsubscribe::*; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Unsubscribe from the given mailbox. /// /// This command unsubscribes from a mailbox, removing it from the @@ -21,28 +16,9 @@ pub struct ImapMailboxUnsubscribeCommand { impl ImapMailboxUnsubscribeCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - - let mut coroutine = ImapMailboxUnsubscribe::new(imap.context, mailbox); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMailboxUnsubscribeResult::Ok(_) => break, - ImapMailboxUnsubscribeResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxUnsubscribeResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxUnsubscribeResult::Err { err, .. } => bail!("{err}"), - } - } - + client.unsubscribe(mailbox)?; printer.out(Message::new("Mailbox successfully unsubscribed")) } } diff --git a/src/imap/message/command.rs b/src/imap/message/cli.rs similarity index 96% rename from src/imap/message/command.rs rename to src/imap/message/cli.rs index 78103bf5..5035fb15 100644 --- a/src/imap/message/command.rs +++ b/src/imap/message/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::imap::{ account::ImapAccount, diff --git a/src/imap/message/copy.rs b/src/imap/message/copy.rs index eafdeb6c..848c446a 100644 --- a/src/imap/message/copy.rs +++ b/src/imap/message/copy.rs @@ -1,20 +1,13 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::{ - rfc3501::{copy::*, select::*}, - types::mailbox::Mailbox, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_imap::types::mailbox::Mailbox; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{ account::ImapAccount, mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag, TargetMailboxNameArg}, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Copy IMAP message(s) to the given mailbox. /// /// This command copies message(s) identified by the given sequence @@ -39,52 +32,17 @@ pub struct ImapMessageCopyCommand { impl ImapMessageCopyCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - if !self.mailbox_no_select.inner { - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - imap.context = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, .. } => break context, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; + client.select(mailbox)?; } let sequence_set = self.sequence_set.as_str().try_into()?; let destination: Mailbox = self.mailbox_dest_name.inner.try_into()?; - let mut coroutine = - ImapMessageCopy::new(imap.context, sequence_set, destination, !self.seq); - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMessageCopyResult::Ok { .. } => break, - ImapMessageCopyResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageCopyResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageCopyResult::Err { err, .. } => bail!("{err}"), - } - } + client.copy(sequence_set, destination, !self.seq)?; printer.out(Message::new("Message(s) successfully copied")) } diff --git a/src/imap/message/export.rs b/src/imap/message/export.rs index cc4ab4dc..7ed425a2 100644 --- a/src/imap/message/export.rs +++ b/src/imap/message/export.rs @@ -1,23 +1,17 @@ use std::{ fs, - io::{self, Read, Write}, - num::NonZeroU32, + io::{self, Write}, path::PathBuf, }; use anyhow::{bail, Result}; use clap::Parser; -use io_imap::{ - rfc3501::{fetch::*, select::*}, - types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, -}; +use io_imap::types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}; use mail_parser::{MessageParser, MimeHeaders}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Export type for message export. #[derive(Debug, Clone, clap::ValueEnum)] pub enum ExportType { @@ -63,34 +57,14 @@ pub struct ImapMessageExportCommand { impl ImapMessageExportCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; + client.select(mailbox)?; - // SELECT mailbox - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - let context = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, .. } => break context, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; - - // FETCH with BODY.PEEK[] to avoid marking as read - let Some(id) = NonZeroU32::new(self.id) else { + if self.id == 0 { bail!("Export message error: ID must be non-zero"); - }; + } let item_names = MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::BodyExt { @@ -99,25 +73,16 @@ impl ImapMessageExportCommand { peek: true, }]); - let mut coroutine = ImapMessageFetchFirst::new(context, id, item_names, !self.seq); - let mut arg: Option<&[u8]> = None; + let sequence_set = self.id.to_string().parse()?; + let mut data = client.fetch(sequence_set, item_names, !self.seq)?; - let items = loop { - match coroutine.resume(arg.take()) { - ImapMessageFetchFirstResult::Ok { items, .. } => break items, - ImapMessageFetchFirstResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageFetchFirstResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageFetchFirstResult::Err { err, .. } => bail!("{err}"), - } + let Some((_, items)) = data.pop_first() else { + bail!( + "Export message `{}` error: no message data returned", + self.id + ); }; - // Extract raw message bytes let mut raw_message: Option> = None; for item in items.into_iter() { if let MessageDataItem::BodyExt { data, .. } = item { diff --git a/src/imap/message/get.rs b/src/imap/message/get.rs index e634ce5d..4a21abe8 100644 --- a/src/imap/message/get.rs +++ b/src/imap/message/get.rs @@ -1,18 +1,11 @@ -use std::{ - fmt, - io::{Read, Write}, - num::NonZeroU32, -}; +use std::fmt; use anyhow::{bail, Result}; use clap::Parser; use comfy_table::{presets, Cell, ContentArrangement, Row, Table}; -use io_imap::{ - rfc3501::{fetch::*, select::*}, - types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, -}; +use io_imap::types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}; use mail_parser::{Addr, Address, ContentType, Message, MessageParser, MimeHeaders}; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::imap::{ @@ -20,8 +13,6 @@ use crate::imap::{ mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag}, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Get a message and display its structure. /// /// This command fetches a message and displays its headers along with @@ -42,32 +33,14 @@ pub struct ImapMessageGetCommand { impl ImapMessageGetCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let Some(id) = NonZeroU32::new(self.id) else { + if self.id == 0 { bail!("ID must be non-zero"); - }; - - let mut buf = [0u8; READ_BUFFER_SIZE]; + } if !self.mailbox_no_select.inner { - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - imap.context = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, .. } => break context, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; + client.select(mailbox)?; } let item_names = @@ -77,22 +50,11 @@ impl ImapMessageGetCommand { peek: true, }]); - let mut coroutine = ImapMessageFetchFirst::new(imap.context, id, item_names, !self.seq); - let mut arg: Option<&[u8]> = None; + let sequence_set = self.id.to_string().parse()?; + let mut data = client.fetch(sequence_set, item_names, !self.seq)?; - let items = loop { - match coroutine.resume(arg.take()) { - ImapMessageFetchFirstResult::Ok { items, .. } => break items, - ImapMessageFetchFirstResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageFetchFirstResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageFetchFirstResult::Err { err, .. } => bail!("{err}"), - } + let Some((_, items)) = data.pop_first() else { + bail!("Get message `{}` error: no message data returned", self.id); }; let mut raw_message: Option> = None; diff --git a/src/imap/message/mod.rs b/src/imap/message/mod.rs index 91cc5818..2cbac40f 100644 --- a/src/imap/message/mod.rs +++ b/src/imap/message/mod.rs @@ -1,4 +1,4 @@ -pub mod command; +pub mod cli; pub mod copy; pub mod export; pub mod get; diff --git a/src/imap/message/move.rs b/src/imap/message/move.rs index 67e9d235..7e82905b 100644 --- a/src/imap/message/move.rs +++ b/src/imap/message/move.rs @@ -1,17 +1,13 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::{rfc3501::select::*, rfc6851::r#move::*, types::mailbox::Mailbox}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_imap::types::mailbox::Mailbox; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{ account::ImapAccount, mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag, TargetMailboxNameArg}, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Move message(s) to the given mailbox. /// /// This command moves messages identified by the given sequence set @@ -37,52 +33,17 @@ pub struct ImapMessageMoveCommand { impl ImapMessageMoveCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - if !self.mailbox_no_select.inner { - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - imap.context = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, .. } => break context, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; + client.select(mailbox)?; } let sequence_set = self.sequence_set.as_str().try_into()?; let destination: Mailbox<'static> = self.mailbox_dest_name.inner.try_into()?; - let mut coroutine = - ImapMessageMove::new(imap.context, sequence_set, destination, !self.seq); - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMessageMoveResult::Ok { .. } => break, - ImapMessageMoveResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageMoveResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageMoveResult::Err { err, .. } => bail!("{err}"), - } - } + client.r#move(sequence_set, destination, !self.seq)?; printer.out(Message::new("Message(s) successfully moved")) } diff --git a/src/imap/message/read.rs b/src/imap/message/read.rs index fdd6ac07..67d6949b 100644 --- a/src/imap/message/read.rs +++ b/src/imap/message/read.rs @@ -1,17 +1,10 @@ -use std::{ - fmt, - io::{Read, Write}, - num::NonZeroU32, -}; +use std::fmt; use anyhow::{bail, Result}; use clap::Parser; -use io_imap::{ - rfc3501::{fetch::*, select::*}, - types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, -}; +use io_imap::types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}; use mail_parser::{Message, MessageParser}; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::imap::{ @@ -19,8 +12,6 @@ use crate::imap::{ mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag}, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Read message content. /// /// This command fetches a message and displays its text content. @@ -47,34 +38,16 @@ pub struct ImapMessageReadCommand { impl ImapMessageReadCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox = self.mailbox_name.inner.try_into()?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - if !self.mailbox_no_select.inner { - let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox); - let mut arg: Option<&[u8]> = None; - - imap.context = loop { - match coroutine.resume(arg.take()) { - ImapMailboxSelectResult::Ok { context, .. } => break context, - ImapMailboxSelectResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMailboxSelectResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"), - } - }; + client.select(mailbox)?; } - let Some(id) = NonZeroU32::new(self.id) else { + if self.id == 0 { bail!("ID must be non-zero"); - }; + } let item_names = MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::BodyExt { @@ -83,22 +56,11 @@ impl ImapMessageReadCommand { peek: true, }]); - let mut coroutine = ImapMessageFetchFirst::new(imap.context, id, item_names, !self.seq); - let mut arg: Option<&[u8]> = None; + let sequence_set = self.id.to_string().parse()?; + let mut data = client.fetch(sequence_set, item_names, !self.seq)?; - let items = loop { - match coroutine.resume(arg.take()) { - ImapMessageFetchFirstResult::Ok { items, .. } => break items, - ImapMessageFetchFirstResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageFetchFirstResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageFetchFirstResult::Err { err, .. } => bail!("{err}"), - } + let Some((_, items)) = data.pop_first() else { + bail!("Read message `{}` error: no message data returned", self.id); }; let mut raw_message: Option> = None; diff --git a/src/imap/message/save.rs b/src/imap/message/save.rs index ea3a724e..b4928c62 100644 --- a/src/imap/message/save.rs +++ b/src/imap/message/save.rs @@ -1,20 +1,14 @@ -use std::io::{stdin, BufRead, IsTerminal, Read, Write}; +use std::io::{stdin, BufRead, IsTerminal}; -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_imap::{ - rfc3501::append::*, - types::{ - core::Literal, extensions::binary::LiteralOrLiteral8, flag::Flag, mailbox::Mailbox, - IntoStatic, - }, +use io_imap::types::{ + core::Literal, extensions::binary::LiteralOrLiteral8, flag::Flag, mailbox::Mailbox, IntoStatic, }; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Save a message to a mailbox. /// /// This command appends a message to the specified mailbox. The @@ -36,7 +30,7 @@ pub struct ImapMessageSaveCommand { impl ImapMessageSaveCommand { pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> { - let mut imap = account.new_imap_session()?; + let mut client = account.new_imap_client()?; let mailbox: Mailbox<'static> = self.mailbox.inner.try_into()?; let message = if !self.message.is_empty() || stdin().is_terminal() || printer.is_json() { self.message @@ -61,24 +55,7 @@ impl ImapMessageSaveCommand { .map(|f| Flag::try_from(f).map(IntoStatic::into_static)) .collect::>()?; - let mut coroutine = ImapMessageAppend::new(imap.context, mailbox, flags, None, message); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - ImapMessageAppendResult::Ok { .. } => break, - ImapMessageAppendResult::WantsRead => { - let n = imap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - ImapMessageAppendResult::WantsWrite(bytes) => { - imap.stream.write_all(&bytes)?; - arg = None; - } - ImapMessageAppendResult::Err { err, .. } => bail!("{err}"), - } - } + client.append(mailbox, flags, None, message)?; printer.out(Message::new("Message successfully saved")) } diff --git a/src/imap/mod.rs b/src/imap/mod.rs index 546f64e6..b1c119b7 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -1,7 +1,8 @@ pub mod account; -pub mod command; +pub mod cli; pub mod envelope; pub mod flag; pub mod id; pub mod mailbox; pub mod message; +pub mod session; diff --git a/src/imap/session.rs b/src/imap/session.rs new file mode 100644 index 00000000..8a29058d --- /dev/null +++ b/src/imap/session.rs @@ -0,0 +1,236 @@ +//! Transitional IMAP session helper ported from `pimalaya-toolbox`. +//! +//! Will be replaced by `io_imap::client::ImapClient` once the +//! protocol-specific subcommands switch over. + +#[cfg(unix)] +use std::os::unix::net::UnixStream; +use std::{ + io::{Read, Write}, + net::TcpStream, +}; + +use anyhow::{bail, Result}; +use io_imap::{ + context::ImapContext, + rfc3501::{ + capability::{ImapCapabilityGet, ImapCapabilityGetResult}, + greeting_with_capability::{ + ImapGreetingWithCapabilityGet, ImapGreetingWithCapabilityGetResult, + }, + login::{ImapSessionLogin, ImapSessionLoginParams, ImapSessionLoginResult}, + starttls::{ImapStartTls, ImapStartTlsResult}, + }, + sasl::authenticate_plain::{ + ImapSessionAuthenticatePlain, ImapSessionAuthenticatePlainParams, + ImapSessionAuthenticatePlainResult, + }, + types::response::Capability, +}; +use log::info; +use pimalaya_stream::{ + sasl::{Sasl, SaslMechanism}, + std::stream::Stream, + tls::{upgrade_tls, Tls}, +}; +#[cfg(windows)] +use uds_windows::UnixStream; +use url::Url; + +const READ_BUFFER_SIZE: usize = 16 * 1024; + +#[derive(Debug)] +pub struct ImapSession { + pub context: ImapContext, + pub stream: Stream, +} + +fn drive_greeting_with_capability( + stream: &mut S, + context: ImapContext, +) -> Result { + let mut buf = [0u8; READ_BUFFER_SIZE]; + let mut coroutine = ImapGreetingWithCapabilityGet::new(context); + let mut arg: Option<&[u8]> = None; + + loop { + match coroutine.resume(arg.take()) { + ImapGreetingWithCapabilityGetResult::Ok(context) => return Ok(context), + ImapGreetingWithCapabilityGetResult::WantsRead => { + let n = stream.read(&mut buf)?; + arg = Some(&buf[..n]); + } + ImapGreetingWithCapabilityGetResult::WantsWrite(bytes) => { + stream.write_all(&bytes)?; + arg = None; + } + ImapGreetingWithCapabilityGetResult::Err { err, .. } => bail!(err), + } + } +} + +fn drive_capability(stream: &mut S, context: ImapContext) -> Result { + let mut buf = [0u8; READ_BUFFER_SIZE]; + let mut coroutine = ImapCapabilityGet::new(context); + let mut arg: Option<&[u8]> = None; + + loop { + match coroutine.resume(arg.take()) { + ImapCapabilityGetResult::Ok(context) => return Ok(context), + ImapCapabilityGetResult::WantsRead => { + let n = stream.read(&mut buf)?; + arg = Some(&buf[..n]); + } + ImapCapabilityGetResult::WantsWrite(bytes) => { + stream.write_all(&bytes)?; + arg = None; + } + ImapCapabilityGetResult::Err { err, .. } => bail!(err), + } + } +} + +fn drive_starttls(stream: &mut S, context: ImapContext) -> Result { + let mut buf = [0u8; READ_BUFFER_SIZE]; + let mut coroutine = ImapStartTls::new(context); + let mut arg: Option<&[u8]> = None; + + loop { + match coroutine.resume(arg.take()) { + ImapStartTlsResult::WantsStartTls { context, .. } => return Ok(context), + ImapStartTlsResult::WantsRead => { + let n = stream.read(&mut buf)?; + arg = Some(&buf[..n]); + } + ImapStartTlsResult::WantsWrite(bytes) => { + stream.write_all(&bytes)?; + arg = None; + } + ImapStartTlsResult::Err { err, .. } => bail!(err), + } + } +} + +impl ImapSession { + pub fn new(url: Url, tls: Tls, starttls: bool, mut sasl: Sasl) -> Result { + info!("connecting to IMAP server using {url}"); + + let context = ImapContext::new(); + let host = url.host_str().unwrap_or("127.0.0.1"); + + let (mut context, mut stream) = match url.scheme() { + scheme if scheme.eq_ignore_ascii_case("imap") => { + let port = url.port().unwrap_or(143); + let mut tcp = TcpStream::connect((host, port))?; + let context = drive_greeting_with_capability(&mut tcp, context)?; + (context, Stream::Tcp(tcp)) + } + scheme if scheme.eq_ignore_ascii_case("imaps") => { + let port = url.port().unwrap_or(993); + let mut tcp = TcpStream::connect((host, port))?; + + let context = if starttls { + drive_starttls(&mut tcp, context)? + } else { + context + }; + + let mut stream = upgrade_tls(host, tcp, &tls, &[b"imap"])?; + + let context = if starttls { + drive_capability(&mut stream, context)? + } else { + drive_greeting_with_capability(&mut stream, context)? + }; + + (context, stream) + } + scheme if scheme.eq_ignore_ascii_case("unix") => { + let sock_path = url.path(); + let mut unix = UnixStream::connect(sock_path)?; + let context = drive_greeting_with_capability(&mut unix, context)?; + (context, Stream::Unix(unix)) + } + scheme => { + bail!("Unknown scheme {scheme}, expected imap, imaps or unix"); + } + }; + + if !context.authenticated { + let ir = context.capability.contains(&Capability::SaslIr); + + let mechanism = sasl + .mechanism + .or(Some(SaslMechanism::Plain).filter(|_| sasl.plain.is_some())) + .or(Some(SaslMechanism::Login).filter(|_| sasl.login.is_some())); + + match mechanism { + None => bail!("no SASL mechanism configured"), + Some(SaslMechanism::Login) => { + let Some(auth) = sasl.login.take() else { + bail!("missing SASL LOGIN configuration"); + }; + + let mut buf = [0u8; READ_BUFFER_SIZE]; + let mut coroutine = ImapSessionLogin::new( + context, + ImapSessionLoginParams::new(auth.username, auth.password)?, + ); + let mut arg: Option<&[u8]> = None; + + context = loop { + match coroutine.resume(arg.take()) { + ImapSessionLoginResult::Ok(c) => break c, + ImapSessionLoginResult::WantsRead => { + let n = stream.read(&mut buf)?; + arg = Some(&buf[..n]); + } + ImapSessionLoginResult::WantsWrite(bytes) => { + stream.write_all(&bytes)?; + arg = None; + } + ImapSessionLoginResult::Err { err, .. } => bail!(err), + } + }; + } + Some(SaslMechanism::Plain) => { + let Some(auth) = sasl.plain.take() else { + bail!("missing SASL PLAIN configuration"); + }; + + let mut buf = [0u8; READ_BUFFER_SIZE]; + let mut coroutine = ImapSessionAuthenticatePlain::new( + context, + ImapSessionAuthenticatePlainParams::new( + auth.authzid, + auth.authcid, + auth.passwd, + ir, + ), + ); + let mut arg: Option<&[u8]> = None; + + context = loop { + match coroutine.resume(arg.take()) { + ImapSessionAuthenticatePlainResult::Ok(c) => break c, + ImapSessionAuthenticatePlainResult::WantsRead => { + let n = stream.read(&mut buf)?; + arg = Some(&buf[..n]); + } + ImapSessionAuthenticatePlainResult::WantsWrite(bytes) => { + stream.write_all(&bytes)?; + arg = None; + } + ImapSessionAuthenticatePlainResult::Err { err, .. } => bail!(err), + } + }; + } + Some(SaslMechanism::Anonymous) => { + unimplemented!("ANONYMOUS SASL mechanism not yet implemented") + } + } + } + + Ok(Self { context, stream }) + } +} diff --git a/src/jmap/account.rs b/src/jmap/account.rs index c4eca4fc..664ad9af 100644 --- a/src/jmap/account.rs +++ b/src/jmap/account.rs @@ -1,16 +1,24 @@ use anyhow::Result; -use pimalaya_toolbox::stream::jmap::JmapSession; +use io_jmap::client::JmapClient; -use crate::{account::Account, config::JmapConfig}; +use crate::{account::Account, config::JmapConfig, jmap::session::JmapSession}; pub type JmapAccount = Account; impl JmapAccount { - pub fn new_jmap_session(&self) -> Result { - JmapSession::new( + /// Establishes the JMAP session (TLS, `/.well-known/jmap` discovery) + /// then hands the resulting stream, bearer token and discovered + /// session off to a fresh [`JmapClient`]. + pub fn new_jmap_client(&self) -> Result { + let session = JmapSession::new( self.backend.server.clone(), self.backend.tls.clone().try_into()?, self.backend.auth.clone().try_into()?, - ) + )?; + Ok(JmapClient::from_parts( + session.stream, + session.http_auth, + session.session, + )) } } diff --git a/src/jmap/command.rs b/src/jmap/cli.rs similarity index 82% rename from src/jmap/command.rs rename to src/jmap/cli.rs index f3979fab..21b00c82 100644 --- a/src/jmap/command.rs +++ b/src/jmap/cli.rs @@ -1,12 +1,12 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::jmap::{ - account::JmapAccount, email::command::JmapEmailCommand, identity::command::JmapIdentityCommand, - mailbox::command::JmapMailboxCommand, query::JmapQueryCommand, - submission::command::JmapSubmissionCommand, thread::command::JmapThreadCommand, - vacation::command::JmapVacationCommand, + account::JmapAccount, email::cli::JmapEmailCommand, identity::cli::JmapIdentityCommand, + mailbox::cli::JmapMailboxCommand, query::JmapQueryCommand, + submission::cli::JmapSubmissionCommand, thread::cli::JmapThreadCommand, + vacation::cli::JmapVacationCommand, }; /// JMAP CLI (requires the `jmap` cargo feature). diff --git a/src/jmap/email/command.rs b/src/jmap/email/cli.rs similarity index 97% rename from src/jmap/email/command.rs rename to src/jmap/email/cli.rs index 40fc36a2..4bd397d4 100644 --- a/src/jmap/email/command.rs +++ b/src/jmap/email/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::jmap::{ account::JmapAccount, diff --git a/src/jmap/email/copy.rs b/src/jmap/email/copy.rs index 48328067..694bc801 100644 --- a/src/jmap/email/copy.rs +++ b/src/jmap/email/copy.rs @@ -1,20 +1,12 @@ -use std::{ - collections::BTreeMap, - io::{Read, Write}, -}; +use std::collections::BTreeMap; use anyhow::{bail, Result}; use clap::Parser; -use io_jmap::rfc8621::{ - email::EmailCopy, - email_copy::{JmapEmailCopy, JmapEmailCopyResult}, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_jmap::rfc8621::email::EmailCopy; +use pimalaya_cli::printer::{Message, Printer}; use crate::jmap::{account::JmapAccount, error::format_set_error}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Copy JMAP emails from another account (Email/copy). #[derive(Debug, Parser)] pub struct JmapEmailCopyCommand { @@ -33,7 +25,7 @@ pub struct JmapEmailCopyCommand { impl JmapEmailCopyCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; let mailbox_ids: BTreeMap = self.mailbox_id.into_iter().map(|m| (m, true)).collect(); @@ -54,34 +46,12 @@ impl JmapEmailCopyCommand { }) .collect(); - let mut coroutine = JmapEmailCopy::new( - &jmap.session, - &jmap.http_auth, - self.from_account.clone(), - emails, - )?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; + let output = client.email_copy(self.from_account.clone(), emails)?; - let not_created = loop { - match coroutine.resume(arg.take()) { - JmapEmailCopyResult::Ok { not_created, .. } => break not_created, - JmapEmailCopyResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapEmailCopyResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapEmailCopyResult::Err(err) => bail!("{err}"), - } - }; - - if !not_created.is_empty() { + if !output.not_created.is_empty() { let mut msg = String::from("Copy JMAP email(s) error"); - for (id, err) in not_created { + for (id, err) in output.not_created { msg.push_str(&format!("\n `{id}`")); msg.push_str(&format_set_error(&err)); } diff --git a/src/jmap/email/delete.rs b/src/jmap/email/delete.rs index c3d9f976..b8110908 100644 --- a/src/jmap/email/delete.rs +++ b/src/jmap/email/delete.rs @@ -1,14 +1,10 @@ -use std::io::{Read, Write}; - use anyhow::{bail, Result}; use clap::Parser; -use io_jmap::rfc8621::email_set::{JmapEmailSet, JmapEmailSetArgs, JmapEmailSetResult}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_jmap::rfc8621::email_set::JmapEmailSetArgs; +use pimalaya_cli::printer::{Message, Printer}; use crate::jmap::{account::JmapAccount, error::format_set_error}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Delete JMAP emails (Email/set destroy). #[derive(Debug, Parser)] pub struct JmapEmailDestroyCommand { @@ -19,37 +15,19 @@ pub struct JmapEmailDestroyCommand { impl JmapEmailDestroyCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; - + let mut client = account.new_jmap_client()?; let mut args = JmapEmailSetArgs::default(); for id in self.ids { args.destroy(id); } - let mut coroutine = JmapEmailSet::new(&jmap.session, &jmap.http_auth, args)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; + let output = client.email_set(args)?; - let not_destroyed = loop { - match coroutine.resume(arg.take()) { - JmapEmailSetResult::Ok { not_destroyed, .. } => break not_destroyed, - JmapEmailSetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapEmailSetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapEmailSetResult::Err(err) => bail!("{err}"), - } - }; - - if !not_destroyed.is_empty() { + if !output.not_destroyed.is_empty() { let mut msg = String::from("Destroy JMAP email(s) error"); - for (id, err) in not_destroyed { + for (id, err) in output.not_destroyed { msg.push_str(&format!("\n `{id}`")); msg.push_str(&format_set_error(&err)); } diff --git a/src/jmap/email/export.rs b/src/jmap/email/export.rs index 246a927c..7d78222b 100644 --- a/src/jmap/email/export.rs +++ b/src/jmap/email/export.rs @@ -1,20 +1,14 @@ -use std::io::{Read, Write}; +use std::net::TcpStream; -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, Result}; use clap::Parser; -use io_jmap::{ - rfc8620::blob_download::{JmapBlobDownload, JmapBlobDownloadResult}, - rfc8621::{ - capabilities::MAIL, - email_get::{JmapEmailGet, JmapEmailGetResult}, - }, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_jmap::{client::JmapClient, rfc8621::capabilities::MAIL}; +use pimalaya_cli::printer::{Message, Printer}; +use pimalaya_stream::tls::upgrade_tls; +use secrecy::SecretString; use url::Url; -use crate::jmap::account::JmapAccount; - -const READ_BUFFER_SIZE: usize = 16 * 1024; +use crate::jmap::{account::JmapAccount, session::JmapAuth}; /// Export a raw RFC 5322 message to stdout (Email/get + blob download). /// @@ -29,51 +23,30 @@ pub struct JmapEmailExportCommand { impl JmapEmailExportCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { let tls = account.backend.tls.clone().try_into()?; - let mut jmap = account.new_jmap_session()?; + let auth: JmapAuth = account.backend.auth.clone().try_into()?; + let http_auth: SecretString = auth.into(); + + let mut client = account.new_jmap_client()?; let properties = Some(vec!["id".to_owned(), "blobId".to_owned()]); + let output = client.email_get(vec![self.id.clone()], properties, false, false, 0)?; - let mut coroutine = JmapEmailGet::new( - &jmap.session, - &jmap.http_auth, - vec![self.id.clone()], - properties, - false, - false, - 0, - )?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let emails = loop { - match coroutine.resume(arg.take()) { - JmapEmailGetResult::Ok { emails, .. } => break emails, - JmapEmailGetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapEmailGetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapEmailGetResult::Err(err) => bail!("{err}"), - } - }; - - let account_id = jmap - .session + let session = client.session().expect("session loaded by new_jmap_client"); + let api_url = session.api_url.clone(); + let account_id = session .primary_accounts .get(MAIL) .map(|s| s.as_str()) .unwrap_or(""); - let blob_id = emails + + let blob_id = output + .emails .into_iter() .next() .and_then(|e| e.blob_id) .ok_or_else(|| anyhow!("Email `{}` not found or has no blobId", self.id))?; - let mut url: Url = jmap - .session + let download_url: Url = session .download_url .replace("{accountId}", account_id) .replace("{blobId}", &blob_id) @@ -81,33 +54,21 @@ impl JmapEmailExportCommand { .replace("{name}", "message.eml") .parse()?; - let mut extra_stream = jmap.connect_if_different(&url, &tls)?; - let mut coroutine = JmapBlobDownload::new(&jmap.http_auth, &url); - let mut arg: Option<&[u8]> = None; - - let data = loop { - match coroutine.resume(arg.take()) { - JmapBlobDownloadResult::Ok { data, .. } => break data, - JmapBlobDownloadResult::WantsRead => { - let stream = extra_stream.as_mut().unwrap_or(&mut jmap.stream); - let n = stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapBlobDownloadResult::WantsWrite(bytes) => { - let stream = extra_stream.as_mut().unwrap_or(&mut jmap.stream); - stream.write_all(&bytes)?; - arg = None; - } - JmapBlobDownloadResult::WantsRedirect { url: new_url, .. } => { - url = new_url; - extra_stream = jmap.connect_if_different(&url, &tls)?; - coroutine = JmapBlobDownload::new(&jmap.http_auth, &url); - arg = None; - } - JmapBlobDownloadResult::Err(err) => bail!("{err}"), - } + let data = if same_authority(&api_url, &download_url) { + client.blob_download(&download_url)? + } else { + let host = download_url.host_str().unwrap_or("localhost"); + let port = download_url.port_or_known_default().unwrap_or(443); + let tcp = TcpStream::connect((host, port))?; + let stream = upgrade_tls(host, tcp, &tls, &[b"http/1.1"])?; + let mut download_client = JmapClient::new(stream, http_auth); + download_client.blob_download(&download_url)? }; printer.out(Message::new(String::from_utf8(data)?)) } } + +fn same_authority(a: &Url, b: &Url) -> bool { + a.host() == b.host() && a.port_or_known_default() == b.port_or_known_default() +} diff --git a/src/jmap/email/get.rs b/src/jmap/email/get.rs index 6ae16663..09894e4c 100644 --- a/src/jmap/email/get.rs +++ b/src/jmap/email/get.rs @@ -1,15 +1,10 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_jmap::rfc8621::email_get::{JmapEmailGet, JmapEmailGetResult}; use log::warn; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::jmap::{account::JmapAccount, email::query::EmailsTable}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Get JMAP emails by ID (Email/get). /// /// Fetches and displays email envelopes as a table. @@ -22,45 +17,17 @@ pub struct JmapEmailGetCommand { impl JmapEmailGetCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; + let output = client.email_get(self.ids.clone(), None, false, false, 0)?; - let mut coroutine = JmapEmailGet::new( - &jmap.session, - &jmap.http_auth, - self.ids.clone(), - None, - false, - false, - 0, - )?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let (emails, not_found) = loop { - match coroutine.resume(arg.take()) { - JmapEmailGetResult::Ok { - emails, not_found, .. - } => break (emails, not_found), - JmapEmailGetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapEmailGetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapEmailGetResult::Err(err) => bail!("{err}"), - } - }; - - for id in not_found { + for id in output.not_found { warn!("email `{id}` not found, ignoring it"); } let table = EmailsTable { preset: account.table_preset, arrangement: account.table_arrangement, - emails, + emails: output.emails, }; printer.out(table) diff --git a/src/jmap/email/import.rs b/src/jmap/email/import.rs index 0fce44b1..d90eef3f 100644 --- a/src/jmap/email/import.rs +++ b/src/jmap/email/import.rs @@ -1,24 +1,21 @@ use std::{ collections::BTreeMap, - io::{stdin, BufRead, IsTerminal, Read, Write}, + io::{stdin, BufRead, IsTerminal}, + net::TcpStream, }; use anyhow::{bail, Result}; use clap::Parser; use io_jmap::{ - rfc8620::blob_upload::{JmapBlobUpload, JmapBlobUploadResult}, - rfc8621::{ - capabilities::MAIL, - email::EmailImport, - email_import::{JmapEmailImport, JmapEmailImportResult}, - }, + client::JmapClient, + rfc8621::{capabilities::MAIL, email::EmailImport}, }; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; +use pimalaya_stream::tls::upgrade_tls; +use secrecy::SecretString; use url::Url; -use crate::jmap::{account::JmapAccount, error::format_set_error}; - -const READ_BUFFER_SIZE: usize = 16 * 1024; +use crate::jmap::{account::JmapAccount, error::format_set_error, session::JmapAuth}; /// Import an RFC 5322 message into a mailbox (upload + Email/import). /// @@ -51,7 +48,10 @@ pub struct JmapEmailImportCommand { impl JmapEmailImportCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { let tls = account.backend.tls.clone().try_into()?; - let mut jmap = account.new_jmap_session()?; + let auth: JmapAuth = account.backend.auth.clone().try_into()?; + let http_auth: SecretString = auth.into(); + + let mut client = account.new_jmap_client()?; let data: Vec = if stdin().is_terminal() || printer.is_json() { self.message @@ -64,39 +64,31 @@ impl JmapEmailImportCommand { lines.join("\r\n").into_bytes() }; - let account_id = jmap - .session + let session = client.session().expect("session loaded by new_jmap_client"); + let api_url = session.api_url.clone(); + let account_id = session .primary_accounts .get(MAIL) .map(|s| s.as_str()) .unwrap_or(""); - let url: Url = jmap - .session + let upload_url: Url = session .upload_url .replace("{accountId}", account_id) .parse()?; - let mut extra_stream = jmap.connect_if_different(&url, &tls)?; - - let mut coroutine = JmapBlobUpload::new(&jmap.http_auth, &url, "message/rfc822", data); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let blob_id = loop { - let stream = extra_stream.as_mut().unwrap_or(&mut jmap.stream); - - match coroutine.resume(arg.take()) { - JmapBlobUploadResult::Ok { blob_id, .. } => break blob_id, - JmapBlobUploadResult::WantsRead => { - let n = stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapBlobUploadResult::WantsWrite(bytes) => { - stream.write_all(&bytes)?; - arg = None; - } - JmapBlobUploadResult::Err(err) => bail!("{err}"), - } + let blob_id = if same_authority(&api_url, &upload_url) { + client + .blob_upload(&upload_url, "message/rfc822", data)? + .blob_id + } else { + let host = upload_url.host_str().unwrap_or("localhost"); + let port = upload_url.port_or_known_default().unwrap_or(443); + let tcp = TcpStream::connect((host, port))?; + let stream = upgrade_tls(host, tcp, &tls, &[b"http/1.1"])?; + let mut upload_client = JmapClient::new(stream, http_auth); + upload_client + .blob_upload(&upload_url, "message/rfc822", data)? + .blob_id }; if self.upload_only { @@ -122,25 +114,9 @@ impl JmapEmailImportCommand { let mut emails = BTreeMap::new(); emails.insert(blob_id.clone(), import); - let mut coroutine = JmapEmailImport::new(&jmap.session, &jmap.http_auth, emails)?; - let mut arg: Option<&[u8]> = None; + let output = client.email_import(emails)?; - let not_created = loop { - match coroutine.resume(arg.take()) { - JmapEmailImportResult::Ok { not_created, .. } => break not_created, - JmapEmailImportResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapEmailImportResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapEmailImportResult::Err(err) => bail!("{err}"), - } - }; - - if let Some(err) = not_created.get(&blob_id) { + if let Some(err) = output.not_created.get(&blob_id) { let mut msg = format!("Import JMAP email from blob `{blob_id}` error"); msg.push_str(&format_set_error(err)); bail!(msg); @@ -149,3 +125,7 @@ impl JmapEmailImportCommand { printer.out(Message::new("Email successfully imported")) } } + +fn same_authority(a: &Url, b: &Url) -> bool { + a.host() == b.host() && a.port_or_known_default() == b.port_or_known_default() +} diff --git a/src/jmap/email/mod.rs b/src/jmap/email/mod.rs index 084aa253..aa3a1d3e 100644 --- a/src/jmap/email/mod.rs +++ b/src/jmap/email/mod.rs @@ -1,4 +1,4 @@ -pub mod command; +pub mod cli; pub mod copy; pub mod delete; pub mod export; diff --git a/src/jmap/email/parse.rs b/src/jmap/email/parse.rs index 0f130b3f..a555f9a1 100644 --- a/src/jmap/email/parse.rs +++ b/src/jmap/email/parse.rs @@ -1,16 +1,11 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_jmap::rfc8621::email_parse::{JmapEmailParse, JmapEmailParseResult}; use log::warn; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::jmap::account::JmapAccount; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Parse RFC 5322 message blobs without storing them (Email/parse). /// /// Useful for reading attached .eml files or message blobs that are @@ -24,44 +19,20 @@ pub struct JmapEmailParseCommand { impl JmapEmailParseCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; + let output = client.email_parse(self.blob_ids.clone(), None)?; - let mut coroutine = - JmapEmailParse::new(&jmap.session, &jmap.http_auth, self.blob_ids.clone(), None)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let (parsed, not_parsable, not_found) = loop { - match coroutine.resume(arg.take()) { - JmapEmailParseResult::Ok { - parsed, - not_parsable, - not_found, - .. - } => break (parsed, not_parsable, not_found), - JmapEmailParseResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapEmailParseResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapEmailParseResult::Err(err) => bail!("{err}"), - } - }; - - for id in not_found { + for id in output.not_found { warn!("blob `{id}` not found, ignoring it"); } - for id in not_parsable { + for id in output.not_parsable { warn!("blob `{id}` not valid MIME message, ignoring it"); } let mut bodies = Vec::new(); - for (_blob_id, email) in parsed { + for (_blob_id, email) in output.parsed { if let Some(body_values) = &email.body_values { if let Some(text_parts) = &email.text_body { for part in text_parts { diff --git a/src/jmap/email/query.rs b/src/jmap/email/query.rs index dcb3b607..2439f5a8 100644 --- a/src/jmap/email/query.rs +++ b/src/jmap/email/query.rs @@ -1,22 +1,16 @@ -use std::{ - fmt, - io::{Read, Write}, -}; +use std::fmt; -use anyhow::{bail, Result}; +use anyhow::Result; use clap::{Parser, ValueEnum}; use comfy_table::{Cell, ContentArrangement, Row, Table}; -use io_jmap::rfc8621::{ - email::{Email, EmailAddress, EmailComparator, EmailFilter, EmailSortProperty}, - email_query::{JmapEmailQuery, JmapEmailQueryResult}, +use io_jmap::rfc8621::email::{ + Email, EmailAddress, EmailComparator, EmailFilter, EmailSortProperty, }; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::jmap::account::JmapAccount; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Query JMAP emails (Email/query + Email/get). /// /// Lists, filters and sorts email envelopes. @@ -93,7 +87,7 @@ pub struct JmapEmailQueryCommand { impl JmapEmailQueryCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; let filter = { let f = EmailFilter { @@ -145,37 +139,18 @@ impl JmapEmailQueryCommand { keyword: None, }]); - let mut coroutine = JmapEmailQuery::new( - &jmap.session, - &jmap.http_auth, + let output = client.email_query( filter, sort, Some(self.page.saturating_sub(1) * self.page_size), Some(self.page_size), None, )?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let emails = loop { - match coroutine.resume(arg.take()) { - JmapEmailQueryResult::Ok { emails, .. } => break emails, - JmapEmailQueryResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapEmailQueryResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapEmailQueryResult::Err(err) => bail!("{err}"), - } - }; let table = EmailsTable { preset: account.table_preset, arrangement: account.table_arrangement, - emails, + emails: output.emails, }; printer.out(table) diff --git a/src/jmap/email/read.rs b/src/jmap/email/read.rs index 049048f5..7563b01d 100644 --- a/src/jmap/email/read.rs +++ b/src/jmap/email/read.rs @@ -1,18 +1,11 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_jmap::rfc8621::{ - email::EmailAddress, - email_get::{JmapEmailGet, JmapEmailGetResult}, -}; +use io_jmap::rfc8621::email::EmailAddress; use log::warn; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::jmap::account::JmapAccount; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Read the content of a JMAP email (Email/get with body). /// /// Shows headers and plain text body by default. @@ -29,44 +22,16 @@ pub struct JmapEmailReadCommand { impl JmapEmailReadCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; + let output = client.email_get(self.ids.clone(), None, !self.html, self.html, 0)?; - let mut coroutine = JmapEmailGet::new( - &jmap.session, - &jmap.http_auth, - self.ids.clone(), - None, - !self.html, - self.html, - 0, - )?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let (emails, not_found) = loop { - match coroutine.resume(arg.take()) { - JmapEmailGetResult::Ok { - emails, not_found, .. - } => break (emails, not_found), - JmapEmailGetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapEmailGetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapEmailGetResult::Err(err) => bail!("{err}"), - } - }; - - for id in not_found { + for id in output.not_found { warn!("email `{id}` not found, ignoring it"); } let mut content = String::new(); - for email in &emails { + for email in &output.emails { if self.html { if let Some(body_values) = &email.body_values { if let Some(html_parts) = &email.html_body { diff --git a/src/jmap/email/update.rs b/src/jmap/email/update.rs index c940e8fb..31139cd2 100644 --- a/src/jmap/email/update.rs +++ b/src/jmap/email/update.rs @@ -1,17 +1,12 @@ -use std::{ - collections::BTreeMap, - io::{Read, Write}, -}; +use std::collections::BTreeMap; use anyhow::{bail, Result}; use clap::Parser; -use io_jmap::rfc8621::email_set::{JmapEmailSet, JmapEmailSetArgs, JmapEmailSetResult}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_jmap::rfc8621::email_set::JmapEmailSetArgs; +use pimalaya_cli::printer::{Message, Printer}; use crate::jmap::{account::JmapAccount, error::format_set_error}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Update JMAP emails via patch operations (Email/set). #[derive(Debug, Parser)] pub struct JmapEmailUpdateCommand { @@ -46,7 +41,7 @@ pub struct JmapEmailUpdateCommand { impl JmapEmailUpdateCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; let mut args = JmapEmailSetArgs::default(); for id in &self.ids { @@ -78,29 +73,12 @@ impl JmapEmailUpdateCommand { } } - let mut coroutine = JmapEmailSet::new(&jmap.session, &jmap.http_auth, args)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; + let output = client.email_set(args)?; - let not_updated = loop { - match coroutine.resume(arg.take()) { - JmapEmailSetResult::Ok { not_updated, .. } => break not_updated, - JmapEmailSetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapEmailSetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapEmailSetResult::Err(err) => bail!("{err}"), - } - }; - - if !not_updated.is_empty() { + if !output.not_updated.is_empty() { let mut msg = String::from("Update JMAP email(s) error"); - for (id, err) in not_updated { + for (id, err) in output.not_updated { msg.push_str(&format!("\n `{id}`")); msg.push_str(&format_set_error(&err)); } diff --git a/src/jmap/identity/command.rs b/src/jmap/identity/cli.rs similarity index 96% rename from src/jmap/identity/command.rs rename to src/jmap/identity/cli.rs index 031ad94f..a2477086 100644 --- a/src/jmap/identity/command.rs +++ b/src/jmap/identity/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::jmap::{ account::JmapAccount, diff --git a/src/jmap/identity/create.rs b/src/jmap/identity/create.rs index 0a72ce9e..e2387f70 100644 --- a/src/jmap/identity/create.rs +++ b/src/jmap/identity/create.rs @@ -1,17 +1,10 @@ -use std::io::{Read, Write}; - use anyhow::{bail, Result}; use clap::Parser; -use io_jmap::rfc8621::{ - identity::IdentityCreate, - identity_set::{JmapIdentitySet, JmapIdentitySetArgs, JmapIdentitySetResult}, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_jmap::rfc8621::{identity::IdentityCreate, identity_set::JmapIdentitySetArgs}; +use pimalaya_cli::printer::{Message, Printer}; use crate::jmap::{account::JmapAccount, error::format_set_error}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Create a JMAP sender identity (Identity/set). #[derive(Debug, Parser)] pub struct JmapIdentityCreateCommand { @@ -32,7 +25,7 @@ pub struct JmapIdentityCreateCommand { impl JmapIdentityCreateCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; let identity = IdentityCreate { name: self.name.clone(), @@ -48,26 +41,9 @@ impl JmapIdentityCreateCommand { let mut args = JmapIdentitySetArgs::default(); args.create(create_id, identity); - let mut coroutine = JmapIdentitySet::new(&jmap.session, &jmap.http_auth, args)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; + let output = client.identity_set(args)?; - let not_created = loop { - match coroutine.resume(arg.take()) { - JmapIdentitySetResult::Ok { not_created, .. } => break not_created, - JmapIdentitySetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapIdentitySetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapIdentitySetResult::Err(err) => bail!("{err}"), - } - }; - - if let Some(err) = not_created.get(create_id) { + if let Some(err) = output.not_created.get(create_id) { let mut msg = format!("Create identity for `{}` error", self.email); msg.push_str(&format_set_error(err)); bail!(msg); diff --git a/src/jmap/identity/delete.rs b/src/jmap/identity/delete.rs index b5c979fe..b8a9ddde 100644 --- a/src/jmap/identity/delete.rs +++ b/src/jmap/identity/delete.rs @@ -1,14 +1,10 @@ -use std::io::{Read, Write}; - use anyhow::{bail, Result}; use clap::Parser; -use io_jmap::rfc8621::identity_set::{JmapIdentitySet, JmapIdentitySetArgs, JmapIdentitySetResult}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_jmap::rfc8621::identity_set::JmapIdentitySetArgs; +use pimalaya_cli::printer::{Message, Printer}; use crate::jmap::{account::JmapAccount, error::format_set_error}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Delete a JMAP sender identity (Identity/set). #[derive(Debug, Parser)] pub struct JmapIdentityDeleteCommand { @@ -19,37 +15,19 @@ pub struct JmapIdentityDeleteCommand { impl JmapIdentityDeleteCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; - + let mut client = account.new_jmap_client()?; let mut args = JmapIdentitySetArgs::default(); for id in self.ids { args.destroy(id); } - let mut coroutine = JmapIdentitySet::new(&jmap.session, &jmap.http_auth, args)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; + let output = client.identity_set(args)?; - let not_destroyed = loop { - match coroutine.resume(arg.take()) { - JmapIdentitySetResult::Ok { not_destroyed, .. } => break not_destroyed, - JmapIdentitySetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapIdentitySetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapIdentitySetResult::Err(err) => bail!("{err}"), - } - }; - - if !not_destroyed.is_empty() { + if !output.not_destroyed.is_empty() { let mut msg = String::from("Destroy JMAP identities error"); - for (id, err) in not_destroyed { + for (id, err) in output.not_destroyed { msg.push_str(&format!("\n `{id}`")); msg.push_str(&format_set_error(&err)); } diff --git a/src/jmap/identity/get.rs b/src/jmap/identity/get.rs index 092e290c..a2afa665 100644 --- a/src/jmap/identity/get.rs +++ b/src/jmap/identity/get.rs @@ -1,23 +1,15 @@ -use std::{ - fmt, - io::{Read, Write}, -}; +use std::fmt; -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; use comfy_table::{Cell, Row, Table}; -use io_jmap::rfc8621::{ - identity::Identity, - identity_get::{JmapIdentityGet, JmapIdentityGetResult}, -}; +use io_jmap::rfc8621::identity::Identity; use log::warn; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::jmap::account::JmapAccount; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Get JMAP identities (Identity/get). /// /// Lists sender identities available for sending email. Pass no IDs to @@ -31,38 +23,16 @@ pub struct JmapIdentityGetCommand { impl JmapIdentityGetCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; + let output = client.identity_get(self.ids)?; - let mut coroutine = JmapIdentityGet::new(&jmap.session, &jmap.http_auth, self.ids)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let (identities, not_found) = loop { - match coroutine.resume(arg.take()) { - JmapIdentityGetResult::Ok { - identities, - not_found, - .. - } => break (identities, not_found), - JmapIdentityGetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapIdentityGetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapIdentityGetResult::Err(err) => bail!("{err}"), - } - }; - - for id in not_found { + for id in output.not_found { warn!("identity `{id}` not found"); } let table = IdentitiesTable { preset: account.table_preset, - identities, + identities: output.identities, }; printer.out(table) diff --git a/src/jmap/identity/mod.rs b/src/jmap/identity/mod.rs index 632abbb5..c82fb41d 100644 --- a/src/jmap/identity/mod.rs +++ b/src/jmap/identity/mod.rs @@ -1,4 +1,4 @@ -pub mod command; +pub mod cli; pub mod create; pub mod delete; pub mod get; diff --git a/src/jmap/identity/update.rs b/src/jmap/identity/update.rs index 6c583659..e8310c16 100644 --- a/src/jmap/identity/update.rs +++ b/src/jmap/identity/update.rs @@ -1,17 +1,10 @@ -use std::io::{Read, Write}; - use anyhow::{bail, Result}; use clap::Parser; -use io_jmap::rfc8621::{ - identity::IdentityUpdate, - identity_set::{JmapIdentitySet, JmapIdentitySetArgs, JmapIdentitySetResult}, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_jmap::rfc8621::{identity::IdentityUpdate, identity_set::JmapIdentitySetArgs}; +use pimalaya_cli::printer::{Message, Printer}; use crate::jmap::{account::JmapAccount, error::format_set_error}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Update a JMAP sender identity (Identity/set). #[derive(Debug, Parser)] pub struct JmapIdentityUpdateCommand { @@ -33,7 +26,7 @@ pub struct JmapIdentityUpdateCommand { impl JmapIdentityUpdateCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; let patch = IdentityUpdate { name: self.name, @@ -46,26 +39,9 @@ impl JmapIdentityUpdateCommand { let mut args = JmapIdentitySetArgs::default(); args.update(self.id.clone(), patch); - let mut coroutine = JmapIdentitySet::new(&jmap.session, &jmap.http_auth, args)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; + let output = client.identity_set(args)?; - let not_updated = loop { - match coroutine.resume(arg.take()) { - JmapIdentitySetResult::Ok { not_updated, .. } => break not_updated, - JmapIdentitySetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapIdentitySetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapIdentitySetResult::Err(err) => bail!("{err}"), - } - }; - - if let Some(err) = not_updated.get(&self.id) { + if let Some(err) = output.not_updated.get(&self.id) { let mut msg = format!("Update identity `{}` error", self.id); msg.push_str(&format_set_error(err)); bail!(msg); diff --git a/src/jmap/mailbox/command.rs b/src/jmap/mailbox/cli.rs similarity index 95% rename from src/jmap/mailbox/command.rs rename to src/jmap/mailbox/cli.rs index b1f94164..14a65067 100644 --- a/src/jmap/mailbox/command.rs +++ b/src/jmap/mailbox/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::jmap::{ account::JmapAccount, diff --git a/src/jmap/mailbox/create.rs b/src/jmap/mailbox/create.rs index 91332104..3aec1fcb 100644 --- a/src/jmap/mailbox/create.rs +++ b/src/jmap/mailbox/create.rs @@ -1,20 +1,12 @@ -use std::{ - collections::BTreeMap, - io::{Read, Write}, -}; +use std::collections::BTreeMap; use anyhow::{bail, Result}; use clap::Parser; -use io_jmap::rfc8621::{ - mailbox::MailboxCreate, - mailbox_set::{JmapMailboxSet, JmapMailboxSetArgs, JmapMailboxSetResult}, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_jmap::rfc8621::{mailbox::MailboxCreate, mailbox_set::JmapMailboxSetArgs}; +use pimalaya_cli::printer::{Message, Printer}; use crate::jmap::{account::JmapAccount, error::format_set_error}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Create a JMAP mailbox. #[derive(Debug, Parser)] pub struct JmapMailboxCreateCommand { @@ -34,7 +26,7 @@ pub struct JmapMailboxCreateCommand { impl JmapMailboxCreateCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; let new_mailbox = MailboxCreate { name: Some(self.name.clone()), @@ -49,26 +41,9 @@ impl JmapMailboxCreateCommand { let mut args = JmapMailboxSetArgs::default(); args.create = Some(create); - let mut coroutine = JmapMailboxSet::new(&jmap.session, &jmap.http_auth, args)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; + let output = client.mailbox_set(args)?; - let not_created = loop { - match coroutine.resume(arg.take()) { - JmapMailboxSetResult::Ok { not_created, .. } => break not_created, - JmapMailboxSetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapMailboxSetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapMailboxSetResult::Err(err) => bail!("{err}"), - } - }; - - if let Some(err) = not_created.get(&self.name) { + if let Some(err) = output.not_created.get(&self.name) { let mut msg = format!("Create JMAP mailbox `{}` error", self.name); msg.push_str(&format_set_error(err)); bail!(msg) diff --git a/src/jmap/mailbox/destroy.rs b/src/jmap/mailbox/destroy.rs index 80e113e1..2100e3c5 100644 --- a/src/jmap/mailbox/destroy.rs +++ b/src/jmap/mailbox/destroy.rs @@ -1,14 +1,10 @@ -use std::io::{Read, Write}; - use anyhow::{bail, Result}; use clap::Parser; -use io_jmap::rfc8621::mailbox_set::{JmapMailboxSet, JmapMailboxSetArgs, JmapMailboxSetResult}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_jmap::rfc8621::mailbox_set::JmapMailboxSetArgs; +use pimalaya_cli::printer::{Message, Printer}; use crate::jmap::{account::JmapAccount, error::format_set_error}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Delete a JMAP mailbox. #[derive(Debug, Parser)] pub struct JmapMailboxDestroyCommand { @@ -23,35 +19,18 @@ pub struct JmapMailboxDestroyCommand { impl JmapMailboxDestroyCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; let mut args = JmapMailboxSetArgs::default(); args.destroy = Some(self.ids.clone()); args.on_destroy_remove_emails = if self.purge { Some(true) } else { None }; - let mut coroutine = JmapMailboxSet::new(&jmap.session, &jmap.http_auth, args)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; + let output = client.mailbox_set(args)?; - let not_destroyed = loop { - match coroutine.resume(arg.take()) { - JmapMailboxSetResult::Ok { not_destroyed, .. } => break not_destroyed, - JmapMailboxSetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapMailboxSetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapMailboxSetResult::Err(err) => bail!("{err}"), - } - }; - - if !not_destroyed.is_empty() { + if !output.not_destroyed.is_empty() { let mut msg = String::from("Destroy JMAP mailbox(es) error"); - for (id, err) in not_destroyed { + for (id, err) in output.not_destroyed { msg.push_str(&format!("\n `{id}`")); msg.push_str(&format_set_error(&err)); } diff --git a/src/jmap/mailbox/get.rs b/src/jmap/mailbox/get.rs index 16392c48..c5fa14e4 100644 --- a/src/jmap/mailbox/get.rs +++ b/src/jmap/mailbox/get.rs @@ -1,15 +1,10 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_jmap::rfc8621::mailbox_get::{JmapMailboxGet, JmapMailboxGetResult}; use log::warn; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::jmap::{account::JmapAccount, mailbox::query::MailboxesTable}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Get JMAP mailboxes by ID (Mailbox/get). #[derive(Debug, Parser)] pub struct JmapMailboxGetCommand { @@ -20,39 +15,16 @@ pub struct JmapMailboxGetCommand { impl JmapMailboxGetCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; + let output = client.mailbox_get(Some(self.ids.clone()), None)?; - let mut coroutine = - JmapMailboxGet::new(&jmap.session, &jmap.http_auth, Some(self.ids.clone()), None)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let (mailboxes, not_found) = loop { - match coroutine.resume(arg.take()) { - JmapMailboxGetResult::Ok { - mailboxes, - not_found, - .. - } => break (mailboxes, not_found), - JmapMailboxGetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapMailboxGetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapMailboxGetResult::Err(err) => bail!("{err}"), - } - }; - - for id in not_found { + for id in output.not_found { warn!("mailbox `{id}` not found, ignoring it"); } let table = MailboxesTable { preset: account.table_preset, - mailboxes, + mailboxes: output.mailboxes, }; printer.out(table) diff --git a/src/jmap/mailbox/mod.rs b/src/jmap/mailbox/mod.rs index 49a87e08..a9b0e3d9 100644 --- a/src/jmap/mailbox/mod.rs +++ b/src/jmap/mailbox/mod.rs @@ -1,4 +1,4 @@ -pub mod command; +pub mod cli; pub mod create; pub mod destroy; pub mod get; diff --git a/src/jmap/mailbox/query.rs b/src/jmap/mailbox/query.rs index 620c8506..71185b77 100644 --- a/src/jmap/mailbox/query.rs +++ b/src/jmap/mailbox/query.rs @@ -1,24 +1,16 @@ -use std::{ - convert::Infallible, - fmt, - io::{Read, Write}, - str::FromStr, -}; +use std::{convert::Infallible, fmt, str::FromStr}; -use anyhow::{bail, Result}; +use anyhow::Result; use clap::{Parser, ValueEnum}; use comfy_table::{Cell, Row, Table}; -use io_jmap::rfc8621::{ - mailbox::{Mailbox, MailboxFilter, MailboxRole, MailboxSortComparator, MailboxSortProperty}, - mailbox_query::{JmapMailboxQuery, JmapMailboxQueryResult}, +use io_jmap::rfc8621::mailbox::{ + Mailbox, MailboxFilter, MailboxRole, MailboxSortComparator, MailboxSortProperty, }; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::jmap::account::JmapAccount; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Query JMAP mailboxes (Mailbox/query + Mailbox/get). /// /// Lists, filters and sorts mailboxes. @@ -64,7 +56,7 @@ pub struct JmapMailboxQueryCommand { impl JmapMailboxQueryCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; let filter = { let f = MailboxFilter { @@ -93,36 +85,17 @@ impl JmapMailboxQueryCommand { is_ascending: Some(!self.desc), }]); - let mut coroutine = JmapMailboxQuery::new( - &jmap.session, - &jmap.http_auth, + let output = client.mailbox_query( filter, sort, Some(self.page.saturating_sub(1) * self.page_size), Some(self.page_size), None, )?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let mailboxes = loop { - match coroutine.resume(arg.take()) { - JmapMailboxQueryResult::Ok { mailboxes, .. } => break mailboxes, - JmapMailboxQueryResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapMailboxQueryResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapMailboxQueryResult::Err(err) => bail!("{err}"), - } - }; let table = MailboxesTable { preset: account.table_preset, - mailboxes, + mailboxes: output.mailboxes, }; printer.out(table) diff --git a/src/jmap/mailbox/update.rs b/src/jmap/mailbox/update.rs index 8fa40a7b..784958c6 100644 --- a/src/jmap/mailbox/update.rs +++ b/src/jmap/mailbox/update.rs @@ -1,20 +1,12 @@ -use std::{ - collections::BTreeMap, - io::{Read, Write}, -}; +use std::collections::BTreeMap; use anyhow::{bail, Result}; use clap::Parser; -use io_jmap::rfc8621::{ - mailbox::MailboxUpdate, - mailbox_set::{JmapMailboxSet, JmapMailboxSetArgs, JmapMailboxSetResult}, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_jmap::rfc8621::{mailbox::MailboxUpdate, mailbox_set::JmapMailboxSetArgs}; +use pimalaya_cli::printer::{Message, Printer}; use crate::jmap::{account::JmapAccount, error::format_set_error, mailbox::query::RoleArg}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Update a JMAP mailbox. #[derive(Debug, Parser)] pub struct JmapMailboxUpdateCommand { @@ -49,7 +41,7 @@ pub struct JmapMailboxUpdateCommand { impl JmapMailboxUpdateCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; let is_subscribed = if self.subscribe { Some(true) @@ -73,26 +65,9 @@ impl JmapMailboxUpdateCommand { let mut args = JmapMailboxSetArgs::default(); args.update = Some(update); - let mut coroutine = JmapMailboxSet::new(&jmap.session, &jmap.http_auth, args)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; + let output = client.mailbox_set(args)?; - let not_updated = loop { - match coroutine.resume(arg.take()) { - JmapMailboxSetResult::Ok { not_updated, .. } => break not_updated, - JmapMailboxSetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapMailboxSetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapMailboxSetResult::Err(err) => bail!("{err}"), - } - }; - - if let Some(err) = not_updated.get(&self.id) { + if let Some(err) = output.not_updated.get(&self.id) { let mut msg = format!("Update JMAP mailbox `{}` error", self.id); msg.push_str(&format_set_error(err)); bail!(msg); diff --git a/src/jmap/mod.rs b/src/jmap/mod.rs index 0eed486b..c1628be7 100644 --- a/src/jmap/mod.rs +++ b/src/jmap/mod.rs @@ -1,10 +1,11 @@ pub mod account; -pub mod command; +pub mod cli; pub mod email; pub mod error; pub mod identity; pub mod mailbox; pub mod query; +pub mod session; pub mod submission; pub mod thread; pub mod vacation; diff --git a/src/jmap/query.rs b/src/jmap/query.rs index 9c87449c..f35c4bb4 100644 --- a/src/jmap/query.rs +++ b/src/jmap/query.rs @@ -1,25 +1,20 @@ use std::{ fmt, - io::{stdin, BufRead, Read, Write}, + io::{stdin, BufRead}, }; use anyhow::{bail, Context, Result}; use clap::Parser; use io_jmap::{ - rfc8620::{ - send::{JmapRequest, JmapSend, JmapSendResult}, - session::capabilities::CORE, - }, + rfc8620::{send::JmapRequest, session::capabilities::CORE}, rfc8621::capabilities::MAIL, }; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use serde_json::Value; use crate::jmap::account::JmapAccount; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Send a raw JMAP method-calls array and print the response. /// /// METHOD_CALLS must be a JSON array of JMAP method call tuples: @@ -43,7 +38,7 @@ pub struct JmapQueryCommand { impl JmapQueryCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; let raw = if self.method_calls.is_empty() || self.method_calls.first().map(|s| s.as_str()) == Some("-") @@ -65,14 +60,11 @@ impl JmapQueryCommand { bail!("METHOD_CALLS must be a JSON array"); }; - let account_id = jmap - .session - .primary_accounts - .get(MAIL) - .cloned() + let account_id = client + .session() + .and_then(|s| s.primary_accounts.get(MAIL).cloned()) .unwrap_or_default(); - // Parse and inject accountId into each call's args. let mut method_calls = Vec::with_capacity(calls_arr.len()); for (i, call) in calls_arr.into_iter().enumerate() { let Value::Array(mut tuple) = call else { @@ -94,7 +86,6 @@ impl JmapQueryCommand { v => bail!("method call #{i} name must be a string, got {v}"), }; - // Inject accountId if the args object doesn't already have it. if let Value::Object(ref mut map) = args { map.entry("accountId") .or_insert_with(|| Value::String(account_id.clone())); @@ -116,24 +107,7 @@ impl JmapQueryCommand { created_ids: None, }; - let mut coroutine = JmapSend::new(&jmap.http_auth, &jmap.session.api_url, request)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let response = loop { - match coroutine.resume(arg.take()) { - JmapSendResult::Ok { response, .. } => break response, - JmapSendResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapSendResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapSendResult::Err(err) => return Err(err.into()), - } - }; + let response = client.send_raw(request)?; printer.out(RawResponse { method_responses: response.method_responses, diff --git a/src/jmap/session.rs b/src/jmap/session.rs new file mode 100644 index 00000000..615bd465 --- /dev/null +++ b/src/jmap/session.rs @@ -0,0 +1,142 @@ +//! Transitional JMAP session helper ported from `pimalaya-toolbox`. +//! +//! Will be replaced by `io_jmap::client::JmapClient` once the +//! protocol-specific subcommands switch over. + +use std::{ + io::{Read, Write}, + net::TcpStream, +}; + +use anyhow::{bail, Result}; +use base64::{prelude::BASE64_STANDARD, Engine}; +use io_jmap::rfc8620::{ + session::JmapSession as IoJmapSession, + session_get::{JmapSessionGet, JmapSessionGetResult}, +}; +use log::info; +use pimalaya_stream::{ + std::stream::Stream, + tls::{upgrade_tls, Tls}, +}; +use secrecy::{ExposeSecret, SecretString}; +use url::Url; + +const READ_BUFFER_SIZE: usize = 16 * 1024; + +/// Authentication for a JMAP session. +// https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes +#[derive(Clone, Debug)] +pub enum JmapAuth { + Header(SecretString), + /// Bearer token (OAuth 2.0). + Bearer(SecretString), + /// HTTP Basic authentication. + Basic { + username: String, + password: SecretString, + }, +} + +impl From for SecretString { + fn from(auth: JmapAuth) -> SecretString { + match auth { + JmapAuth::Header(auth) => auth, + JmapAuth::Bearer(token) => { + let token = token.expose_secret(); + format!("Bearer {token}").into() + } + JmapAuth::Basic { username, password } => { + let creds = format!("{}:{}", username, password.expose_secret()); + let creds = BASE64_STANDARD.encode(creds.into_bytes()); + format!("Basic {creds}").into() + } + } + } +} + +/// A live JMAP session over a TLS connection. +#[derive(Debug)] +pub struct JmapSession { + pub session: IoJmapSession, + pub stream: Stream, + pub http_auth: SecretString, +} + +fn use_tls(scheme: &str) -> bool { + scheme.eq_ignore_ascii_case("https") || scheme.eq_ignore_ascii_case("jmaps") +} + +fn default_port(scheme: &str) -> u16 { + if use_tls(scheme) { + 443 + } else { + 80 + } +} + +fn connect(url: &Url, tls: &Tls) -> Result { + let host = url.host_str().unwrap_or("localhost"); + let port = url.port().unwrap_or_else(|| default_port(url.scheme())); + let tcp = TcpStream::connect((host, port))?; + + if use_tls(url.scheme()) { + upgrade_tls(host, tcp, tls, &[b"http/1.1"]) + } else { + Ok(Stream::Tcp(tcp)) + } +} + +impl JmapSession { + /// Establishes a JMAP session. + pub fn new(server: String, tls: Tls, auth: JmapAuth) -> Result { + let url = match Url::parse(&server) { + Ok(url) => url, + Err(url::ParseError::RelativeUrlWithoutBase) => { + Url::parse(&format!("https://{server}"))? + } + Err(e) => return Err(e.into()), + }; + + info!("connecting to JMAP server {url}"); + + match url.scheme() { + s if s.eq_ignore_ascii_case("https") || s.eq_ignore_ascii_case("jmaps") => {} + s if s.eq_ignore_ascii_case("http") || s.eq_ignore_ascii_case("jmap") => {} + scheme => bail!("unsupported JMAP scheme `{scheme}`, expected http/https/jmap/jmaps"), + } + + let mut stream = connect(&url, &tls)?; + + let http_auth: SecretString = auth.into(); + let mut coroutine = JmapSessionGet::new(&http_auth, &url); + let mut buf = [0u8; READ_BUFFER_SIZE]; + let mut arg: Option<&[u8]> = None; + + let session = loop { + match coroutine.resume(arg.take()) { + JmapSessionGetResult::Ok { session, .. } => break session, + JmapSessionGetResult::WantsRead => { + let n = stream.read(&mut buf)?; + arg = Some(&buf[..n]); + } + JmapSessionGetResult::WantsWrite(bytes) => { + stream.write_all(&bytes)?; + arg = None; + } + JmapSessionGetResult::WantsRedirect { url: new_url, .. } => { + stream = connect(&new_url, &tls)?; + coroutine = JmapSessionGet::new(&http_auth, &new_url); + arg = None; + } + JmapSessionGetResult::Err(err) => return Err(err.into()), + } + }; + + Ok(Self { + session, + stream, + http_auth, + }) + } +} diff --git a/src/jmap/submission/cancel.rs b/src/jmap/submission/cancel.rs index d018a5cf..3b342c1a 100644 --- a/src/jmap/submission/cancel.rs +++ b/src/jmap/submission/cancel.rs @@ -1,16 +1,9 @@ -use std::io::{Read, Write}; - -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use clap::Parser; -use io_jmap::rfc8621::email_submission_cancel::{ - JmapEmailSubmissionCancel, JmapEmailSubmissionCancelResult, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::jmap::{account::JmapAccount, error::format_set_error}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Cancel (undo) a pending JMAP email submission (EmailSubmission/set). /// /// Only submissions with `undoStatus: "pending"` can be canceled. @@ -24,33 +17,13 @@ pub struct JmapSubmissionCancelCommand { impl JmapSubmissionCancelCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; + let output = client.email_submission_cancel(self.ids.clone())?; - let mut coroutine = - JmapEmailSubmissionCancel::new(&jmap.session, &jmap.http_auth, self.ids.clone()) - .map_err(|e| anyhow!("{e}"))?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let not_updated = loop { - match coroutine.resume(arg.take()) { - JmapEmailSubmissionCancelResult::Ok { not_updated, .. } => break not_updated, - JmapEmailSubmissionCancelResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapEmailSubmissionCancelResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapEmailSubmissionCancelResult::Err(err) => bail!("{err}"), - } - }; - - if !not_updated.is_empty() { + if !output.not_updated.is_empty() { let mut msg = String::from("Cancel submission(s) error"); - for (id, err) in ¬_updated { + for (id, err) in &output.not_updated { msg.push_str(&format!("\n `{id}`")); msg.push_str(&format_set_error(err)); } diff --git a/src/jmap/submission/command.rs b/src/jmap/submission/cli.rs similarity index 96% rename from src/jmap/submission/command.rs rename to src/jmap/submission/cli.rs index ae208c93..3be78281 100644 --- a/src/jmap/submission/command.rs +++ b/src/jmap/submission/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::jmap::{ account::JmapAccount, diff --git a/src/jmap/submission/create.rs b/src/jmap/submission/create.rs index 69bc4f08..8cf34395 100644 --- a/src/jmap/submission/create.rs +++ b/src/jmap/submission/create.rs @@ -1,22 +1,16 @@ -use std::{ - collections::BTreeMap, - io::{Read, Write}, -}; +use std::collections::BTreeMap; use anyhow::{bail, Result}; use clap::Parser; -use io_jmap::rfc8621::{ - email_submission::{EmailAddressWithParameters, EmailSubmissionCreate, Envelope}, - email_submission_set::{JmapEmailSubmissionSet, JmapEmailSubmissionSetResult}, +use io_jmap::rfc8621::email_submission::{ + EmailAddressWithParameters, EmailSubmissionCreate, Envelope, }; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::jmap::{ account::JmapAccount, error::format_set_error, submission::query::SubmissionsTable, }; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Submit a JMAP email for sending (EmailSubmission/set). /// /// The email must already exist as a draft in the JMAP account. @@ -42,7 +36,7 @@ pub struct JmapSubmissionCreateCommand { impl JmapSubmissionCreateCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; let envelope = if let Some(mail_from_addr) = self.mail_from { let rcpt_to = self @@ -73,31 +67,9 @@ impl JmapSubmissionCreateCommand { let mut submissions = BTreeMap::new(); submissions.insert(self.email_id.clone(), submission); - let mut coroutine = - JmapEmailSubmissionSet::new(&jmap.session, &jmap.http_auth, submissions)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; + let output = client.email_submission_set(submissions)?; - let (created, not_created) = loop { - match coroutine.resume(arg.take()) { - JmapEmailSubmissionSetResult::Ok { - created, - not_created, - .. - } => break (created, not_created), - JmapEmailSubmissionSetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapEmailSubmissionSetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapEmailSubmissionSetResult::Err(err) => bail!("{err}"), - } - }; - - if let Some(err) = not_created.get(&self.email_id) { + if let Some(err) = output.not_created.get(&self.email_id) { let mut msg = format!("Send email `{}` error", self.email_id); msg.push_str(&format_set_error(err)); bail!(msg); @@ -105,7 +77,7 @@ impl JmapSubmissionCreateCommand { let table = SubmissionsTable { preset: account.table_preset, - submissions: created.into_values().collect(), + submissions: output.created.into_values().collect(), }; printer.out(table) diff --git a/src/jmap/submission/get.rs b/src/jmap/submission/get.rs index d7619141..73cb7e04 100644 --- a/src/jmap/submission/get.rs +++ b/src/jmap/submission/get.rs @@ -1,17 +1,10 @@ -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_jmap::rfc8621::email_submission_get::{ - JmapEmailSubmissionGet, JmapEmailSubmissionGetResult, -}; use log::warn; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::jmap::{account::JmapAccount, submission::query::SubmissionsTable}; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Get JMAP email submissions by ID (EmailSubmission/get). #[derive(Debug, Parser)] pub struct JmapSubmissionGetCommand { @@ -22,39 +15,16 @@ pub struct JmapSubmissionGetCommand { impl JmapSubmissionGetCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; + let output = client.email_submission_get(Some(self.ids.clone()))?; - let mut coroutine = - JmapEmailSubmissionGet::new(&jmap.session, &jmap.http_auth, Some(self.ids.clone()))?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let (submissions, not_found) = loop { - match coroutine.resume(arg.take()) { - JmapEmailSubmissionGetResult::Ok { - submissions, - not_found, - .. - } => break (submissions, not_found), - JmapEmailSubmissionGetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapEmailSubmissionGetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapEmailSubmissionGetResult::Err(err) => bail!("{err}"), - } - }; - - for id in not_found { + for id in output.not_found { warn!("submission `{id}` not found, ignoring it"); } let table = SubmissionsTable { preset: account.table_preset, - submissions, + submissions: output.submissions, }; printer.out(table) diff --git a/src/jmap/submission/mod.rs b/src/jmap/submission/mod.rs index ad776e78..7deef411 100644 --- a/src/jmap/submission/mod.rs +++ b/src/jmap/submission/mod.rs @@ -1,5 +1,5 @@ pub mod cancel; -pub mod command; +pub mod cli; pub mod create; pub mod get; pub mod query; diff --git a/src/jmap/submission/query.rs b/src/jmap/submission/query.rs index 7f19f1bf..90fa7acf 100644 --- a/src/jmap/submission/query.rs +++ b/src/jmap/submission/query.rs @@ -1,22 +1,14 @@ -use std::{ - fmt, - io::{Read, Write}, -}; +use std::fmt; -use anyhow::{bail, Result}; +use anyhow::Result; use clap::{Parser, ValueEnum}; use comfy_table::{Cell, Row, Table}; -use io_jmap::rfc8621::{ - email_submission::{EmailSubmission, EmailSubmissionFilter, UndoStatus}, - email_submission_query::{JmapEmailSubmissionQuery, JmapEmailSubmissionQueryResult}, -}; -use pimalaya_toolbox::terminal::printer::Printer; +use io_jmap::rfc8621::email_submission::{EmailSubmission, EmailSubmissionFilter, UndoStatus}; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::jmap::account::JmapAccount; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// CLI proxy for [`UndoStatus`]. #[derive(Clone, Debug, ValueEnum)] pub enum UndoStatusArg { @@ -61,7 +53,7 @@ pub struct JmapSubmissionQueryCommand { impl JmapSubmissionQueryCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; let filter = { let f = EmailSubmissionFilter { @@ -80,35 +72,16 @@ impl JmapSubmissionQueryCommand { } }; - let mut coroutine = JmapEmailSubmissionQuery::new( - &jmap.session, - &jmap.http_auth, + let output = client.email_submission_query( filter, None, Some(self.page.saturating_sub(1) * self.page_size), Some(self.page_size), )?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let submissions = loop { - match coroutine.resume(arg.take()) { - JmapEmailSubmissionQueryResult::Ok { submissions, .. } => break submissions, - JmapEmailSubmissionQueryResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapEmailSubmissionQueryResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapEmailSubmissionQueryResult::Err(err) => bail!("{err}"), - } - }; let table = SubmissionsTable { preset: account.table_preset, - submissions, + submissions: output.submissions, }; printer.out(table) diff --git a/src/jmap/thread/command.rs b/src/jmap/thread/cli.rs similarity index 90% rename from src/jmap/thread/command.rs rename to src/jmap/thread/cli.rs index 5000812b..92a35baa 100644 --- a/src/jmap/thread/command.rs +++ b/src/jmap/thread/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::jmap::{account::JmapAccount, thread::get::JmapThreadGetCommand}; diff --git a/src/jmap/thread/get.rs b/src/jmap/thread/get.rs index 0f9ad07c..188099be 100644 --- a/src/jmap/thread/get.rs +++ b/src/jmap/thread/get.rs @@ -1,23 +1,15 @@ -use std::{ - fmt, - io::{Read, Write}, -}; +use std::fmt; -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; use comfy_table::{Cell, Row, Table}; -use io_jmap::rfc8621::{ - thread::Thread, - thread_get::{JmapThreadGet, JmapThreadGetResult}, -}; +use io_jmap::rfc8621::thread::Thread; use log::warn; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::jmap::account::JmapAccount; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Get JMAP threads by ID (Thread/get). /// /// Each thread contains an ordered list of email IDs in the thread. @@ -30,36 +22,16 @@ pub struct JmapThreadGetCommand { impl JmapThreadGetCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; + let output = client.thread_get(self.ids.clone())?; - let mut coroutine = JmapThreadGet::new(&jmap.session, &jmap.http_auth, self.ids.clone())?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let (threads, not_found) = loop { - match coroutine.resume(arg.take()) { - JmapThreadGetResult::Ok { - threads, not_found, .. - } => break (threads, not_found), - JmapThreadGetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapThreadGetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapThreadGetResult::Err(err) => bail!("{err}"), - } - }; - - for id in not_found { + for id in output.not_found { warn!("thread `{id}` not found, ignoring it"); } printer.out(ThreadsTable { preset: account.table_preset, - threads, + threads: output.threads, }) } } diff --git a/src/jmap/thread/mod.rs b/src/jmap/thread/mod.rs index 60680179..e15dd984 100644 --- a/src/jmap/thread/mod.rs +++ b/src/jmap/thread/mod.rs @@ -1,2 +1,2 @@ -pub mod command; +pub mod cli; pub mod get; diff --git a/src/jmap/vacation/command.rs b/src/jmap/vacation/cli.rs similarity index 93% rename from src/jmap/vacation/command.rs rename to src/jmap/vacation/cli.rs index 6326e87a..6b1881ed 100644 --- a/src/jmap/vacation/command.rs +++ b/src/jmap/vacation/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::jmap::{ account::JmapAccount, diff --git a/src/jmap/vacation/get.rs b/src/jmap/vacation/get.rs index 27fd04af..370eecc6 100644 --- a/src/jmap/vacation/get.rs +++ b/src/jmap/vacation/get.rs @@ -1,61 +1,32 @@ -use std::{ - fmt, - io::{Read, Write}, -}; +use std::fmt; use anyhow::{bail, Result}; use clap::Parser; use comfy_table::{Cell, Row, Table}; -use io_jmap::rfc8621::{ - capabilities::VACATION_RESPONSE, - vacation_response::VacationResponse, - vacation_response_get::{JmapVacationResponseGet, JmapVacationResponseGetResult}, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_jmap::rfc8621::{capabilities::VACATION_RESPONSE, vacation_response::VacationResponse}; +use pimalaya_cli::printer::{Message, Printer}; use serde::Serialize; use crate::jmap::account::JmapAccount; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Get the JMAP vacation response (VacationResponse/get). #[derive(Debug, Parser)] pub struct JmapVacationGetCommand; impl JmapVacationGetCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; - // Skip the request if the server does not advertise the - // vacation-response capability. - let has_vacation = jmap.session.capabilities.contains_key(VACATION_RESPONSE); + let has_vacation = client + .session() + .map(|s| s.capabilities.contains_key(VACATION_RESPONSE)) + .unwrap_or(false); if !has_vacation { bail!("Vacation response is not supported by the server"); } - let mut coroutine = JmapVacationResponseGet::new(&jmap.session, &jmap.http_auth)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let vacation = loop { - match coroutine.resume(arg.take()) { - JmapVacationResponseGetResult::Ok { - vacation_response, .. - } => break vacation_response, - JmapVacationResponseGetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapVacationResponseGetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapVacationResponseGetResult::Err(err) => bail!("{err}"), - } - }; - - let Some(vacation) = vacation else { + let Some(vacation) = client.vacation_response_get()? else { return printer.out(Message::new("No vacation response configured")); }; diff --git a/src/jmap/vacation/mod.rs b/src/jmap/vacation/mod.rs index a6c8df6f..f802e40a 100644 --- a/src/jmap/vacation/mod.rs +++ b/src/jmap/vacation/mod.rs @@ -1,3 +1,3 @@ -pub mod command; +pub mod cli; pub mod get; pub mod set; diff --git a/src/jmap/vacation/set.rs b/src/jmap/vacation/set.rs index 7d1e0d45..bd35b689 100644 --- a/src/jmap/vacation/set.rs +++ b/src/jmap/vacation/set.rs @@ -1,18 +1,12 @@ -use std::io::{Read, Write}; - use anyhow::{bail, Result}; use clap::Parser; use io_jmap::rfc8621::{ - capabilities::VACATION_RESPONSE, - vacation_response::VacationResponseUpdate, - vacation_response_set::{JmapVacationResponseSet, JmapVacationResponseSetResult}, + capabilities::VACATION_RESPONSE, vacation_response::VacationResponseUpdate, }; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::jmap::account::JmapAccount; -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Update the JMAP vacation response (VacationResponse/set). #[derive(Debug, Parser)] pub struct JmapVacationSetCommand { @@ -47,11 +41,12 @@ pub struct JmapVacationSetCommand { impl JmapVacationSetCommand { pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> { - let mut jmap = account.new_jmap_session()?; + let mut client = account.new_jmap_client()?; - // Skip the request if the server does not advertise the - // vacation-response capability. - let has_vacation = jmap.session.capabilities.contains_key(VACATION_RESPONSE); + let has_vacation = client + .session() + .map(|s| s.capabilities.contains_key(VACATION_RESPONSE)) + .unwrap_or(false); if !has_vacation { bail!("Vacation response is not supported by the server"); @@ -74,24 +69,7 @@ impl JmapVacationSetCommand { html_body: self.html_body, }; - let mut coroutine = JmapVacationResponseSet::new(&jmap.session, &jmap.http_auth, patch)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - JmapVacationResponseSetResult::Ok { .. } => break, - JmapVacationResponseSetResult::WantsRead => { - let n = jmap.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - JmapVacationResponseSetResult::WantsWrite(bytes) => { - jmap.stream.write_all(&bytes)?; - arg = None; - } - JmapVacationResponseSetResult::Err(err) => bail!("{err}"), - } - } + client.vacation_response_set(patch)?; printer.out(Message::new("Vacation response successfully updated")) } diff --git a/src/mailboxes/command.rs b/src/mailboxes/cli.rs similarity index 94% rename from src/mailboxes/command.rs rename to src/mailboxes/cli.rs index 3b76e49c..ad7dcd48 100644 --- a/src/mailboxes/command.rs +++ b/src/mailboxes/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::{ cli::BackendArg, diff --git a/src/mailboxes/list.rs b/src/mailboxes/list.rs index 25eb0ce4..a4ff5d9e 100644 --- a/src/mailboxes/list.rs +++ b/src/mailboxes/list.rs @@ -1,22 +1,14 @@ -#[cfg(feature = "maildir")] -use std::collections::{BTreeMap, BTreeSet}; -#[cfg(any(feature = "imap", feature = "jmap"))] -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::{ - account::Account, cli::BackendArg, config::{AccountConfig, Config}, + email_client::build, mailboxes::table::MailboxesTable, }; -#[cfg(any(feature = "imap", feature = "jmap"))] -const READ_BUFFER_SIZE: usize = 8 * 1024; - /// List mailboxes for the active account, regardless of the underlying /// backend (IMAP, JMAP or Maildir). #[derive(Debug, Parser)] @@ -27,138 +19,16 @@ impl MailboxesListCommand { self, printer: &mut impl Printer, config: Config, - mut account_config: AccountConfig, + account_config: AccountConfig, backend: BackendArg, ) -> Result<()> { - #[cfg(feature = "imap")] - if backend.allows_imap() { - if let Some(imap_config) = account_config.imap.take() { - use io_email::imap::mailbox_list::{MailboxList, MailboxListResult}; - use pimalaya_toolbox::stream::imap::ImapSession; + let mut ctx = build(config, account_config, backend)?; + let mailboxes = ctx.client.list_mailboxes()?; - let account = Account::new(config, account_config, imap_config)?; - let mut session = ImapSession::new( - account.backend.url.clone(), - account.backend.tls.clone().try_into()?, - account.backend.starttls, - account.backend.sasl.clone().try_into()?, - )?; - let mut coroutine = MailboxList::new(session.context); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let mailboxes = loop { - match coroutine.resume(arg.take()) { - MailboxListResult::Ok(mailboxes) => break mailboxes, - MailboxListResult::WantsRead => { - let n = session.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - MailboxListResult::WantsWrite(bytes) => { - session.stream.write_all(&bytes)?; - } - MailboxListResult::Err(err) => bail!("{err}"), - } - }; - - return printer.out(MailboxesTable { - preset: account.table_preset, - arrangement: account.table_arrangement, - mailboxes, - }); - } - } - - #[cfg(feature = "jmap")] - if backend.allows_jmap() { - if let Some(jmap_config) = account_config.jmap.take() { - use io_email::jmap::mailbox_list::{MailboxList, MailboxListResult}; - use pimalaya_toolbox::stream::jmap::JmapSession; - - let account = Account::new(config, account_config, jmap_config)?; - let mut session = JmapSession::new( - account.backend.server.clone(), - account.backend.tls.clone().try_into()?, - account.backend.auth.clone().try_into()?, - )?; - let mut coroutine = MailboxList::new(&session.session, &session.http_auth)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let mailboxes = loop { - match coroutine.resume(arg.take()) { - MailboxListResult::Ok(mailboxes) => break mailboxes, - MailboxListResult::WantsRead => { - let n = session.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - MailboxListResult::WantsWrite(bytes) => { - session.stream.write_all(&bytes)?; - } - MailboxListResult::Err(err) => bail!("{err}"), - } - }; - - return printer.out(MailboxesTable { - preset: account.table_preset, - arrangement: account.table_arrangement, - mailboxes, - }); - } - } - - #[cfg(feature = "maildir")] - if backend.allows_maildir() { - if let Some(maildir_config) = account_config.maildir.take() { - use io_email::maildir::mailbox_list::{ - MailboxList, MailboxListArg, MailboxListResult, - }; - - let account = Account::new(config, account_config, maildir_config)?; - let mut coroutine = MailboxList::new(&account.backend.root); - let mut arg: Option = None; - - let mailboxes = loop { - match coroutine.resume(arg.take()) { - MailboxListResult::Ok(mailboxes) => break mailboxes, - MailboxListResult::WantsDirRead(paths) => { - arg = Some(MailboxListArg::DirRead(read_dirs(&paths)?)); - } - MailboxListResult::Err(err) => bail!("{err}"), - } - }; - - return printer.out(MailboxesTable { - preset: account.table_preset, - arrangement: account.table_arrangement, - mailboxes, - }); - } - } - - bail!("no backend matching `{backend}` is configured for this account") + printer.out(MailboxesTable { + preset: ctx.table_preset, + arrangement: ctx.table_arrangement, + mailboxes, + }) } } - -#[cfg(feature = "maildir")] -fn read_dirs(paths: &BTreeSet) -> Result>> { - use std::fs; - - let mut out = BTreeMap::new(); - - for path in paths { - let mut entries = BTreeSet::new(); - - for entry in fs::read_dir(path)? { - let entry = entry?; - - if let Some(s) = entry.path().to_str() { - entries.insert(s.to_owned()); - } - } - - out.insert(path.clone(), entries); - } - - Ok(out) -} diff --git a/src/mailboxes/mod.rs b/src/mailboxes/mod.rs index 8ca0c9cd..0cf8a1a4 100644 --- a/src/mailboxes/mod.rs +++ b/src/mailboxes/mod.rs @@ -1,3 +1,3 @@ -pub mod command; +pub mod cli; pub mod list; pub mod table; diff --git a/src/maildir/account.rs b/src/maildir/account.rs index 0d8ec5ec..3c86937c 100644 --- a/src/maildir/account.rs +++ b/src/maildir/account.rs @@ -1,3 +1,13 @@ +use io_maildir::client::MaildirClient; + use crate::{account::Account, config::MaildirConfig}; pub type MaildirAccount = Account; + +impl MaildirAccount { + /// Builds a [`MaildirClient`] rooted at the configured Maildir + /// path. + pub fn new_maildir_client(&self) -> MaildirClient { + MaildirClient::new(self.backend.root.clone()) + } +} diff --git a/src/maildir/cli.rs b/src/maildir/cli.rs new file mode 100644 index 00000000..d17ae003 --- /dev/null +++ b/src/maildir/cli.rs @@ -0,0 +1,46 @@ +use anyhow::Result; +use clap::Subcommand; +use pimalaya_cli::printer::Printer; + +use crate::maildir::{ + account::MaildirAccount, create::MaildirMailboxCreateCommand, + delete::MaildirMailboxDeleteCommand, envelope::cli::MaildirEnvelopeCommand, + flag::cli::MaildirFlagCommand, list::MaildirMailboxListCommand, + message::cli::MaildirMessageCommand, rename::MaildirMailboxRenameCommand, +}; + +/// MAILDIR CLI (requires the `maildir` cargo feature). +/// +/// This command gives you access to the MAILDIR CLI API, and allows you +/// to manage MAILDIR mailboxes, envelopes, flags, messages etc. +#[derive(Debug, Subcommand)] +#[command(rename_all = "kebab-case")] +pub enum MaildirCommand { + Create(MaildirMailboxCreateCommand), + Rename(MaildirMailboxRenameCommand), + Delete(MaildirMailboxDeleteCommand), + List(MaildirMailboxListCommand), + + #[command(subcommand)] + #[command(aliases = ["msgs", "msg"])] + Messages(MaildirMessageCommand), + #[command(subcommand)] + Flags(MaildirFlagCommand), + #[command(subcommand)] + Envelopes(MaildirEnvelopeCommand), +} + +impl MaildirCommand { + pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { + match self { + Self::Create(cmd) => cmd.execute(printer, account), + Self::Rename(cmd) => cmd.execute(printer, account), + Self::Delete(cmd) => cmd.execute(printer, account), + Self::List(cmd) => cmd.execute(printer, account), + + Self::Messages(cmd) => cmd.execute(printer, account), + Self::Flags(cmd) => cmd.execute(printer, account), + Self::Envelopes(cmd) => cmd.execute(printer, account), + } + } +} diff --git a/src/maildir/command.rs b/src/maildir/command.rs index 66e9ee89..b35e07cd 100644 --- a/src/maildir/command.rs +++ b/src/maildir/command.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::maildir::{ account::MaildirAccount, create::MaildirMailboxCreateCommand, diff --git a/src/maildir/create.rs b/src/maildir/create.rs index 96df03a1..2eb944cd 100644 --- a/src/maildir/create.rs +++ b/src/maildir/create.rs @@ -1,11 +1,8 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_maildir::coroutines::maildir_create::{ - MaildirCreate, MaildirCreateArg, MaildirCreateResult, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; -use crate::maildir::{account::MaildirAccount, arg::MaildirNameArg, runtime}; +use crate::maildir::{account::MaildirAccount, arg::MaildirNameArg}; /// Create the given mailbox. /// @@ -19,22 +16,9 @@ pub struct MaildirMailboxCreateCommand { impl MaildirMailboxCreateCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { - let path = account.backend.root.join(self.maildir_name.inner); - - let mut coroutine = MaildirCreate::new(path); - let mut arg = None; - - loop { - match coroutine.resume(arg.take()) { - MaildirCreateResult::Ok => break, - MaildirCreateResult::WantsDirCreate(paths) => { - runtime::dir_create(paths)?; - arg = Some(MaildirCreateArg::DirCreate); - } - MaildirCreateResult::Err(err) => bail!("{err}"), - } - } - + let path = account.backend.root.join(&self.maildir_name.inner); + let client = account.new_maildir_client(); + client.create_maildir(path)?; printer.out(Message::new("Maildir successfully created")) } } diff --git a/src/maildir/delete.rs b/src/maildir/delete.rs index a35fa614..23903349 100644 --- a/src/maildir/delete.rs +++ b/src/maildir/delete.rs @@ -1,11 +1,8 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_maildir::coroutines::maildir_delete::{ - MaildirDelete, MaildirDeleteArg, MaildirDeleteResult, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; -use crate::maildir::{account::MaildirAccount, arg::MaildirPathFlag, runtime}; +use crate::maildir::{account::MaildirAccount, arg::MaildirPathFlag}; /// Delete the given mailbox. /// @@ -19,22 +16,9 @@ pub struct MaildirMailboxDeleteCommand { impl MaildirMailboxDeleteCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { - let path = account.backend.root.join(self.maildir_path.inner); - - let mut coroutine = MaildirDelete::new(path); - let mut arg = None; - - loop { - match coroutine.resume(arg.take()) { - MaildirDeleteResult::Ok => break, - MaildirDeleteResult::WantsDirRemove(paths) => { - runtime::dir_remove(paths)?; - arg = Some(MaildirDeleteArg::DirRemove); - } - MaildirDeleteResult::Err(err) => bail!("{err}"), - } - } - + let path = account.backend.root.join(&self.maildir_path.inner); + let client = account.new_maildir_client(); + client.delete_maildir(path)?; printer.out(Message::new("Maildir successfully deleted")) } } diff --git a/src/maildir/envelope/command.rs b/src/maildir/envelope/cli.rs similarity index 94% rename from src/maildir/envelope/command.rs rename to src/maildir/envelope/cli.rs index 2839396a..c82b2eab 100644 --- a/src/maildir/envelope/command.rs +++ b/src/maildir/envelope/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::maildir::{ account::MaildirAccount, diff --git a/src/maildir/envelope/get.rs b/src/maildir/envelope/get.rs index fe194404..4ce9d8e5 100644 --- a/src/maildir/envelope/get.rs +++ b/src/maildir/envelope/get.rs @@ -3,18 +3,14 @@ use std::fmt; use anyhow::{bail, Result}; use clap::Parser; use comfy_table::{Cell, Row, Table}; -use io_maildir::{ - coroutines::message_get::{MaildirMessageGet, MaildirMessageGetArg, MaildirMessageGetResult}, - maildir::Maildir, -}; +use io_maildir::maildir::Maildir; use mail_parser::Header; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::maildir::{ account::MaildirAccount, arg::{MaildirPathFlag, MessageIdArg}, - runtime, }; /// Get a single MAILDIR envelope. @@ -34,24 +30,11 @@ impl MaildirEnvelopeGetCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { let maildir = match Maildir::try_from(self.maildir.inner.clone()) { Ok(maildir) => maildir, - Err(_) => Maildir::try_from(account.backend.root.join(self.maildir.inner))?, + Err(_) => Maildir::try_from(account.backend.root.join(&self.maildir.inner))?, }; - let mut coroutine = MaildirMessageGet::new(maildir, &self.id.inner); - let mut arg = None; - - let message = loop { - match coroutine.resume(arg.take()) { - MaildirMessageGetResult::Ok(message) => break message, - MaildirMessageGetResult::WantsDirRead(paths) => { - arg = Some(MaildirMessageGetArg::DirRead(runtime::dir_read(paths)?)); - } - MaildirMessageGetResult::WantsFileRead(paths) => { - arg = Some(MaildirMessageGetArg::FileRead(runtime::file_read(paths)?)); - } - MaildirMessageGetResult::Err(err) => bail!("{err}"), - } - }; + let client = account.new_maildir_client(); + let message = client.get(maildir, &self.id.inner)?; let path = message.path().to_owned(); diff --git a/src/maildir/envelope/list.rs b/src/maildir/envelope/list.rs index e1201a4c..bd35d321 100644 --- a/src/maildir/envelope/list.rs +++ b/src/maildir/envelope/list.rs @@ -1,18 +1,13 @@ use std::fmt; -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; use comfy_table::{Cell, ContentArrangement, Row, Table}; -use io_maildir::{ - coroutines::message_list::{ - MaildirMessagesList, MaildirMessagesListArg, MaildirMessagesListResult, - }, - maildir::Maildir, -}; -use pimalaya_toolbox::terminal::printer::Printer; +use io_maildir::maildir::Maildir; +use pimalaya_cli::printer::Printer; use serde::Serialize; -use crate::maildir::{account::MaildirAccount, arg::MaildirPathFlag, runtime}; +use crate::maildir::{account::MaildirAccount, arg::MaildirPathFlag}; /// List MAILDIR envelopes from the given mailbox. /// @@ -29,24 +24,11 @@ impl MaildirEnvelopeListCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { let maildir = match Maildir::try_from(self.maildir.inner.clone()) { Ok(maildir) => maildir, - Err(_) => Maildir::try_from(account.backend.root.join(self.maildir.inner))?, + Err(_) => Maildir::try_from(account.backend.root.join(&self.maildir.inner))?, }; - let mut coroutine = MaildirMessagesList::new(maildir); - let mut arg = None; - - let messages = loop { - match coroutine.resume(arg.take()) { - MaildirMessagesListResult::Ok(messages) => break messages, - MaildirMessagesListResult::WantsDirRead(paths) => { - arg = Some(MaildirMessagesListArg::DirRead(runtime::dir_read(paths)?)); - } - MaildirMessagesListResult::WantsFileRead(paths) => { - arg = Some(MaildirMessagesListArg::FileRead(runtime::file_read(paths)?)); - } - MaildirMessagesListResult::Err(err) => bail!("{err}"), - } - }; + let client = account.new_maildir_client(); + let messages = client.list_messages(maildir)?; let mut envelopes = Vec::with_capacity(messages.len()); diff --git a/src/maildir/envelope/mod.rs b/src/maildir/envelope/mod.rs index 497c1316..aaddc1a8 100644 --- a/src/maildir/envelope/mod.rs +++ b/src/maildir/envelope/mod.rs @@ -1,3 +1,3 @@ -pub mod command; +pub mod cli; pub mod get; pub mod list; diff --git a/src/maildir/flag/add.rs b/src/maildir/flag/add.rs index 99bb89f6..eb601796 100644 --- a/src/maildir/flag/add.rs +++ b/src/maildir/flag/add.rs @@ -1,17 +1,12 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_maildir::{ - coroutines::flags_add::{MaildirFlagsAdd, MaildirFlagsAddArg, MaildirFlagsAddResult}, - flag::Flags, - maildir::Maildir, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_maildir::{flag::Flags, maildir::Maildir}; +use pimalaya_cli::printer::{Message, Printer}; use crate::maildir::{ account::MaildirAccount, arg::{MaildirPathFlag, MessageIdsArg}, flag::arg::FlagArg, - runtime, }; /// Add MAILDIR flag(s) to message(s). @@ -34,28 +29,14 @@ impl MaildirFlagAddCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { let maildir = match Maildir::try_from(self.maildir.inner.clone()) { Ok(maildir) => maildir, - Err(_) => Maildir::try_from(account.backend.root.join(self.maildir.inner))?, + Err(_) => Maildir::try_from(account.backend.root.join(&self.maildir.inner))?, }; let flags = Flags::from_iter(self.flags.into_iter().map(Into::into)); + let client = account.new_maildir_client(); for id in self.ids.inner { - let mut coroutine = MaildirFlagsAdd::new(maildir.clone(), id, flags.clone()); - let mut arg = None; - - loop { - match coroutine.resume(arg.take()) { - MaildirFlagsAddResult::Ok => break, - MaildirFlagsAddResult::WantsDirRead(paths) => { - arg = Some(MaildirFlagsAddArg::DirRead(runtime::dir_read(paths)?)); - } - MaildirFlagsAddResult::WantsRename(pairs) => { - runtime::rename(pairs)?; - arg = Some(MaildirFlagsAddArg::Rename); - } - MaildirFlagsAddResult::Err(err) => bail!("{err}"), - } - } + client.add_flags(maildir.clone(), id, flags.clone())?; } printer.out(Message::new("Flag(s) successfully added")) diff --git a/src/maildir/flag/command.rs b/src/maildir/flag/cli.rs similarity index 95% rename from src/maildir/flag/command.rs rename to src/maildir/flag/cli.rs index 91ce541c..0ec18bc9 100644 --- a/src/maildir/flag/command.rs +++ b/src/maildir/flag/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::maildir::{ account::MaildirAccount, diff --git a/src/maildir/flag/list.rs b/src/maildir/flag/list.rs index 91f9cdf4..05448fa9 100644 --- a/src/maildir/flag/list.rs +++ b/src/maildir/flag/list.rs @@ -4,7 +4,7 @@ use anyhow::Result; use clap::Parser; use comfy_table::{Cell, ContentArrangement, Row, Table}; use io_maildir::flag::Flag; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::maildir::account::MaildirAccount; diff --git a/src/maildir/flag/mod.rs b/src/maildir/flag/mod.rs index 661ce207..8e72513c 100644 --- a/src/maildir/flag/mod.rs +++ b/src/maildir/flag/mod.rs @@ -1,6 +1,6 @@ pub mod add; pub mod arg; -pub mod command; +pub mod cli; pub mod list; pub mod remove; pub mod set; diff --git a/src/maildir/flag/remove.rs b/src/maildir/flag/remove.rs index 312c6e29..f664d304 100644 --- a/src/maildir/flag/remove.rs +++ b/src/maildir/flag/remove.rs @@ -1,19 +1,12 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_maildir::{ - coroutines::flags_remove::{ - MaildirFlagsRemove, MaildirFlagsRemoveArg, MaildirFlagsRemoveResult, - }, - flag::Flags, - maildir::Maildir, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_maildir::{flag::Flags, maildir::Maildir}; +use pimalaya_cli::printer::{Message, Printer}; use crate::maildir::{ account::MaildirAccount, arg::{MaildirPathFlag, MessageIdsArg}, flag::arg::FlagArg, - runtime, }; /// Remove MAILDIR flag(s) to message(s). @@ -36,28 +29,14 @@ impl MaildirFlagRemoveCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { let maildir = match Maildir::try_from(self.maildir.inner.clone()) { Ok(maildir) => maildir, - Err(_) => Maildir::try_from(account.backend.root.join(self.maildir.inner))?, + Err(_) => Maildir::try_from(account.backend.root.join(&self.maildir.inner))?, }; let flags = Flags::from_iter(self.flags.into_iter().map(Into::into)); + let client = account.new_maildir_client(); for id in self.ids.inner { - let mut coroutine = MaildirFlagsRemove::new(maildir.clone(), id, flags.clone()); - let mut arg = None; - - loop { - match coroutine.resume(arg.take()) { - MaildirFlagsRemoveResult::Ok => break, - MaildirFlagsRemoveResult::WantsDirRead(paths) => { - arg = Some(MaildirFlagsRemoveArg::DirRead(runtime::dir_read(paths)?)); - } - MaildirFlagsRemoveResult::WantsRename(pairs) => { - runtime::rename(pairs)?; - arg = Some(MaildirFlagsRemoveArg::Rename); - } - MaildirFlagsRemoveResult::Err(err) => bail!("{err}"), - } - } + client.remove_flags(maildir.clone(), id, flags.clone())?; } printer.out(Message::new("Flag(s) successfully removed")) diff --git a/src/maildir/flag/set.rs b/src/maildir/flag/set.rs index 4bcc0b86..1d351bae 100644 --- a/src/maildir/flag/set.rs +++ b/src/maildir/flag/set.rs @@ -1,17 +1,12 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_maildir::{ - coroutines::flags_set::{MaildirFlagsSet, MaildirFlagsSetArg, MaildirFlagsSetResult}, - flag::Flags, - maildir::Maildir, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_maildir::{flag::Flags, maildir::Maildir}; +use pimalaya_cli::printer::{Message, Printer}; use crate::maildir::{ account::MaildirAccount, arg::{MaildirPathFlag, MessageIdsArg}, flag::arg::FlagArg, - runtime, }; /// Set MAILDIR flag(s) to message(s). @@ -34,28 +29,14 @@ impl MaildirFlagSetCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { let maildir = match Maildir::try_from(self.maildir.inner.clone()) { Ok(maildir) => maildir, - Err(_) => Maildir::try_from(account.backend.root.join(self.maildir.inner))?, + Err(_) => Maildir::try_from(account.backend.root.join(&self.maildir.inner))?, }; let flags = Flags::from_iter(self.flags.into_iter().map(Into::into)); + let client = account.new_maildir_client(); for id in self.ids.inner { - let mut coroutine = MaildirFlagsSet::new(maildir.clone(), id, flags.clone()); - let mut arg = None; - - loop { - match coroutine.resume(arg.take()) { - MaildirFlagsSetResult::Ok => break, - MaildirFlagsSetResult::WantsDirRead(paths) => { - arg = Some(MaildirFlagsSetArg::DirRead(runtime::dir_read(paths)?)); - } - MaildirFlagsSetResult::WantsRename(pairs) => { - runtime::rename(pairs)?; - arg = Some(MaildirFlagsSetArg::Rename); - } - MaildirFlagsSetResult::Err(err) => bail!("{err}"), - } - } + client.set_flags(maildir.clone(), id, flags.clone())?; } printer.out(Message::new("Flag(s) successfully changed")) diff --git a/src/maildir/list.rs b/src/maildir/list.rs index 8aba1875..9638c19d 100644 --- a/src/maildir/list.rs +++ b/src/maildir/list.rs @@ -1,16 +1,13 @@ use std::{fmt, path::PathBuf}; -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; use comfy_table::{Cell, Row, Table}; -use io_maildir::{ - coroutines::maildir_list::{MaildirList, MaildirListArg, MaildirListResult}, - maildir::Maildir, -}; -use pimalaya_toolbox::terminal::printer::Printer; +use io_maildir::maildir::Maildir; +use pimalaya_cli::printer::Printer; use serde::Serialize; -use crate::maildir::{account::MaildirAccount, runtime}; +use crate::maildir::account::MaildirAccount; /// List, search and filter maildirs. /// @@ -22,18 +19,8 @@ pub struct MaildirMailboxListCommand; impl MaildirMailboxListCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { - let mut coroutine = MaildirList::new(account.backend.root); - let mut arg = None; - - let maildirs = loop { - match coroutine.resume(arg.take()) { - MaildirListResult::Ok(maildirs) => break maildirs, - MaildirListResult::WantsDirRead(paths) => { - arg = Some(MaildirListArg::DirRead(runtime::dir_read(paths)?)); - } - MaildirListResult::Err(err) => bail!("{err}"), - } - }; + let client = account.new_maildir_client(); + let maildirs = client.list_maildirs()?; let table = MaildirsTable { preset: account.table_preset, diff --git a/src/maildir/message/command.rs b/src/maildir/message/cli.rs similarity index 96% rename from src/maildir/message/command.rs rename to src/maildir/message/cli.rs index 694af74f..36874470 100644 --- a/src/maildir/message/command.rs +++ b/src/maildir/message/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::maildir::{ account::MaildirAccount, diff --git a/src/maildir/message/copy.rs b/src/maildir/message/copy.rs index 3ae31701..08d40fc9 100644 --- a/src/maildir/message/copy.rs +++ b/src/maildir/message/copy.rs @@ -1,17 +1,11 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_maildir::{ - coroutines::message_copy::{ - MaildirMessageCopy, MaildirMessageCopyArg, MaildirMessageCopyResult, - }, - maildir::Maildir, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_maildir::maildir::Maildir; +use pimalaya_cli::printer::{Message, Printer}; use crate::maildir::{ account::MaildirAccount, arg::{MaildirPathFlag, MaildirSubdirArg, MessageIdsArg, TargetMaildirPathFlag}, - runtime, }; /// Copy Maildir message to the given mailbox. @@ -36,36 +30,23 @@ impl MaildirMessageCopyCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { let source = match Maildir::try_from(self.source.inner.clone()) { Ok(maildir) => maildir, - Err(_) => Maildir::try_from(account.backend.root.join(self.source.inner))?, + Err(_) => Maildir::try_from(account.backend.root.join(&self.source.inner))?, }; let target = match Maildir::try_from(self.target.inner.clone()) { Ok(maildir) => maildir, - Err(_) => Maildir::try_from(account.backend.root.join(self.target.inner))?, + Err(_) => Maildir::try_from(account.backend.root.join(&self.target.inner))?, }; + let client = account.new_maildir_client(); + for id in self.ids.inner { - let mut coroutine = MaildirMessageCopy::new( + client.copy( id, source.clone(), target.clone(), self.subdir.clone().map(Into::into), - ); - let mut arg = None; - - loop { - match coroutine.resume(arg.take()) { - MaildirMessageCopyResult::Ok => break, - MaildirMessageCopyResult::WantsDirRead(paths) => { - arg = Some(MaildirMessageCopyArg::DirRead(runtime::dir_read(paths)?)); - } - MaildirMessageCopyResult::WantsCopy(pairs) => { - runtime::copy(pairs)?; - arg = Some(MaildirMessageCopyArg::Copy); - } - MaildirMessageCopyResult::Err(err) => bail!("{err}"), - } - } + )?; } printer.out(Message::new("Message(s) successfully copied")) diff --git a/src/maildir/message/export.rs b/src/maildir/message/export.rs index 316485b5..995ccba3 100644 --- a/src/maildir/message/export.rs +++ b/src/maildir/message/export.rs @@ -3,19 +3,15 @@ use std::{fmt, fs, path::PathBuf}; use anyhow::{bail, Result}; use clap::{Parser, ValueEnum}; use convert_case::ccase; -use io_maildir::{ - coroutines::message_get::{MaildirMessageGet, MaildirMessageGetArg, MaildirMessageGetResult}, - maildir::Maildir, -}; +use io_maildir::maildir::Maildir; use mail_parser::MimeHeaders; use mime_guess::{get_mime_extensions_str, mime::OCTET_STREAM}; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::maildir::{ account::MaildirAccount, arg::{MaildirPathFlag, MessageIdArg}, - runtime, }; /// Export a message. @@ -48,24 +44,11 @@ impl MaildirMessageExportCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { let maildir = match Maildir::try_from(self.maildir.inner.clone()) { Ok(maildir) => maildir, - Err(_) => Maildir::try_from(account.backend.root.join(self.maildir.inner))?, + Err(_) => Maildir::try_from(account.backend.root.join(&self.maildir.inner))?, }; - let mut coroutine = MaildirMessageGet::new(maildir, &self.id.inner); - let mut arg = None; - - let msg = loop { - match coroutine.resume(arg.take()) { - MaildirMessageGetResult::Ok(msg) => break msg, - MaildirMessageGetResult::WantsDirRead(paths) => { - arg = Some(MaildirMessageGetArg::DirRead(runtime::dir_read(paths)?)); - } - MaildirMessageGetResult::WantsFileRead(paths) => { - arg = Some(MaildirMessageGetArg::FileRead(runtime::file_read(paths)?)); - } - MaildirMessageGetResult::Err(err) => bail!("{err}"), - } - }; + let client = account.new_maildir_client(); + let msg = client.get(maildir, &self.id.inner)?; match self.r#type { ExportType::Raw => { diff --git a/src/maildir/message/get.rs b/src/maildir/message/get.rs index bf9c8c8d..1a8b4a83 100644 --- a/src/maildir/message/get.rs +++ b/src/maildir/message/get.rs @@ -2,18 +2,13 @@ use std::fmt; use anyhow::{bail, Result}; use clap::Parser; -use io_maildir::{ - coroutines::message_get::{MaildirMessageGet, MaildirMessageGetArg, MaildirMessageGetResult}, - maildir::Maildir, - types::Message, -}; -use pimalaya_toolbox::terminal::printer::Printer; +use io_maildir::{maildir::Maildir, types::Message}; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::maildir::{ account::MaildirAccount, arg::{MaildirPathFlag, MessageIdArg}, - runtime, }; /// Get Maildir message to the given mailbox. @@ -32,24 +27,11 @@ impl MaildirMessageGetCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { let maildir = match Maildir::try_from(self.maildir.inner.clone()) { Ok(maildir) => maildir, - Err(_) => Maildir::try_from(account.backend.root.join(self.maildir.inner))?, + Err(_) => Maildir::try_from(account.backend.root.join(&self.maildir.inner))?, }; - let mut coroutine = MaildirMessageGet::new(maildir, &self.id.inner); - let mut arg = None; - - let msg = loop { - match coroutine.resume(arg.take()) { - MaildirMessageGetResult::Ok(msg) => break msg, - MaildirMessageGetResult::WantsDirRead(paths) => { - arg = Some(MaildirMessageGetArg::DirRead(runtime::dir_read(paths)?)); - } - MaildirMessageGetResult::WantsFileRead(paths) => { - arg = Some(MaildirMessageGetArg::FileRead(runtime::file_read(paths)?)); - } - MaildirMessageGetResult::Err(err) => bail!("{err}"), - } - }; + let client = account.new_maildir_client(); + let msg = client.get(maildir, &self.id.inner)?; let path = msg.path().to_owned(); diff --git a/src/maildir/message/mod.rs b/src/maildir/message/mod.rs index 91cc5818..2cbac40f 100644 --- a/src/maildir/message/mod.rs +++ b/src/maildir/message/mod.rs @@ -1,4 +1,4 @@ -pub mod command; +pub mod cli; pub mod copy; pub mod export; pub mod get; diff --git a/src/maildir/message/move.rs b/src/maildir/message/move.rs index 6621ecd2..1aed9b90 100644 --- a/src/maildir/message/move.rs +++ b/src/maildir/message/move.rs @@ -1,17 +1,11 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_maildir::{ - coroutines::message_move::{ - MaildirMessageMove, MaildirMessageMoveArg, MaildirMessageMoveResult, - }, - maildir::Maildir, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use io_maildir::maildir::Maildir; +use pimalaya_cli::printer::{Message, Printer}; use crate::maildir::{ account::MaildirAccount, arg::{MaildirPathFlag, MaildirSubdirArg, MessageIdsArg, TargetMaildirPathFlag}, - runtime, }; /// Move Maildir message to the given mailbox. @@ -36,36 +30,23 @@ impl MaildirMessageMoveCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { let source = match Maildir::try_from(self.source.inner.clone()) { Ok(maildir) => maildir, - Err(_) => Maildir::try_from(account.backend.root.join(self.source.inner))?, + Err(_) => Maildir::try_from(account.backend.root.join(&self.source.inner))?, }; let target = match Maildir::try_from(self.target.inner.clone()) { Ok(maildir) => maildir, - Err(_) => Maildir::try_from(account.backend.root.join(self.target.inner))?, + Err(_) => Maildir::try_from(account.backend.root.join(&self.target.inner))?, }; + let client = account.new_maildir_client(); + for id in self.ids.inner { - let mut coroutine = MaildirMessageMove::new( + client.r#move( id, source.clone(), target.clone(), self.subdir.clone().map(Into::into), - ); - let mut arg = None; - - loop { - match coroutine.resume(arg.take()) { - MaildirMessageMoveResult::Ok => break, - MaildirMessageMoveResult::WantsDirRead(paths) => { - arg = Some(MaildirMessageMoveArg::DirRead(runtime::dir_read(paths)?)); - } - MaildirMessageMoveResult::WantsRename(pairs) => { - runtime::rename(pairs)?; - arg = Some(MaildirMessageMoveArg::Rename); - } - MaildirMessageMoveResult::Err(err) => bail!("{err}"), - } - } + )?; } printer.out(Message::new("Message(s) successfully copied")) diff --git a/src/maildir/message/read.rs b/src/maildir/message/read.rs index cd2cf38c..598dfbcf 100644 --- a/src/maildir/message/read.rs +++ b/src/maildir/message/read.rs @@ -2,18 +2,13 @@ use std::fmt; use anyhow::{bail, Result}; use clap::Parser; -use io_maildir::{ - coroutines::message_get::{MaildirMessageGet, MaildirMessageGetArg, MaildirMessageGetResult}, - maildir::Maildir, - types::Message, -}; -use pimalaya_toolbox::terminal::printer::Printer; +use io_maildir::{maildir::Maildir, types::Message}; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::maildir::{ account::MaildirAccount, arg::{MaildirPathFlag, MessageIdArg}, - runtime, }; /// Read message content. @@ -39,24 +34,11 @@ impl MaildirMessageReadCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { let maildir = match Maildir::try_from(self.maildir.inner.clone()) { Ok(maildir) => maildir, - Err(_) => Maildir::try_from(account.backend.root.join(self.maildir.inner))?, + Err(_) => Maildir::try_from(account.backend.root.join(&self.maildir.inner))?, }; - let mut coroutine = MaildirMessageGet::new(maildir, &self.id.inner); - let mut arg = None; - - let message = loop { - match coroutine.resume(arg.take()) { - MaildirMessageGetResult::Ok(msg) => break msg, - MaildirMessageGetResult::WantsDirRead(paths) => { - arg = Some(MaildirMessageGetArg::DirRead(runtime::dir_read(paths)?)); - } - MaildirMessageGetResult::WantsFileRead(paths) => { - arg = Some(MaildirMessageGetArg::FileRead(runtime::file_read(paths)?)); - } - MaildirMessageGetResult::Err(err) => bail!("{err}"), - } - }; + let client = account.new_maildir_client(); + let message = client.get(maildir, &self.id.inner)?; let path = message.path().to_owned(); diff --git a/src/maildir/message/save.rs b/src/maildir/message/save.rs index 9c0a8d64..dae17145 100644 --- a/src/maildir/message/save.rs +++ b/src/maildir/message/save.rs @@ -4,23 +4,16 @@ use std::{ path::PathBuf, }; -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_maildir::{ - coroutines::message_store::{ - MaildirMessageStore, MaildirMessageStoreArg, MaildirMessageStoreResult, - }, - flag::Flags, - maildir::Maildir, -}; -use pimalaya_toolbox::terminal::printer::Printer; +use io_maildir::{flag::Flags, maildir::Maildir}; +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::maildir::{ account::MaildirAccount, arg::{MaildirPathFlag, MaildirSubdirArg}, flag::arg::FlagArg, - runtime, }; /// Save a message to a mailbox. @@ -51,7 +44,7 @@ impl MaildirMessageSaveCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { let maildir = match Maildir::try_from(self.maildir.inner.clone()) { Ok(maildir) => maildir, - Err(_) => Maildir::try_from(account.backend.root.join(self.maildir.inner))?, + Err(_) => Maildir::try_from(account.backend.root.join(&self.maildir.inner))?, }; let msg = if stdin().is_terminal() || printer.is_json() { @@ -69,27 +62,10 @@ impl MaildirMessageSaveCommand { }; let flags = Flags::from_iter(self.flags.into_iter().map(Into::into)); + let client = account.new_maildir_client(); + let (id, path) = client.store(maildir, self.subdir.into(), flags, msg.into_bytes())?; - let mut coroutine = - MaildirMessageStore::new(maildir, self.subdir.into(), flags, msg.into_bytes()); - let mut arg = None; - - let out = loop { - match coroutine.resume(arg.take()) { - MaildirMessageStoreResult::Ok { id, path } => break StoredMessage { id, path }, - MaildirMessageStoreResult::WantsFileCreate(files) => { - runtime::file_create(files)?; - arg = Some(MaildirMessageStoreArg::FileCreate); - } - MaildirMessageStoreResult::WantsRename(pairs) => { - runtime::rename(pairs)?; - arg = Some(MaildirMessageStoreArg::Rename); - } - MaildirMessageStoreResult::Err(err) => bail!("{err}"), - } - }; - - printer.out(out) + printer.out(StoredMessage { id, path }) } } diff --git a/src/maildir/mod.rs b/src/maildir/mod.rs index 7e1386e5..07f7496f 100644 --- a/src/maildir/mod.rs +++ b/src/maildir/mod.rs @@ -1,6 +1,6 @@ pub mod account; pub mod arg; -pub mod command; +pub mod cli; pub mod create; pub mod delete; pub mod envelope; diff --git a/src/maildir/rename.rs b/src/maildir/rename.rs index 5f5e81ca..c4ba1f44 100644 --- a/src/maildir/rename.rs +++ b/src/maildir/rename.rs @@ -1,14 +1,10 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Parser; -use io_maildir::coroutines::maildir_rename::{ - MaildirRename, MaildirRenameArg, MaildirRenameResult, -}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::maildir::{ account::MaildirAccount, arg::{MaildirNameArg, MaildirPathFlag}, - runtime, }; /// Rename the given mailbox. @@ -25,22 +21,9 @@ pub struct MaildirMailboxRenameCommand { impl MaildirMailboxRenameCommand { pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> { - let path = account.backend.root.join(self.maildir_path.inner); - - let mut coroutine = MaildirRename::new(path, self.maildir_name.inner); - let mut arg = None; - - loop { - match coroutine.resume(arg.take()) { - MaildirRenameResult::Ok => break, - MaildirRenameResult::WantsRename(pairs) => { - runtime::rename(pairs)?; - arg = Some(MaildirRenameArg::Rename); - } - MaildirRenameResult::Err(err) => bail!("{err}"), - } - } - + let path = account.backend.root.join(&self.maildir_path.inner); + let client = account.new_maildir_client(); + client.rename_maildir(path, self.maildir_name.inner)?; printer.out(Message::new("Maildir successfully renamed")) } } diff --git a/src/maildir/runtime.rs b/src/maildir/runtime.rs index 4fb4dd24..580d60dc 100644 --- a/src/maildir/runtime.rs +++ b/src/maildir/runtime.rs @@ -8,6 +8,10 @@ //! Each helper performs the operation and returns the raw output //! (only meaningful for read-style ops). Callers wrap the result in //! the appropriate `*Arg` variant for their coroutine. +//! +//! Used by the cross-protocol shared commands (`messages add`, `mv`, +//! `copy`); the protocol-specific `maildir` subcommands now go +//! through [`io_maildir::client::MaildirClient`]. use std::{ collections::{BTreeMap, BTreeSet}, diff --git a/src/main.rs b/src/main.rs index 23064ae5..9793da9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod account; mod attachments; mod cli; mod config; +mod email_client; mod envelopes; mod flags; #[cfg(feature = "imap")] @@ -15,9 +16,10 @@ mod maildir; mod messages; #[cfg(feature = "smtp")] mod smtp; +mod wizard; use clap::Parser; -use pimalaya_toolbox::terminal::{error::ErrorReport, log::Logger, printer::StdoutPrinter}; +use pimalaya_cli::{error::ErrorReport, log::Logger, printer::StdoutPrinter}; use crate::cli::HimalayaCli; diff --git a/src/messages/add.rs b/src/messages/add.rs index 8604ce69..bbb29c40 100644 --- a/src/messages/add.rs +++ b/src/messages/add.rs @@ -8,8 +8,8 @@ use std::{ use anyhow::{bail, Result}; use clap::Parser; #[cfg(any(feature = "imap", feature = "jmap", feature = "maildir"))] -use pimalaya_toolbox::terminal::printer::Message; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Message; +use pimalaya_cli::printer::Printer; use crate::{ cli::BackendArg, @@ -60,9 +60,9 @@ impl MessagesAddCommand { #[cfg(feature = "imap")] if backend.allows_imap() { if let Some(imap_config) = account_config.imap.take() { + use crate::imap::session::ImapSession; use io_email::imap::message_add::{MessageAdd, MessageAddResult}; use io_imap::types::mailbox::Mailbox; - use pimalaya_toolbox::stream::imap::ImapSession; let account = crate::account::Account::new(config, account_config, imap_config)?; let mut session = ImapSession::new( @@ -99,8 +99,8 @@ impl MessagesAddCommand { #[cfg(feature = "jmap")] if backend.allows_jmap() { if let Some(jmap_config) = account_config.jmap.take() { + use crate::jmap::session::JmapSession; use io_email::jmap::message_add::{MessageAdd, MessageAddResult}; - use pimalaya_toolbox::stream::jmap::JmapSession; let account = crate::account::Account::new(config, account_config, jmap_config)?; let mut session = JmapSession::new( diff --git a/src/messages/command.rs b/src/messages/cli.rs similarity index 97% rename from src/messages/command.rs rename to src/messages/cli.rs index 1adb3837..4c98e4ad 100644 --- a/src/messages/command.rs +++ b/src/messages/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::{ cli::BackendArg, diff --git a/src/messages/compose.rs b/src/messages/compose.rs index 55c73d64..d4b308fc 100644 --- a/src/messages/compose.rs +++ b/src/messages/compose.rs @@ -10,7 +10,7 @@ use mail_builder::headers::raw::Raw; use mail_builder::{headers::address::Address, MessageBuilder}; #[cfg(any(feature = "imap", feature = "jmap", feature = "maildir"))] use mail_parser::{HeaderValue, MessageParser}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::{ cli::BackendArg, @@ -510,7 +510,7 @@ fn send_raw( if backend.allows_smtp() { if let Some(smtp_config) = account_config.smtp.take() { use io_email::smtp::message_send::{MessageSend, MessageSendResult}; - use pimalaya_toolbox::stream::smtp::SmtpSession; + use pimalaya_stream::std::smtp::SmtpSession; let account = crate::account::Account::new(config, account_config, smtp_config)?; let mut session = SmtpSession::new( @@ -544,8 +544,8 @@ fn send_raw( #[cfg(feature = "jmap")] if backend.allows_jmap() { if let Some(jmap_config) = account_config.jmap.take() { + use crate::jmap::session::JmapSession; use io_email::jmap::message_send::{MessageSend, MessageSendResult}; - use pimalaya_toolbox::stream::jmap::JmapSession; let identity_id = jmap_config.identity_id.clone().ok_or_else(|| { anyhow!( diff --git a/src/messages/copy.rs b/src/messages/copy.rs index 4e95d7e8..07dee973 100644 --- a/src/messages/copy.rs +++ b/src/messages/copy.rs @@ -3,7 +3,7 @@ use std::io::{Read, Write}; use anyhow::{bail, Result}; use clap::Parser; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::{ account::Account, @@ -53,9 +53,9 @@ impl MessagesCopyCommand { #[cfg(feature = "imap")] if backend.allows_imap() { if let Some(imap_config) = account_config.imap.take() { + use crate::imap::session::ImapSession; use io_email::imap::message_copy::{MessageCopy, MessageCopyResult}; use io_imap::types::{mailbox::Mailbox, sequence::SequenceSet}; - use pimalaya_toolbox::stream::imap::ImapSession; let account = Account::new(config, account_config, imap_config)?; let mut session = ImapSession::new( @@ -93,8 +93,8 @@ impl MessagesCopyCommand { #[cfg(feature = "jmap")] if backend.allows_jmap() { if let Some(jmap_config) = account_config.jmap.take() { + use crate::jmap::session::JmapSession; use io_email::jmap::message_copy::{MessageCopy, MessageCopyResult}; - use pimalaya_toolbox::stream::jmap::JmapSession; let account = Account::new(config, account_config, jmap_config)?; let mut session = JmapSession::new( diff --git a/src/messages/fetch.rs b/src/messages/fetch.rs index bfdc8315..66b1814d 100644 --- a/src/messages/fetch.rs +++ b/src/messages/fetch.rs @@ -6,19 +6,14 @@ //! the IMAP id as a UID — sequence-number addressing belongs to the //! protocol-specific `imap` subcommands. -#[cfg(any(feature = "imap", feature = "jmap"))] -use std::io::{Read, Write}; - -use anyhow::{bail, Result}; +use anyhow::Result; use crate::{ cli::BackendArg, config::{AccountConfig, Config}, + email_client::build, }; -#[cfg(any(feature = "imap", feature = "jmap"))] -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Fetches the raw RFC 5322 bytes of `id` from `mailbox` via the first /// configured backend that matches `backend`. Bails when no backend /// matches. @@ -29,115 +24,6 @@ pub(crate) fn fetch_raw( mailbox: &str, id: &str, ) -> Result> { - #[cfg(feature = "imap")] - if backend.allows_imap() { - if let Some(imap_config) = account_config.imap.clone() { - use std::num::NonZeroU32; - - use io_email::imap::message_get::{MessageGet, MessageGetResult}; - use io_imap::types::mailbox::Mailbox; - use pimalaya_toolbox::stream::imap::ImapSession; - - let account = - crate::account::Account::new(config.clone(), account_config.clone(), imap_config)?; - let mut session = ImapSession::new( - account.backend.url.clone(), - account.backend.tls.clone().try_into()?, - account.backend.starttls, - account.backend.sasl.clone().try_into()?, - )?; - - let imap_mailbox: Mailbox<'static> = mailbox.to_owned().try_into()?; - let id: NonZeroU32 = id.parse()?; - let mut coroutine = MessageGet::new(session.context, imap_mailbox, id, true); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - return loop { - match coroutine.resume(arg.take()) { - MessageGetResult::Ok(raw) => break Ok(raw), - MessageGetResult::WantsRead => { - let n = session.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - MessageGetResult::WantsWrite(bytes) => { - session.stream.write_all(&bytes)?; - } - MessageGetResult::Err(err) => bail!("{err}"), - } - }; - } - } - - #[cfg(feature = "jmap")] - if backend.allows_jmap() { - if let Some(jmap_config) = account_config.jmap.clone() { - use io_email::jmap::message_get::{MessageGet, MessageGetResult}; - use pimalaya_toolbox::stream::jmap::JmapSession; - - let _ = mailbox; - let account = - crate::account::Account::new(config.clone(), account_config.clone(), jmap_config)?; - let mut session = JmapSession::new( - account.backend.server.clone(), - account.backend.tls.clone().try_into()?, - account.backend.auth.clone().try_into()?, - )?; - let mut coroutine = MessageGet::new(&session.session, &session.http_auth, id)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - return loop { - match coroutine.resume(arg.take()) { - MessageGetResult::Ok(raw) => break Ok(raw), - MessageGetResult::WantsRead => { - let n = session.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - MessageGetResult::WantsWrite(bytes) => { - session.stream.write_all(&bytes)?; - } - MessageGetResult::Err(err) => bail!("{err}"), - } - }; - } - } - - #[cfg(feature = "maildir")] - if backend.allows_maildir() { - if let Some(maildir_config) = account_config.maildir.clone() { - use io_email::maildir::message_get::{MessageGet, MessageGetArg, MessageGetResult}; - use io_maildir::maildir::Maildir; - - let account = crate::account::Account::new( - config.clone(), - account_config.clone(), - maildir_config, - )?; - let path = account.backend.root.join(mailbox); - let maildir = Maildir::try_from(path)?; - - let mut coroutine = MessageGet::new(maildir, id); - let mut arg: Option = None; - - return loop { - match coroutine.resume(arg.take()) { - MessageGetResult::Ok(raw) => break Ok(raw), - MessageGetResult::WantsDirRead(paths) => { - arg = Some(MessageGetArg::DirRead(crate::maildir::runtime::dir_read( - paths, - )?)); - } - MessageGetResult::WantsFileRead(paths) => { - arg = Some(MessageGetArg::FileRead(crate::maildir::runtime::file_read( - paths, - )?)); - } - MessageGetResult::Err(err) => bail!("{err}"), - } - }; - } - } - - bail!("no backend matching `{backend}` is configured for this account") + let mut ctx = build(config.clone(), account_config.clone(), backend)?; + Ok(ctx.client.get_message(mailbox, id)?) } diff --git a/src/messages/get.rs b/src/messages/get.rs index 1ca7f7fe..4bcd629c 100644 --- a/src/messages/get.rs +++ b/src/messages/get.rs @@ -1,28 +1,20 @@ -#[cfg(feature = "maildir")] -use std::collections::{BTreeMap, BTreeSet}; -#[cfg(any(feature = "imap", feature = "jmap", feature = "maildir"))] -use std::fmt; -#[cfg(any(feature = "imap", feature = "jmap"))] -use std::io::Read; -use std::io::{stdout, Write}; +use std::{ + fmt, + io::{stdout, Write}, +}; use anyhow::{bail, Result}; use clap::Parser; -#[cfg(any(feature = "imap", feature = "jmap", feature = "maildir"))] use mail_parser::{Message, MessageParser}; -use pimalaya_toolbox::terminal::printer::Printer; -#[cfg(any(feature = "imap", feature = "jmap", feature = "maildir"))] +use pimalaya_cli::printer::Printer; use serde::Serialize; use crate::{ - account::Account, cli::BackendArg, config::{AccountConfig, Config}, + messages::fetch::fetch_raw, }; -#[cfg(any(feature = "imap", feature = "jmap"))] -const READ_BUFFER_SIZE: usize = 16 * 1024; - /// Get a message from the active account. /// /// By default the message is parsed and rendered as headers + text @@ -57,180 +49,33 @@ impl MessagesGetCommand { self, printer: &mut impl Printer, config: Config, - mut account_config: AccountConfig, + account_config: AccountConfig, backend: BackendArg, ) -> Result<()> { if self.raw && printer.is_json() { bail!("`--raw` and `--json` cannot be combined"); } - #[cfg(feature = "imap")] - if backend.allows_imap() { - if let Some(imap_config) = account_config.imap.take() { - use std::num::NonZeroU32; + let raw = fetch_raw(&config, &account_config, backend, &self.mailbox, &self.id)?; - use io_email::imap::message_get::{MessageGet, MessageGetResult}; - use io_imap::types::mailbox::Mailbox; - use pimalaya_toolbox::stream::imap::ImapSession; - - let account = Account::new(config, account_config, imap_config)?; - let mut session = ImapSession::new( - account.backend.url.clone(), - account.backend.tls.clone().try_into()?, - account.backend.starttls, - account.backend.sasl.clone().try_into()?, - )?; - - let mailbox: Mailbox<'static> = self.mailbox.clone().try_into()?; - let id: NonZeroU32 = self.id.parse()?; - let mut coroutine = MessageGet::new(session.context, mailbox, id, true); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let raw = loop { - match coroutine.resume(arg.take()) { - MessageGetResult::Ok(raw) => break raw, - MessageGetResult::WantsRead => { - let n = session.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - MessageGetResult::WantsWrite(bytes) => { - session.stream.write_all(&bytes)?; - } - MessageGetResult::Err(err) => bail!("{err}"), - } - }; - - return emit(printer, raw, self.raw); - } + if self.raw { + let mut out = stdout().lock(); + out.write_all(&raw)?; + return Ok(()); } - #[cfg(feature = "jmap")] - if backend.allows_jmap() { - if let Some(jmap_config) = account_config.jmap.take() { - use io_email::jmap::message_get::{MessageGet, MessageGetResult}; - use pimalaya_toolbox::stream::jmap::JmapSession; + let Some(parsed) = MessageParser::new().parse(&raw) else { + bail!("Failed to parse RFC 5322 message"); + }; - let account = Account::new(config, account_config, jmap_config)?; - let mut session = JmapSession::new( - account.backend.server.clone(), - account.backend.tls.clone().try_into()?, - account.backend.auth.clone().try_into()?, - )?; - let mut coroutine = - MessageGet::new(&session.session, &session.http_auth, &self.id)?; - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - let raw = loop { - match coroutine.resume(arg.take()) { - MessageGetResult::Ok(raw) => break raw, - MessageGetResult::WantsRead => { - let n = session.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - MessageGetResult::WantsWrite(bytes) => { - session.stream.write_all(&bytes)?; - } - MessageGetResult::Err(err) => bail!("{err}"), - } - }; - - return emit(printer, raw, self.raw); - } - } - - #[cfg(feature = "maildir")] - if backend.allows_maildir() { - if let Some(maildir_config) = account_config.maildir.take() { - use io_email::maildir::message_get::{MessageGet, MessageGetArg, MessageGetResult}; - use io_maildir::maildir::Maildir; - - let account = Account::new(config, account_config, maildir_config)?; - let path = account.backend.root.join(&self.mailbox); - let maildir = Maildir::try_from(path)?; - - let mut coroutine = MessageGet::new(maildir, &self.id); - let mut arg: Option = None; - - let raw = loop { - match coroutine.resume(arg.take()) { - MessageGetResult::Ok(raw) => break raw, - MessageGetResult::WantsDirRead(paths) => { - arg = Some(MessageGetArg::DirRead(read_dirs(&paths)?)); - } - MessageGetResult::WantsFileRead(paths) => { - arg = Some(MessageGetArg::FileRead(read_files(&paths)?)); - } - MessageGetResult::Err(err) => bail!("{err}"), - } - }; - - return emit(printer, raw, self.raw); - } - } - - bail!("no backend matching `{backend}` is configured for this account") + printer.out(MessageView(parsed.into_owned())) } } -#[cfg(any(feature = "imap", feature = "jmap", feature = "maildir"))] -fn emit(printer: &mut impl Printer, raw: Vec, raw_mode: bool) -> Result<()> { - if raw_mode { - let mut out = stdout().lock(); - out.write_all(&raw)?; - return Ok(()); - } - - let Some(parsed) = MessageParser::new().parse(&raw) else { - bail!("Failed to parse RFC 5322 message"); - }; - - printer.out(MessageView(parsed.into_owned())) -} - -#[cfg(feature = "maildir")] -fn read_dirs(paths: &BTreeSet) -> Result>> { - use std::fs; - - let mut out = BTreeMap::new(); - - for path in paths { - let mut entries = BTreeSet::new(); - - for entry in fs::read_dir(path)? { - let entry = entry?; - - if let Some(s) = entry.path().to_str() { - entries.insert(s.to_owned()); - } - } - - out.insert(path.clone(), entries); - } - - Ok(out) -} - -#[cfg(feature = "maildir")] -fn read_files(paths: &BTreeSet) -> Result>> { - use std::fs; - - let mut out = BTreeMap::new(); - - for path in paths { - out.insert(path.clone(), fs::read(path)?); - } - - Ok(out) -} - -#[cfg(any(feature = "imap", feature = "jmap", feature = "maildir"))] #[derive(Serialize)] #[serde(transparent)] pub struct MessageView(Message<'static>); -#[cfg(any(feature = "imap", feature = "jmap", feature = "maildir"))] impl fmt::Display for MessageView { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for header in self.0.headers() { diff --git a/src/messages/mod.rs b/src/messages/mod.rs index 6849055a..968652e2 100644 --- a/src/messages/mod.rs +++ b/src/messages/mod.rs @@ -1,5 +1,5 @@ pub mod add; -pub mod command; +pub mod cli; pub mod compose; pub mod copy; #[cfg(any(feature = "imap", feature = "jmap", feature = "maildir"))] diff --git a/src/messages/mv.rs b/src/messages/mv.rs index 92ec837c..1ff44bef 100644 --- a/src/messages/mv.rs +++ b/src/messages/mv.rs @@ -3,7 +3,7 @@ use std::io::{Read, Write}; use anyhow::{bail, Result}; use clap::Parser; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::{ account::Account, @@ -53,9 +53,9 @@ impl MessagesMoveCommand { #[cfg(feature = "imap")] if backend.allows_imap() { if let Some(imap_config) = account_config.imap.take() { + use crate::imap::session::ImapSession; use io_email::imap::message_move::{MessageMove, MessageMoveResult}; use io_imap::types::{mailbox::Mailbox, sequence::SequenceSet}; - use pimalaya_toolbox::stream::imap::ImapSession; let account = Account::new(config, account_config, imap_config)?; let mut session = ImapSession::new( @@ -93,8 +93,8 @@ impl MessagesMoveCommand { #[cfg(feature = "jmap")] if backend.allows_jmap() { if let Some(jmap_config) = account_config.jmap.take() { + use crate::jmap::session::JmapSession; use io_email::jmap::message_move::{MessageMove, MessageMoveResult}; - use pimalaya_toolbox::stream::jmap::JmapSession; let account = Account::new(config, account_config, jmap_config)?; let mut session = JmapSession::new( diff --git a/src/messages/send.rs b/src/messages/send.rs index c72ce3b1..4b608acf 100644 --- a/src/messages/send.rs +++ b/src/messages/send.rs @@ -4,7 +4,7 @@ use std::io::{Read, Write}; use anyhow::{bail, Result}; use clap::Parser; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::{ cli::BackendArg, @@ -52,7 +52,7 @@ impl MessagesSendCommand { if backend.allows_smtp() { if let Some(smtp_config) = account_config.smtp.take() { use io_email::smtp::message_send::{MessageSend, MessageSendResult}; - use pimalaya_toolbox::stream::smtp::SmtpSession; + use pimalaya_stream::std::smtp::SmtpSession; let account = crate::account::Account::new(config, account_config, smtp_config)?; let mut session = SmtpSession::new( @@ -88,8 +88,8 @@ impl MessagesSendCommand { #[cfg(feature = "jmap")] if backend.allows_jmap() { if let Some(jmap_config) = account_config.jmap.take() { + use crate::jmap::session::JmapSession; use io_email::jmap::message_send::{MessageSend, MessageSendResult}; - use pimalaya_toolbox::stream::jmap::JmapSession; let identity_id = jmap_config.identity_id.clone().ok_or_else(|| { anyhow::anyhow!( diff --git a/src/smtp/account.rs b/src/smtp/account.rs index 34eb5bc5..47a89e13 100644 --- a/src/smtp/account.rs +++ b/src/smtp/account.rs @@ -1,17 +1,23 @@ use anyhow::Result; -use pimalaya_toolbox::stream::smtp::SmtpSession; +use io_smtp::client::SmtpClient; +use pimalaya_stream::std::smtp::SmtpSession; use crate::{account::Account, config::SmtpConfig}; pub type SmtpAccount = Account; impl SmtpAccount { - pub fn new_smtp_session(&self) -> Result { - SmtpSession::new( + /// Opens the SMTP connection (TCP/TLS/STARTTLS, greeting, EHLO, + /// SASL), then hands the established stream off to a fresh + /// [`SmtpClient`]. SMTP send is stateless after auth, so no + /// session context needs to follow the stream. + pub fn new_smtp_client(&self) -> Result { + let session = SmtpSession::new( self.backend.url.clone(), self.backend.tls.clone().try_into()?, self.backend.starttls, self.backend.sasl.clone().try_into()?, - ) + )?; + Ok(SmtpClient::new(session.stream)) } } diff --git a/src/smtp/command.rs b/src/smtp/cli.rs similarity index 83% rename from src/smtp/command.rs rename to src/smtp/cli.rs index 19e8089f..8431f289 100644 --- a/src/smtp/command.rs +++ b/src/smtp/cli.rs @@ -1,8 +1,8 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; -use crate::smtp::{account::SmtpAccount, message::command::SmtpMessageCommand}; +use crate::smtp::{account::SmtpAccount, message::cli::SmtpMessageCommand}; /// SMTP CLI (requires `smtp` cargo feature). /// diff --git a/src/smtp/message/command.rs b/src/smtp/message/cli.rs similarity index 92% rename from src/smtp/message/command.rs rename to src/smtp/message/cli.rs index 4f79b9ba..7d73c5e0 100644 --- a/src/smtp/message/command.rs +++ b/src/smtp/message/cli.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Subcommand; -use pimalaya_toolbox::terminal::printer::Printer; +use pimalaya_cli::printer::Printer; use crate::smtp::{account::SmtpAccount, message::send::SmtpMessageSendCommand}; diff --git a/src/smtp/message/mod.rs b/src/smtp/message/mod.rs index 3bfb26e5..1a641f63 100644 --- a/src/smtp/message/mod.rs +++ b/src/smtp/message/mod.rs @@ -1,2 +1,2 @@ -pub mod command; +pub mod cli; pub mod send; diff --git a/src/smtp/message/send.rs b/src/smtp/message/send.rs index 91c137aa..d1e617f2 100644 --- a/src/smtp/message/send.rs +++ b/src/smtp/message/send.rs @@ -1,25 +1,20 @@ use std::{ borrow::Cow, collections::HashSet, - io::{stdin, BufRead, IsTerminal, Read, Write}, + io::{stdin, BufRead, IsTerminal}, }; use anyhow::{bail, Result}; use clap::Parser; -use io_smtp::{ - rfc5321::types::{ - domain::Domain, ehlo_domain::EhloDomain, forward_path::ForwardPath, local_part::LocalPart, - mailbox::Mailbox, reverse_path::ReversePath, - }, - send::*, +use io_smtp::rfc5321::types::{ + domain::Domain, ehlo_domain::EhloDomain, forward_path::ForwardPath, local_part::LocalPart, + mailbox::Mailbox, reverse_path::ReversePath, }; use mail_parser::{Addr, Address, HeaderName, HeaderValue, MessageParser}; -use pimalaya_toolbox::terminal::printer::{Message, Printer}; +use pimalaya_cli::printer::{Message, Printer}; use crate::smtp::account::SmtpAccount; -const READ_BUFFER_SIZE: usize = 8 * 1024; - /// Send a message to a mailbox. /// /// This command appends a message to the specified mailbox. The @@ -34,7 +29,7 @@ pub struct SmtpMessageSendCommand { impl SmtpMessageSendCommand { pub fn execute(self, printer: &mut impl Printer, account: SmtpAccount) -> Result<()> { - let mut smtp = account.new_smtp_session()?; + let mut client = account.new_smtp_client()?; let message = if stdin().is_terminal() || printer.is_json() { self.message @@ -52,24 +47,7 @@ impl SmtpMessageSendCommand { let (reverse_path, forward_paths) = into_smtp_msg(message.as_bytes())?; - let mut coroutine = SmtpMessageSend::new(reverse_path, forward_paths, message.into_bytes()); - let mut buf = [0u8; READ_BUFFER_SIZE]; - let mut arg: Option<&[u8]> = None; - - loop { - match coroutine.resume(arg.take()) { - SmtpMessageSendResult::Ok => break, - SmtpMessageSendResult::WantsRead => { - let n = smtp.stream.read(&mut buf)?; - arg = Some(&buf[..n]); - } - SmtpMessageSendResult::WantsWrite(bytes) => { - smtp.stream.write_all(&bytes)?; - arg = None; - } - SmtpMessageSendResult::Err(err) => bail!("{err}"), - } - } + client.send(reverse_path, forward_paths, message.into_bytes())?; printer.out(Message::new("Message successfully sent")) } diff --git a/src/smtp/mod.rs b/src/smtp/mod.rs index 306628f0..d29d63d8 100644 --- a/src/smtp/mod.rs +++ b/src/smtp/mod.rs @@ -1,3 +1,3 @@ pub mod account; -pub mod command; +pub mod cli; pub mod message; diff --git a/src/wizard.rs b/src/wizard.rs new file mode 100644 index 00000000..7229e4fb --- /dev/null +++ b/src/wizard.rs @@ -0,0 +1,231 @@ +//! Interactive configuration wizard. +//! +//! Triggered by `cli::load_or_wizard` when no config file is found +//! ([`pimalaya_config::toml::TomlConfig::from_paths_or_default`] +//! returned `Ok(None)`). +//! +//! Flow: +//! +//! 1. Confirm with the user. Exit if they decline. +//! 2. Ask for an account name and email address. +//! 3. Run discovery — currently PACC only; Mozilla Autoconfig will +//! join it once `io-discovery`'s `autoconfig` feature builds again. +//! Both probes will then run in parallel via `std::thread::scope`, +//! PACC results preferred. +//! 4. Convert any discovery hit into [`WizardImapConfig`] / +//! [`WizardSmtpConfig`] defaults, hand them to the per-protocol +//! wizards in [`pimalaya_cli::wizard`]. +//! 5. Build a [`Config`], write it to `target`, return it. + +use std::{collections::HashMap, path::Path, process::exit, thread}; + +use anyhow::{anyhow, bail, Result}; +use io_discovery::pacc::{ + client::{DiscoveryPaccClient, DiscoveryPaccClientError}, + types::PaccConfig, +}; +use io_process::command::Command; +use log::{debug, info}; +use pimalaya_cli::wizard::{ + imap::{ + self as imap_wizard, Encryption as ImapEncryption, ImapAuth, ImapSecret, WizardImapConfig, + }, + smtp::{ + self as smtp_wizard, Encryption as SmtpEncryption, SmtpAuth, SmtpSecret, WizardSmtpConfig, + }, +}; +use pimalaya_config::secret::Secret; +use url::Url; + +use crate::config::{ + AccountConfig, Config, ImapConfig, SaslConfig, SaslMechanismConfig, SaslPlainConfig, SmtpConfig, +}; + +/// DNS resolver used by PACC discovery. Cloudflare's `1.1.1.1` is a +/// reasonable default; we'll make this configurable later. +const DEFAULT_RESOLVER: &str = "tcp://1.1.1.1:53"; + +pub fn run_or_exit(target: &Path) -> Result { + let prompt = format!( + "No configuration found. Create one at {}?", + target.display(), + ); + + if !pimalaya_cli::prompt::bool(&prompt, true)? { + exit(0); + } + + let account_name = pimalaya_cli::prompt::text("Account name:", Some("default"))?; + let email = pimalaya_cli::prompt::text::<&str>("Email address:", None)?; + + let (local_part, domain) = email + .split_once('@') + .ok_or_else(|| anyhow!("Invalid email address `{email}`: missing `@`"))?; + + info!("Discovering provider settings for {domain}…"); + let (imap_defaults, smtp_defaults) = discover(domain); + + let imap = imap_wizard::run(&account_name, local_part, domain, imap_defaults.as_ref())?; + let smtp = smtp_wizard::run(&account_name, local_part, domain, smtp_defaults.as_ref())?; + + let account = AccountConfig { + default: true, + downloads_dir: None, + table_preset: None, + table_arrangement: None, + imap: Some(imap_to_config(imap)?), + jmap: None, + maildir: None, + smtp: Some(smtp_to_config(smtp)?), + }; + + let config = Config { + downloads_dir: None, + table_preset: None, + table_arrangement: None, + accounts: HashMap::from([(account_name, account)]), + }; + + config.write(target)?; + info!("Configuration written to {}.", target.display()); + + Ok(config) +} + +/// Runs configured discovery probes in parallel and returns the +/// merged IMAP/SMTP defaults. Currently PACC-only; Mozilla Autoconfig +/// will join once io-discovery's `autoconfig` feature compiles. +fn discover(domain: &str) -> (Option, Option) { + thread::scope(|scope| { + let pacc = scope.spawn(|| run_pacc(domain)); + + let pacc = pacc.join().unwrap_or_else(|_| { + debug!("PACC discovery thread panicked"); + None + }); + + match pacc { + Some(config) => pacc_defaults(&config), + None => (None, None), + } + }) +} + +fn run_pacc(domain: &str) -> Option { + let resolver: Url = match DEFAULT_RESOLVER.parse() { + Ok(url) => url, + Err(err) => { + debug!("PACC: invalid default resolver `{DEFAULT_RESOLVER}`: {err}"); + return None; + } + }; + + let mut client = DiscoveryPaccClient::new(resolver); + match client.discover(domain) { + Ok(config) => Some(config), + Err(DiscoveryPaccClientError::Discovery(err)) => { + debug!("PACC discovery for {domain} failed: {err}"); + None + } + Err(err) => { + debug!("PACC transport error for {domain}: {err}"); + None + } + } +} + +fn pacc_defaults(config: &PaccConfig) -> (Option, Option) { + let imap = config.protocols.imap.as_ref().map(|p| WizardImapConfig { + host: p.host.clone(), + port: 993, + encryption: ImapEncryption::Tls, + login: String::new(), + // Placeholder; the user picks their real auth in the wizard. + // Only the host/port/encryption fields are read as defaults. + auth: ImapAuth::Password(ImapSecret::Raw(String::new().into())), + }); + + let smtp = config.protocols.smtp.as_ref().map(|p| WizardSmtpConfig { + host: p.host.clone(), + port: 465, + encryption: SmtpEncryption::Tls, + login: String::new(), + auth: SmtpAuth::Password(SmtpSecret::Raw(String::new().into())), + }); + + (imap, smtp) +} + +fn imap_to_config(w: WizardImapConfig) -> Result { + let scheme = match w.encryption { + ImapEncryption::Tls => "imaps", + ImapEncryption::StartTls | ImapEncryption::None => "imap", + }; + let url = Url::parse(&format!("{scheme}://{}:{}", w.host, w.port))?; + let starttls = matches!(w.encryption, ImapEncryption::StartTls); + let sasl = build_sasl_imap(&w.login, w.auth)?; + + Ok(ImapConfig { + url, + tls: Default::default(), + starttls, + sasl, + }) +} + +fn smtp_to_config(w: WizardSmtpConfig) -> Result { + let scheme = match w.encryption { + SmtpEncryption::Tls => "smtps", + SmtpEncryption::StartTls | SmtpEncryption::None => "smtp", + }; + let url = Url::parse(&format!("{scheme}://{}:{}", w.host, w.port))?; + let starttls = matches!(w.encryption, SmtpEncryption::StartTls); + let sasl = build_sasl_smtp(&w.login, w.auth)?; + + Ok(SmtpConfig { + url, + tls: Default::default(), + starttls, + sasl, + }) +} + +fn build_sasl_imap(login: &str, auth: ImapAuth) -> Result { + let ImapAuth::Password(secret) = auth; + let passwd = match secret { + ImapSecret::Raw(s) => Secret::Raw(s), + ImapSecret::Command(cmd) => Secret::Command(parse_cmd(&cmd)?), + }; + + Ok(plain_sasl(login, passwd)) +} + +fn build_sasl_smtp(login: &str, auth: SmtpAuth) -> Result { + let SmtpAuth::Password(secret) = auth; + let passwd = match secret { + SmtpSecret::Raw(s) => Secret::Raw(s), + SmtpSecret::Command(cmd) => Secret::Command(parse_cmd(&cmd)?), + }; + + Ok(plain_sasl(login, passwd)) +} + +fn plain_sasl(login: &str, passwd: Secret) -> SaslConfig { + SaslConfig { + mechanism: Some(SaslMechanismConfig::Plain), + login: None, + plain: Some(SaslPlainConfig { + authzid: None, + authcid: login.to_owned(), + passwd, + }), + anonymous: None, + } +} + +fn parse_cmd(cmd: &str) -> Result { + if cmd.trim().is_empty() { + bail!("Empty shell command for secret"); + } + Ok(Command::new(cmd)) +}