From cec658aff4bb764b507683fcb125fa3de4fea5a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 25 Nov 2023 12:37:00 +0100 Subject: [PATCH 01/29] bump lib with backend features --- Cargo.lock | 2730 +++++++++++++++++---------- Cargo.toml | 16 +- config.sample.toml | 60 +- src/backend.rs | 237 +++ src/cache/id_mapper.rs | 73 +- src/config/config.rs | 68 +- src/config/prelude.rs | 221 ++- src/domain/account/accounts.rs | 45 +- src/domain/account/config.rs | 223 +-- src/domain/account/handlers.rs | 137 +- src/domain/account/wizard.rs | 6 +- src/domain/backend/imap/handlers.rs | 1 - src/domain/backend/imap/mod.rs | 4 +- src/domain/backend/maildir/mod.rs | 2 +- src/domain/backend/mod.rs | 2 +- src/domain/email/handlers.rs | 196 +- src/domain/flag/handlers.rs | 25 +- src/domain/folder/handlers.rs | 23 +- src/domain/sender/mod.rs | 2 +- src/domain/sender/sendmail/mod.rs | 2 +- src/domain/sender/smtp/mod.rs | 2 +- src/domain/sender/wizard.rs | 1 - src/domain/tpl/handlers.rs | 35 +- src/lib.rs | 1 + src/main.rs | 301 ++- src/ui/editor.rs | 14 +- 26 files changed, 2622 insertions(+), 1805 deletions(-) create mode 100644 src/backend.rs diff --git a/Cargo.lock b/Cargo.lock index 1b1f4de..b3bfaa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -26,9 +35,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher", @@ -37,41 +46,22 @@ dependencies = [ [[package]] name = "ahash" -version = "0.3.8" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" -dependencies = [ - "const-random", -] - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom 0.2.8", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", - "getrandom 0.2.8", + "getrandom", "once_cell", "version_check", + "zerocopy", ] [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] @@ -82,6 +72,18 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -92,31 +94,85 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.66" +name = "anstream" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" [[package]] name = "async-recursion" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.39", ] [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.39", ] [[package]] @@ -136,6 +192,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -156,9 +227,9 @@ checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" [[package]] name = "base64" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" @@ -180,9 +251,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.2.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "block-buffer" @@ -214,15 +285,24 @@ dependencies = [ [[package]] name = "bstr" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" dependencies = [ "memchr", - "regex-automata", + "regex-automata 0.4.3", "serde", ] +[[package]] +name = "btoi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd6407f73a9b8b6162d8a2ef999fe6afd7cc15902ebf42c5cd296addf17e0ad" +dependencies = [ + "num-traits", +] + [[package]] name = "buffer-redux" version = "1.0.0" @@ -247,27 +327,27 @@ checksum = "b00b8763668c99f8d9101b8a0dd82106f58265464531a79b2cef0d9a30c17dd2" [[package]] name = "bumpalo" -version = "3.11.1" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytecount" -version = "0.6.3" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" +checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "bzip2" @@ -311,11 +391,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.77" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -329,9 +410,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b40ccee03b5175c18cde8f37e7d2a33bcef6f8ec8f7cc0d81090d1bb380949c9" +checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" dependencies = [ "smallvec", "target-lexicon", @@ -343,49 +424,33 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "charset" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e9079d1a12a2cc2bffb5db039c43661836ead4082120d5844f02555aca2d46" -dependencies = [ - "base64 0.13.1", - "encoding_rs", -] - [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "serde", - "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets 0.48.5", ] [[package]] name = "chumsky" -version = "0.8.0" +version = "1.0.0-alpha.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d02796e4586c6c41aeb68eae9bfb4558a522c35f1430c14b40136c3706e09e4" +checksum = "8edacfd1c8ea3db7e11640b5444a1703baee4c3b8c21ffd0726e09a92ab9abe5" dependencies = [ - "ahash 0.3.8", -] - -[[package]] -name = "chumsky" -version = "1.0.0-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc3172a80699de358070dd99f80ea8badc6cdf8ac2417cb5a96e6d81bf5fe06d" -dependencies = [ - "hashbrown 0.13.2", + "hashbrown 0.14.2", + "regex-automata 0.3.9", + "serde", "stacker", + "unicode-ident", + "vergen", ] [[package]] @@ -400,96 +465,80 @@ dependencies = [ [[package]] name = "clap" -version = "4.1.4" +version = "4.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76" +checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" dependencies = [ - "bitflags 1.3.2", + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" +dependencies = [ + "anstream", + "anstyle", "clap_lex", - "is-terminal", "strsim 0.10.0", - "termcolor", ] [[package]] name = "clap_complete" -version = "4.0.6" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b3c9eae0de7bf8e3f904a5e40612b21fb2e2e566456d177809a48b892d24da" +checksum = "bffe91f06a11b4b9420f62103854e90867812cd5d01557f853c5ee8e791b12ae" dependencies = [ "clap", ] [[package]] name = "clap_lex" -version = "0.3.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" -dependencies = [ - "os_str_bytes", -] +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "clap_mangen" -version = "0.2.5" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e503c3058af0a0854668ea01db55c622482a080092fede9dd2e00a00a9436504" +checksum = "d3be86020147691e1d2ef58f75346a3d4d94807bfc473e377d52f09f0f7d77f7" dependencies = [ "clap", "roff", ] [[package]] -name = "codespan-reporting" -version = "0.11.1" +name = "clru" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] +checksum = "b8191fa7302e03607ff0e237d4246cc043ff5b3cb9409d995172ba3bea16b807" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "console" -version = "0.15.2" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" dependencies = [ "encode_unicode", "lazy_static", "libc", - "terminal_size", "unicode-width", - "winapi", + "windows-sys 0.45.0", ] [[package]] name = "const-oid" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747" - -[[package]] -name = "const-random" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" -dependencies = [ - "const-random-macro", - "proc-macro-hack", -] - -[[package]] -name = "const-random-macro" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" -dependencies = [ - "getrandom 0.2.8", - "once_cell", - "proc-macro-hack", - "tiny-keccak", -] +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" [[package]] name = "constant_time_eq" @@ -524,9 +573,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "coredump" @@ -539,9 +588,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -561,21 +610,11 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -584,40 +623,34 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.13" +version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "memoffset", + "memoffset 0.9.0", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.14" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - [[package]] name = "crypto-bigint" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "subtle", "zeroize", ] @@ -644,27 +677,14 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "3.2.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek" -version = "4.0.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest 0.10.6", + "digest", "fiat-crypto", "platforms", "rustc_version", @@ -674,13 +694,13 @@ dependencies = [ [[package]] name = "curve25519-dalek-derive" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.39", ] [[package]] @@ -689,50 +709,6 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" -[[package]] -name = "cxx" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 1.0.104", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.104", -] - [[package]] name = "darling" version = "0.10.2" @@ -764,7 +740,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.9.3", - "syn 1.0.104", + "syn 1.0.109", ] [[package]] @@ -778,7 +754,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 1.0.104", + "syn 1.0.109", ] [[package]] @@ -789,7 +765,7 @@ checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" dependencies = [ "darling_core 0.10.2", "quote", - "syn 1.0.104", + "syn 1.0.109", ] [[package]] @@ -800,26 +776,35 @@ checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ "darling_core 0.14.4", "quote", - "syn 1.0.104", + "syn 1.0.109", ] [[package]] name = "data-encoding" -version = "2.3.3" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "der" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7ed52955ce76b1554f509074bb357d3fb8ac9b51288a65a3fd480d1dfba946" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" dependencies = [ "const-oid", "pem-rfc7468", "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive_builder" version = "0.12.0" @@ -838,7 +823,7 @@ dependencies = [ "darling 0.14.4", "proc-macro2", "quote", - "syn 1.0.104", + "syn 1.0.109", ] [[package]] @@ -848,7 +833,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" dependencies = [ "derive_builder_core", - "syn 1.0.104", + "syn 1.0.109", ] [[package]] @@ -862,29 +847,21 @@ dependencies = [ [[package]] name = "dialoguer" -version = "0.10.2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92e7e37ecef6857fdc0c0c5d42fd5b0938e46590c2183cc92dd310a6d078eb1" +checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87" dependencies = [ "console", + "shell-words", "tempfile", "zeroize", ] [[package]] name = "digest" -version = "0.9.0" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "const-oid", @@ -898,7 +875,16 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" dependencies = [ - "dirs-sys", + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", ] [[package]] @@ -913,13 +899,31 @@ dependencies = [ ] [[package]] -name = "ecdsa" -version = "0.16.7" +name = "dirs-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0997c976637b606099b9985693efa3581e84e41f5c11ba5255f88711058ad428" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest 0.10.6", + "digest", "elliptic-curve", "rfc6979", "signature", @@ -928,9 +932,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "2.2.1" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", "signature", @@ -938,39 +942,40 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.0.0-rc.3" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa8e9049d5d72bfc12acbc05914731b5322f79b5e2f195e9f2d705fca22ab4c" +checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0" dependencies = [ - "curve25519-dalek 4.0.0-rc.3", + "curve25519-dalek", "ed25519", "serde", "sha2", + "subtle", "zeroize", ] [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" -version = "0.13.5" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest 0.10.6", + "digest", "ff", "generic-array", "group", "hkdf", "pem-rfc7468", "pkcs8", - "rand_core 0.6.4", + "rand_core", "sec1", "subtle", "zeroize", @@ -979,14 +984,13 @@ dependencies = [ [[package]] name = "email-lib" version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ea77debd2ffe4299a58f5a4718d13d1b97faef45555f9ec3cb3db74721076" dependencies = [ "advisory-lock", + "anyhow", "async-trait", "chrono", "convert_case", - "dirs", + "dirs 4.0.0", "futures", "imap", "imap-proto", @@ -997,29 +1001,28 @@ dependencies = [ "mail-send", "maildirpp", "md5", - "mml-lib", + "mml-lib 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "notmuch", - "oauth-lib", + "oauth-lib 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "once_cell", "ouroboros", "pgp-lib", "process-lib", "rayon", "regex", - "rfc2047-decoder", "rusqlite", - "rustls 0.21.1", + "rustls 0.21.9", "rustls-native-certs", "secret-lib", - "shellexpand-utils", + "shellexpand-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror", "tokio", - "tokio-rustls 0.24.0", + "tokio-rustls", "tree_magic_mini", "urlencoding", "utf7-imap", "uuid", - "webpki-roots 0.22.6", + "webpki-roots", ] [[package]] @@ -1039,23 +1042,23 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] [[package]] name = "enum-as-inner" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" dependencies = [ "heck", "proc-macro2", "quote", - "syn 1.0.104", + "syn 2.0.39", ] [[package]] @@ -1079,32 +1082,21 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.3.23" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54558e0ba96fbe24280072642eceb9d7d442e32c7ec0ea9e7ecd7b4ea2cf4e11" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" dependencies = [ "serde", ] [[package]] name = "errno" -version = "0.2.8" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", "libc", + "windows-sys 0.48.0", ] [[package]] @@ -1121,12 +1113,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "1.8.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" -dependencies = [ - "instant", -] +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "ff" @@ -1134,15 +1123,27 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ - "rand_core 0.6.4", + "rand_core", "subtle", ] [[package]] name = "fiat-crypto" -version = "0.1.20" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" + +[[package]] +name = "filetime" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "windows-sys 0.48.0", +] [[package]] name = "fixedbitset" @@ -1152,9 +1153,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", @@ -1168,9 +1169,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -1193,14 +1194,14 @@ dependencies = [ "darling 0.10.2", "proc-macro2", "quote", - "syn 1.0.104", + "syn 1.0.109", ] [[package]] name = "futures" -version = "0.3.25" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" dependencies = [ "futures-channel", "futures-core", @@ -1213,9 +1214,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.25" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -1223,15 +1224,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.25" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.25" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -1240,38 +1241,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.25" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-macro" -version = "0.3.25" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 1.0.104", + "syn 2.0.39", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.25" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-util" -version = "0.3.25" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-channel", "futures-core", @@ -1298,36 +1299,571 @@ dependencies = [ [[package]] name = "gethostname" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a329e22866dd78b35d2c639a4a23d7b950aeae300dfd79f4fb19f74055c2404" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "gix" +version = "0.43.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c256ea71cc1967faaefdaad15f334146b7c806f12460dcafd3afed845c8c78dd" +dependencies = [ + "gix-actor", + "gix-attributes", + "gix-config", + "gix-credentials", + "gix-date", + "gix-diff", + "gix-discover", + "gix-features 0.28.1", + "gix-glob", + "gix-hash 0.10.4", + "gix-hashtable", + "gix-index", + "gix-lock", + "gix-mailmap", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-prompt", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-sec", + "gix-tempfile", + "gix-traverse", + "gix-url", + "gix-validate", + "gix-worktree", + "log", + "once_cell", + "signal-hook", + "smallvec", + "thiserror", + "unicode-normalization", +] + +[[package]] +name = "gix-actor" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc22b0cdc52237667c301dd7cdc6ead8f8f73c9f824e9942c8ebd6b764f6c0bf" +dependencies = [ + "bstr", + "btoi", + "gix-date", + "itoa", + "nom", + "thiserror", +] + +[[package]] +name = "gix-attributes" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2231a25934a240d0a4b6f4478401c73ee81d8be52de0293eedbc172334abf3e1" +dependencies = [ + "bstr", + "gix-features 0.28.1", + "gix-glob", + "gix-path", + "gix-quote", + "thiserror", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ccab4bc576844ddb51b78d81b4a42d73e6229660fa614dfc3d3999c874d1959" +dependencies = [ + "thiserror", +] + +[[package]] +name = "gix-chunk" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b42ea64420f7994000130328f3c7a2038f639120518870436d31b8bde704493" +dependencies = [ + "thiserror", +] + +[[package]] +name = "gix-command" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c576cfbf577f72c097b5f88aedea502cd62952bdc1fb3adcab4531d5525a4c7" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-config" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbad5ce54a8fc997acc50febd89ec80fa6e97cb7f8d0654cb229936407489d8" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features 0.28.1", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "log", + "memchr", + "nom", + "once_cell", + "smallvec", + "thiserror", + "unicode-bom", +] + +[[package]] +name = "gix-config-value" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d09154c0c8677e4da0ec35e896f56ee3e338e741b9599fae06075edd83a4081c" +dependencies = [ + "bitflags 1.3.2", + "bstr", + "gix-path", + "libc", + "thiserror", +] + +[[package]] +name = "gix-credentials" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "750b684197374518ea057e0a0594713e07683faa0a3f43c0f93d97f64130ad8d" +dependencies = [ + "bstr", + "gix-command", + "gix-config-value", + "gix-path", + "gix-prompt", + "gix-sec", + "gix-url", + "thiserror", +] + +[[package]] +name = "gix-date" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96271912ce39822501616f177dea7218784e6c63be90d5f36322ff3a722aae2" +dependencies = [ + "bstr", + "itoa", + "thiserror", + "time", +] + +[[package]] +name = "gix-diff" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "103a0fa79b0d438f5ecb662502f052e530ace4fe1fe8e1c83c0c6da76d728e67" +dependencies = [ + "gix-hash 0.10.4", + "gix-object", + "imara-diff", + "thiserror", +] + +[[package]] +name = "gix-discover" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eba8ba458cb8f4a6c33409b0fe650b1258655175a7ffd1d24fafd3ed31d880b" +dependencies = [ + "bstr", + "dunce", + "gix-hash 0.10.4", + "gix-path", + "gix-ref", + "gix-sec", + "thiserror", +] + +[[package]] +name = "gix-features" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b76f9a80f6dd7be66442ae86e1f534effad9546676a392acc95e269d0c21c22" +dependencies = [ + "crc32fast", + "flate2", + "gix-hash 0.10.4", + "libc", + "once_cell", + "prodash", + "sha1_smol", + "thiserror", + "walkdir", +] + +[[package]] +name = "gix-features" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf69b0f5c701cc3ae22d3204b671907668f6437ca88862d355eaf9bc47a4f897" +dependencies = [ + "gix-hash 0.11.4", + "libc", +] + +[[package]] +name = "gix-fs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b37a1832f691fdc09910bd267f9a2e413737c1f9ec68c6e31f9e802616278a9" +dependencies = [ + "gix-features 0.29.0", +] + +[[package]] +name = "gix-glob" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e43efd776bc543f46f0fd0ca3d920c37af71a764a16f2aebd89765e9ff2993" +dependencies = [ + "bitflags 1.3.2", + "bstr", +] + +[[package]] +name = "gix-hash" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a258595457bc192d1f1c59d0d168a1e34e2be9b97a614e14995416185de41a7" +dependencies = [ + "hex", + "thiserror", +] + +[[package]] +name = "gix-hash" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b422ff2ad9a0628baaad6da468cf05385bf3f5ab495ad5a33cce99b9f41092f" +dependencies = [ + "hex", + "thiserror", +] + +[[package]] +name = "gix-hashtable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e55e40dfd694884f0eb78796c5bddcf2f8b295dace47039099dd7e76534973" +dependencies = [ + "gix-hash 0.10.4", + "hashbrown 0.13.2", + "parking_lot", +] + +[[package]] +name = "gix-index" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "717ab601ece7921f59fe86849dbe27d44a46ebb883b5885732c4f30df4996177" +dependencies = [ + "bitflags 1.3.2", + "bstr", + "btoi", + "filetime", + "gix-bitmap", + "gix-features 0.28.1", + "gix-hash 0.10.4", + "gix-lock", + "gix-object", + "gix-traverse", + "itoa", + "memmap2", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-lock" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c693d7f05730fa74a7c467150adc7cea393518410c65f0672f80226b8111555" +dependencies = [ + "gix-tempfile", + "gix-utils", + "thiserror", +] + +[[package]] +name = "gix-mailmap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b66aea5e52875cd4915f4957a6f4b75831a36981e2ec3f5fad9e370e444fe1a" +dependencies = [ + "bstr", + "gix-actor", + "thiserror", +] + +[[package]] +name = "gix-object" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df068db9180ee935fbb70504848369e270bdcb576b05c0faa8b9fd3b86fc017" +dependencies = [ + "bstr", + "btoi", + "gix-actor", + "gix-features 0.28.1", + "gix-hash 0.10.4", + "gix-validate", + "hex", + "itoa", + "nom", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-odb" +version = "0.43.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83af2e3e36005bfe010927f0dff41fb5acc3e3d89c6f1174135b3a34086bda2" +dependencies = [ + "arc-swap", + "gix-features 0.28.1", + "gix-hash 0.10.4", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "parking_lot", + "tempfile", + "thiserror", +] + +[[package]] +name = "gix-pack" +version = "0.33.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9401911c7fe032ad7b31c6a6b5be59cb283d1d6c999417a8215056efe6d635f3" +dependencies = [ + "clru", + "gix-chunk", + "gix-diff", + "gix-features 0.28.1", + "gix-hash 0.10.4", + "gix-hashtable", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-traverse", + "memmap2", + "parking_lot", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-path" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32370dce200bb951df013e03dff35b4233fc7a89458642b047629b91734a7e19" +dependencies = [ + "bstr", + "thiserror", +] + +[[package]] +name = "gix-prompt" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3034d4d935aef2c7bf719aaa54b88c520e82413118d886ae880a31d5bdee57" +dependencies = [ + "gix-command", + "gix-config-value", + "nix", + "parking_lot", + "thiserror", +] + +[[package]] +name = "gix-quote" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "475c86a97dd0127ba4465fbb239abac9ea10e68301470c9791a6dd5351cdc905" +dependencies = [ + "bstr", + "btoi", + "thiserror", +] + +[[package]] +name = "gix-ref" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e909396ed3b176823991ccc391c276ae2a015e54edaafa3566d35123cfac9d" +dependencies = [ + "gix-actor", + "gix-features 0.28.1", + "gix-hash 0.10.4", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-validate", + "memmap2", + "nom", + "thiserror", +] + +[[package]] +name = "gix-refspec" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aba332462bda2e8efeae4302b39a6ed01ad56ef772fd5b7ef197cf2798294d65" +dependencies = [ + "bstr", + "gix-hash 0.10.4", + "gix-revision", + "gix-validate", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-revision" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6f6ff53f888858afc24bf12628446a14279ceec148df6194481f306f553ad2" +dependencies = [ + "bstr", + "gix-date", + "gix-hash 0.10.4", + "gix-hashtable", + "gix-object", + "thiserror", +] + +[[package]] +name = "gix-sec" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ffa5bf0772f9b01de501c035b6b084cf9b8bb07dec41e3afc6a17336a65f47" +dependencies = [ + "bitflags 1.3.2", + "dirs 4.0.0", + "gix-path", "libc", "windows", ] [[package]] -name = "getrandom" -version = "0.1.16" +name = "gix-tempfile" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +checksum = "d71a0d32f34e71e86586124225caefd78dabc605d0486de580d717653addf182" dependencies = [ - "cfg-if", + "gix-fs", "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "once_cell", + "parking_lot", + "signal-hook", + "signal-hook-registry", + "tempfile", ] [[package]] -name = "getrandom" -version = "0.2.8" +name = "gix-traverse" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "dd9a4a07bb22168dc79c60e1a6a41919d198187ca83d8a5940ad8d7122a45df3" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", + "gix-hash 0.10.4", + "gix-hashtable", + "gix-object", + "thiserror", +] + +[[package]] +name = "gix-url" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a22b4b32ad14d68f7b7fb6458fa58d44b01797d94c1b8f4db2d9c7b3c366b5" +dependencies = [ + "bstr", + "gix-features 0.28.1", + "gix-path", + "home", + "thiserror", + "url", +] + +[[package]] +name = "gix-utils" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85d89dc728613e26e0ed952a19583744e7f5240fcd4aa30d6c824ffd8b52f0f" +dependencies = [ + "fastrand", +] + +[[package]] +name = "gix-validate" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba9b3737b2cef3dcd014633485f0034b0f1a931ee54aeb7d8f87f177f3c89040" +dependencies = [ + "bstr", + "thiserror", +] + +[[package]] +name = "gix-worktree" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ec9a000b4f24af706c3cc680c7cda235656cbe3216336522f5692773b8a301" +dependencies = [ + "bstr", + "gix-attributes", + "gix-features 0.28.1", + "gix-glob", + "gix-hash 0.10.4", + "gix-index", + "gix-object", + "gix-path", + "io-close", + "thiserror", ] [[package]] @@ -1352,7 +1888,7 @@ dependencies = [ "gpg-error", "gpgme-sys", "libc", - "memoffset", + "memoffset 0.7.1", "once_cell", "smallvec", "static_assertions", @@ -1368,7 +1904,7 @@ dependencies = [ "libc", "libgpg-error-sys", "system-deps", - "winreg", + "winreg 0.10.1", ] [[package]] @@ -1378,15 +1914,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core 0.6.4", + "rand_core", "subtle", ] [[package]] name = "h2" -version = "0.3.18" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", @@ -1394,7 +1930,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.2", + "indexmap", "slab", "tokio", "tokio-util", @@ -1406,32 +1942,30 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.6", -] [[package]] name = "hashbrown" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash 0.8.3", -] [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "hashlink" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.12.3", + "hashbrown 0.14.2", ] [[package]] @@ -1451,12 +1985,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -1464,6 +1995,57 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-proto" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091a6fbccf4860009355e3efc52ff4acf37a63489aad7435372d44ceeb6fbbcf" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.4.0", + "ipnet", + "once_cell", + "rand", + "ring 0.16.20", + "rustls 0.21.9", + "rustls-pemfile", + "thiserror", + "tinyvec", + "tokio", + "tokio-rustls", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b8f021164e6a984c9030023544c57789c51760065cd510572fedcfb04164e8" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot", + "rand", + "resolv-conf", + "rustls 0.21.9", + "smallvec", + "thiserror", + "tokio", + "tokio-rustls", + "tracing", +] + [[package]] name = "himalaya" version = "0.9.0" @@ -1478,7 +2060,7 @@ dependencies = [ "console", "coredump", "dialoguer", - "dirs", + "dirs 4.0.0", "email-lib", "email_address", "env_logger", @@ -1487,21 +2069,21 @@ dependencies = [ "keyring-lib", "log", "md5", - "mml-lib", - "oauth-lib", + "mml-lib 1.0.1", + "oauth-lib 0.1.0", "once_cell", "process-lib", "rusqlite", "secret-lib", "serde", "serde_json", - "shellexpand-utils", + "shellexpand-utils 0.1.0", "tempfile", "termcolor", "terminal_size", "tokio", - "toml", - "toml_edit", + "toml 0.7.8", + "toml_edit 0.19.15", "unicode-width", "url", "uuid", @@ -1522,7 +2104,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.6", + "digest", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", ] [[package]] @@ -1538,9 +2129,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -1566,9 +2157,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" @@ -1578,9 +2169,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.26" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -1593,7 +2184,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -1602,55 +2193,41 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.2" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" -dependencies = [ - "http", - "hyper", - "rustls 0.20.8", - "tokio", - "tokio-rustls 0.23.4", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http", "hyper", "log", - "rustls 0.21.1", + "rustls 0.21.9", "rustls-native-certs", "tokio", - "tokio-rustls 0.24.0", + "tokio-rustls", ] [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows-core", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] @@ -1670,20 +2247,19 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.2.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1695,7 +2271,7 @@ version = "3.0.0-alpha.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cceec1222cd3c9b196695fe296dc6ddaa617e06b0c49742140ff9bbc87af628" dependencies = [ - "base64 0.21.0", + "base64 0.21.5", "bufstream", "chrono", "imap-proto", @@ -1716,32 +2292,33 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "1.9.2" +name = "imara-diff" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "e98c1d0ad70fc91b8b9654b1f33db55e59579d3b3de2bffdced0fdb810570cb8" dependencies = [ - "autocfg", + "ahash", "hashbrown 0.12.3", ] [[package]] name = "indexmap" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.2", ] [[package]] name = "indicatif" -version = "0.17.3" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" +checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" dependencies = [ "console", + "instant", "number_prefix", "portable-atomic", "unicode-width", @@ -1766,65 +2343,53 @@ dependencies = [ ] [[package]] -name = "io-lifetimes" -version = "1.0.3" +name = "io-close" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc" dependencies = [ "libc", - "windows-sys 0.42.0", + "winapi", ] [[package]] name = "ipconfig" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.5", "widestring", - "winapi", - "winreg", + "windows-sys 0.48.0", + "winreg 0.50.0", ] [[package]] name = "ipnet" -version = "2.7.2" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" - -[[package]] -name = "is-terminal" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" -dependencies = [ - "hermit-abi 0.2.6", - "io-lifetimes", - "rustix", - "windows-sys 0.42.0", -] +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" dependencies = [ "wasm-bindgen", ] @@ -1840,9 +2405,9 @@ dependencies = [ [[package]] name = "keyring" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04ac4b8b0884cdf23c4619d139acf43839eac4f0739b92980c2a6d460d9c84f5" +checksum = "9549a129bd08149e0a71b2d1ce2729780d47127991bfd0a78cc1df697ec72492" dependencies = [ "byteorder", "lazy_static", @@ -1868,14 +2433,14 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" dependencies = [ - "spin", + "spin 0.5.2", ] [[package]] name = "libc" -version = "0.2.137" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libgpg-error-sys" @@ -1885,14 +2450,25 @@ checksum = "c97079310f39c835d3bd73578379d040f779614bb331c7ffbb6630fee6420290" dependencies = [ "build-rs", "system-deps", - "winreg", + "winreg 0.10.1", ] [[package]] name = "libm" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall 0.4.1", +] [[package]] name = "libsqlite3-sys" @@ -1905,15 +2481,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "link-cplusplus" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" -dependencies = [ - "cc", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1932,15 +2499,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -1948,12 +2515,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "lru-cache" @@ -1966,48 +2530,48 @@ dependencies = [ [[package]] name = "mail-auth" -version = "0.3.2" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6b0969bac270a60560d3a6f89c812b3e39b2f4b3ca8d866063f482030d14f19" +checksum = "ab6524549b92c6f1250ddefe10f67bcedb9de0674a026b21e88dc1958437f8a6" dependencies = [ - "ahash 0.8.3", + "ahash", "flate2", + "hickory-resolver", "lru-cache", "mail-builder", "mail-parser", "parking_lot", "quick-xml", - "ring", + "ring 0.17.5", "rustls-pemfile", "serde", "serde_json", - "trust-dns-resolver", "zip", ] [[package]] name = "mail-builder" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765969f4385f88a62738e8ed63e2fa630571d7ed6fd96ca6932d699513dd8c28" +checksum = "ef70f53409852d2612f2249810cbbe0c9931ca25b739b734bafc7f61d88051d4" dependencies = [ "gethostname", ] [[package]] name = "mail-parser" -version = "0.8.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4158a1c18963244e083888b21465846dfb68d6170850ed1ab4742edd57c9d47" +checksum = "0e51d275d90a41584d12ea25fc4a7d7d00236a4718308b994ed13a7ff90a6985" dependencies = [ "encoding_rs", ] [[package]] name = "mail-send" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6d2b8d0cb56f199d36f527ff96453cf3b1cdfdacb5e4d154ac1d8fcd89873c2" +checksum = "4cbc58a799366b3b2956a2c5ae7e2892ea34b3016343cbbdc5deb844aa6c0973" dependencies = [ "base64 0.20.0", "gethostname", @@ -2015,11 +2579,11 @@ dependencies = [ "mail-builder", "md5", "rand", - "rustls 0.21.1", + "rustls 0.21.9", "smtp-proto", "tokio", - "tokio-rustls 0.24.0", - "webpki-roots 0.23.1", + "tokio-rustls", + "webpki-roots", ] [[package]] @@ -2039,19 +2603,14 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "md-5" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ - "digest 0.10.6", + "cfg-if", + "digest", ] [[package]] @@ -2062,9 +2621,18 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] [[package]] name = "memoffset" @@ -2076,10 +2644,19 @@ dependencies = [ ] [[package]] -name = "mime" -version = "0.3.16" +name = "memoffset" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" @@ -2098,24 +2675,38 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.42.0", + "wasi", + "windows-sys 0.48.0", ] [[package]] name = "mml-lib" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3d0041c291179f654d5e256a8473415dfea8fc055e8c51358a3c99dac2cf9e" +version = "1.0.1" dependencies = [ "async-recursion", - "chumsky 1.0.0-alpha.4", + "chumsky", + "log", + "mail-builder", + "mail-parser", + "nanohtml2text", + "shellexpand-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror", + "tree_magic_mini", +] + +[[package]] +name = "mml-lib" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915efb96feb76c123001341d1631dc06c9bbe14c1a249ef709cf84b341eb8295" +dependencies = [ + "async-recursion", + "chumsky", "gpgme", "keyring-lib", "log", @@ -2125,7 +2716,7 @@ dependencies = [ "pgp-lib", "process-lib", "secret-lib", - "shellexpand-utils", + "shellexpand-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror", "tree_magic_mini", ] @@ -2137,10 +2728,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "999681fe3c0524336e98ece1c25ee4278607f25cc1e361ad0f9201c8bf56dc2c" [[package]] -name = "nom" -version = "7.1.1" +name = "nix" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", @@ -2176,13 +2778,13 @@ dependencies = [ [[package]] name = "num-derive" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" dependencies = [ "proc-macro2", "quote", - "syn 1.0.104", + "syn 2.0.39", ] [[package]] @@ -2208,9 +2810,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", "libm", @@ -2218,11 +2820,20 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.3", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" dependencies = [ - "hermit-abi 0.2.6", "libc", ] @@ -2232,6 +2843,18 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "oauth-lib" +version = "0.1.0" +dependencies = [ + "log", + "oauth2", + "reqwest", + "thiserror", + "tokio", + "url", +] + [[package]] name = "oauth-lib" version = "0.1.0" @@ -2248,13 +2871,13 @@ dependencies = [ [[package]] name = "oauth2" -version = "4.3.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeaf26a72311c087f8c5ba617c96fac67a5c04f430e716ac8d8ab2de62e23368" +checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" dependencies = [ "base64 0.13.1", "chrono", - "getrandom 0.2.8", + "getrandom", "http", "rand", "reqwest", @@ -2267,10 +2890,19 @@ dependencies = [ ] [[package]] -name = "once_cell" -version = "1.16.0" +name = "object" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "openssl-probe" @@ -2279,19 +2911,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] -name = "os_str_bytes" -version = "6.4.1" +name = "option-ext" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" dependencies = [ "memchr", ] [[package]] name = "ouroboros" -version = "0.15.5" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca" +checksum = "e1358bd1558bd2a083fed428ffeda486fbfb323e698cdda7794259d592ca72db" dependencies = [ "aliasable", "ouroboros_macro", @@ -2299,15 +2937,15 @@ dependencies = [ [[package]] name = "ouroboros_macro" -version = "0.15.5" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d" +checksum = "5f7d21ccd03305a674437ee1248f3ab5d4b1db095cf1caf49f1713ddf61956b7" dependencies = [ "Inflector", "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.104", + "syn 1.0.109", ] [[package]] @@ -2346,15 +2984,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.5" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", - "windows-sys 0.42.0", + "windows-targets 0.48.5", ] [[package]] @@ -2364,7 +3002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -2374,7 +3012,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest 0.10.6", + "digest", "hmac", "password-hash", "sha2", @@ -2391,9 +3029,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" @@ -2402,17 +3040,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.0.0", + "indexmap", ] [[package]] name = "pgp" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a79d6411154d1a9908e7a2c4bac60a5742f6125823c2c30780c7039aef02f0" +checksum = "27e1f8e085bfa9b85763fe3ddaacbe90a09cd847b3833129153a6cb063bbe132" dependencies = [ "aes", - "base64 0.21.0", + "base64 0.21.5", "bitfield", "block-padding", "blowfish", @@ -2425,9 +3063,10 @@ dependencies = [ "chrono", "cipher", "crc24", + "curve25519-dalek", "derive_builder", "des", - "digest 0.10.6", + "digest", "ed25519-dalek", "elliptic-curve", "flate2", @@ -2465,7 +3104,7 @@ dependencies = [ "async-recursion", "futures", "hyper", - "hyper-rustls 0.24.1", + "hyper-rustls", "log", "pgp", "rand", @@ -2479,9 +3118,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -2512,21 +3151,27 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "platforms" -version = "3.0.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" +checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" [[package]] name = "portable-atomic" -version = "0.3.19" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" +checksum = "3bccab0e7fd7cc19f820a1c8c91720af652d0c88dc9664dd72aef2614f04af3b" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" @@ -2536,9 +3181,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "primeorder" -version = "0.13.2" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ "elliptic-curve", ] @@ -2552,7 +3197,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn 1.0.104", + "syn 1.0.109", "version_check", ] @@ -2567,17 +3212,11 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -2593,6 +3232,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "prodash" +version = "23.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9516b775656bc3e8985e19cd4b8c0c0de045095074e453d2c0a513b5f978392d" + [[package]] name = "psm" version = "0.1.21" @@ -2610,28 +3255,22 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" -version = "0.28.2" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] -[[package]] -name = "quoted_printable" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb" - [[package]] name = "rand" version = "0.8.5" @@ -2640,7 +3279,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -2650,16 +3289,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", + "rand_core", ] [[package]] @@ -2668,14 +3298,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.8", + "getrandom", ] [[package]] name = "rayon" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ "either", "rayon-core", @@ -2683,75 +3313,96 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.10.2" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ - "getrandom 0.2.8", - "redox_syscall", + "getrandom", + "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.8.3" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", ] [[package]] name = "regex-automata" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" - -[[package]] -name = "regex-syntax" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" dependencies = [ - "winapi", + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", ] [[package]] -name = "reqwest" -version = "0.11.17" +name = "regex-automata" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ - "base64 0.21.0", + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64 0.21.5", "bytes", "encoding_rs", "futures-core", @@ -2760,7 +3411,7 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-rustls 0.23.2", + "hyper-rustls", "ipnet", "js-sys", "log", @@ -2768,20 +3419,21 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.20.8", + "rustls 0.21.9", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.22.6", - "winreg", + "webpki-roots", + "winreg 0.50.0", ] [[package]] @@ -2794,20 +3446,6 @@ dependencies = [ "quick-error", ] -[[package]] -name = "rfc2047-decoder" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11347d014ae34e1d367aaf9191c37bf071b161e2e1c09c8559c7717e87030e11" -dependencies = [ - "base64 0.13.1", - "charset", - "chumsky 0.8.0", - "memchr", - "quoted_printable", - "thiserror", -] - [[package]] name = "rfc6979" version = "0.4.0" @@ -2827,19 +3465,33 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", - "untrusted", + "spin 0.5.2", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] + [[package]] name = "ripemd" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" dependencies = [ - "digest 0.10.6", + "digest", ] [[package]] @@ -2850,21 +3502,20 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" [[package]] name = "rsa" -version = "0.9.0-pre.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65db0998ad35adcaca498b7358992e088ee16cc783fe6fb899da203e113a63e5" +checksum = "6a3211b01eea83d80687da9eef70e39d65144a3894866a5153a2723e425a157f" dependencies = [ - "byteorder", "const-oid", - "digest 0.10.6", + "digest", "num-bigint-dig", "num-integer", - "num-iter", "num-traits", "pkcs1", "pkcs8", - "rand_core 0.6.4", + "rand_core", "signature", + "spki", "subtle", "zeroize", ] @@ -2875,7 +3526,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 2.2.1", + "bitflags 2.4.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -2883,6 +3534,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc_version" version = "0.4.0" @@ -2894,38 +3551,37 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.5" +version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.1", "errno", - "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] name = "rustls" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" dependencies = [ "log", - "ring", + "ring 0.16.20", "sct", "webpki", ] [[package]] name = "rustls" -version = "0.21.1" +version = "0.21.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" +checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" dependencies = [ "log", - "ring", + "ring 0.17.5", "rustls-webpki", "sct", ] @@ -2937,16 +3593,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c6a18f8d10f71bce9bca6eaeb80429460e652f3bcf0381f0c5f8954abf7b3b8" dependencies = [ "log", - "rustls 0.20.8", + "rustls 0.20.9", "rustls-native-certs", "webpki", ] [[package]] name = "rustls-native-certs" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", "rustls-pemfile", @@ -2956,28 +3612,34 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.0", + "base64 0.21.5", ] [[package]] name = "rustls-webpki" -version = "0.100.1" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.5", + "untrusted 0.9.0", ] [[package]] -name = "ryu" -version = "1.0.11" +name = "rustversion" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "safemem" @@ -2986,35 +3648,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" [[package]] -name = "schannel" -version = "0.1.20" +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "lazy_static", - "windows-sys 0.36.1", + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", ] [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring", - "untrusted", + "ring 0.17.5", + "untrusted 0.9.0", ] [[package]] @@ -3045,9 +3709,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.7.0" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -3058,9 +3722,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", @@ -3068,35 +3732,35 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.160" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.39", ] [[package]] name = "serde_json" -version = "1.0.89" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -3105,18 +3769,19 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" dependencies = [ + "itoa", "serde", ] [[package]] name = "serde_spanned" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" dependencies = [ "serde", ] @@ -3135,24 +3800,30 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest", ] [[package]] -name = "sha2" -version = "0.10.6" +name = "sha1_smol" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest", ] [[package]] @@ -3161,10 +3832,16 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest 0.10.6", + "digest", "keccak", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shellexpand" version = "3.1.0" @@ -3172,10 +3849,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" dependencies = [ "bstr", - "dirs", + "dirs 5.0.1", "os_str_bytes", ] +[[package]] +name = "shellexpand-utils" +version = "0.1.0" +dependencies = [ + "log", + "shellexpand", + "thiserror", +] + [[package]] name = "shellexpand-utils" version = "0.1.0" @@ -3187,6 +3873,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -3198,28 +3894,28 @@ dependencies = [ [[package]] name = "signature" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.6", - "rand_core 0.6.4", + "digest", + "rand_core", ] [[package]] name = "slab" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "smtp-proto" @@ -3229,20 +3925,36 @@ checksum = "d4b756ac662e92a0e5b360349bea5f0b0784d4be4541eff2972049dfdfd7f862" [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", ] +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" version = "0.7.2" @@ -3286,15 +3998,15 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "1.0.104" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -3303,53 +4015,73 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" -version = "6.1.1" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3" +checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml", + "toml 0.8.8", "version-compare", ] [[package]] name = "target-lexicon" -version = "0.12.11" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" +checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" [[package]] name = "tempfile" -version = "3.3.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", + "redox_syscall 0.4.1", + "rustix", + "windows-sys 0.48.0", ] [[package]] name = "termcolor" -version = "1.1.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] @@ -3366,58 +4098,53 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.39", ] [[package]] name = "time" -version = "0.1.45" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ + "deranged", + "itoa", "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" -dependencies = [ + "num_threads", + "powerfmt", "serde", "time-core", + "time-macros", ] [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] -name = "tiny-keccak" -version = "2.0.2" +name = "time-macros" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ - "crunchy", + "time-core", ] [[package]] @@ -3431,66 +4158,54 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.25.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", - "memchr", "mio", "num_cpus", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.5", "tokio-macros", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.104", + "syn 2.0.39", ] [[package]] name = "tokio-rustls" -version = "0.23.4" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.20.8", - "tokio", - "webpki", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" -dependencies = [ - "rustls 0.21.1", + "rustls 0.21.9", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -3502,32 +4217,57 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.4" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.21.0", ] [[package]] name = "toml_datetime" -version = "0.6.2" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.9" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 1.9.2", + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", "serde", "serde_spanned", "toml_datetime", @@ -3542,11 +4282,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3554,20 +4293,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.39", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] @@ -3586,59 +4325,6 @@ dependencies = [ "petgraph", ] -[[package]] -name = "trust-dns-proto" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna 0.2.3", - "ipnet", - "lazy_static", - "rand", - "ring", - "rustls 0.20.8", - "rustls-pemfile", - "smallvec", - "thiserror", - "tinyvec", - "tokio", - "tokio-rustls 0.23.4", - "tracing", - "url", - "webpki", -] - -[[package]] -name = "trust-dns-resolver" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" -dependencies = [ - "cfg-if", - "futures-util", - "ipconfig", - "lazy_static", - "lru-cache", - "parking_lot", - "resolv-conf", - "rustls 0.20.8", - "smallvec", - "thiserror", - "tokio", - "tokio-rustls 0.23.4", - "tracing", - "trust-dns-proto", - "webpki-roots 0.22.6", -] - [[package]] name = "try-lock" version = "0.2.4" @@ -3656,9 +4342,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" @@ -3667,10 +4353,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] -name = "unicode-ident" -version = "1.0.5" +name = "unicode-bom" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "63ec69f541d875b783ca40184d655f2927c95f0bffd486faa83cd3ac3529ec32" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -3683,9 +4375,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "untrusted" @@ -3694,22 +4386,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] -name = "url" -version = "2.4.0" +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna 0.4.0", + "idna 0.5.0", "percent-encoding", "serde", ] [[package]] name = "urlencoding" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "utf7-imap" @@ -3722,13 +4420,19 @@ dependencies = [ "regex", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.8", + "getrandom", ] [[package]] @@ -3737,6 +4441,18 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vergen" +version = "8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b86a8af1dedf089b1c78338678e4c7492b6045649042d94faf19690499d236" +dependencies = [ + "anyhow", + "gix", + "rustversion", + "time", +] + [[package]] name = "version-compare" version = "0.1.1" @@ -3750,26 +4466,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] -name = "want" -version = "0.3.0" +name = "walkdir" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ - "log", - "try-lock", + "same-file", + "winapi-util", ] [[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] [[package]] name = "wasi" @@ -3779,9 +4492,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3789,24 +4502,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.104", + "syn 2.0.39", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.33" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" dependencies = [ "cfg-if", "js-sys", @@ -3816,9 +4529,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3826,28 +4539,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", - "syn 1.0.104", + "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" [[package]] name = "web-sys" -version = "0.3.60" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" dependencies = [ "js-sys", "wasm-bindgen", @@ -3855,37 +4568,25 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.0" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring", - "untrusted", + "ring 0.17.5", + "untrusted 0.9.0", ] [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - -[[package]] -name = "webpki-roots" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" -dependencies = [ - "rustls-webpki", -] +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" [[package]] name = "widestring" -version = "0.5.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" [[package]] name = "winapi" @@ -3905,9 +4606,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -3924,43 +4625,72 @@ version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" dependencies = [ - "windows_aarch64_gnullvm", + "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] -name = "windows-sys" -version = "0.36.1" +name = "windows-core" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", + "windows-targets 0.48.5", ] [[package]] name = "windows-sys" -version = "0.42.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows_aarch64_gnullvm", + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -3968,10 +4698,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" +name = "windows_aarch64_gnullvm" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -3980,10 +4710,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] -name = "windows_i686_gnu" -version = "0.36.1" +name = "windows_aarch64_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -3992,10 +4722,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] -name = "windows_i686_msvc" -version = "0.36.1" +name = "windows_i686_gnu" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -4004,10 +4734,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" +name = "windows_i686_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -4015,6 +4745,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -4022,10 +4758,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" +name = "windows_x86_64_gnullvm" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -4034,10 +4770,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] -name = "winnow" -version = "0.4.6" +name = "windows_x86_64_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" dependencies = [ "memchr", ] @@ -4052,27 +4794,58 @@ dependencies = [ ] [[package]] -name = "x25519-dalek" -version = "2.0.0-pre.1" +name = "winreg" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "curve25519-dalek 3.2.0", - "rand_core 0.6.4", + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", "zeroize", ] [[package]] name = "z-base-32" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e897c250a7dfbb7002a324817f2540ab789a8f32d505032ca623a4109245e87" +checksum = "80a0d98613370af88e15bd2047702d7c78c8c6aba44403eb227c8ad706871f92" + +[[package]] +name = "zerocopy" +version = "0.7.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] [[package]] name = "zeroize" -version = "1.5.7" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] @@ -4085,7 +4858,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.39", ] [[package]] @@ -4104,7 +4877,7 @@ dependencies = [ "hmac", "pbkdf2", "sha1", - "time 0.3.21", + "time", "zstd", ] @@ -4129,11 +4902,10 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" +version = "2.0.9+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" dependencies = [ "cc", - "libc", "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index 2a6eb22..e8f25e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,9 @@ version = "3.3" [dependencies.anyhow] version = "1.0" +[dependencies.async-trait] +version = "0.1" + [dependencies.atty] version = "0.2" @@ -88,21 +91,23 @@ version = "0.7.0" version = "1.16.0" [dependencies.email-lib] -version = "=0.15.3" +# version = "=0.15.3" default-features = false +path = "/home/soywod/sourcehut/pimalaya/email" [dependencies.keyring-lib] version = "=0.1.0" [dependencies.oauth-lib] -version = "=0.1.0" +# version = "=0.1.0" +path = "/home/soywod/sourcehut/pimalaya/oauth" [dependencies.process-lib] version = "=0.1.0" [dependencies.mml-lib] -version = "=0.5.0" -default-features = false +# version = "=1.0.1" +path = "/home/soywod/sourcehut/pimalaya/mml" [dependencies.secret-lib] version = "=0.1.0" @@ -115,7 +120,8 @@ features = ["derive"] version = "1.0" [dependencies.shellexpand-utils] -version = "=0.1.0" +# version = "=0.1.0" +path = "/home/soywod/sourcehut/pimalaya/shellexpand-utils" [dependencies.termcolor] version = "1.1" diff --git a/config.sample.toml b/config.sample.toml index 0b24037..2fd521c 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -1,54 +1,18 @@ -display-name = "Display NAME" -signature-delim = "~~" -signature = "~/.signature" -downloads-dir = "~/downloads" -folder-listing-page-size = 12 -email-listing-page-size = 12 -email-reading-headers = ["From", "To"] -email-reading-verify-cmd = "gpg --verify -q" -email-reading-decrypt-cmd = "gpg -dq" -email-writing-sign-cmd = "gpg -o - -saq" -email-writing-encrypt-cmd = "gpg -o - -eqar " - [example] default = true -display-name = "Display NAME (gmail)" -email = "display.name@gmail.local" +# The display-name and email are used to build the full email address: +# "My example account" +display-name = "My example account" +email = "example@localhost" + +# The default backend used for all the features like adding folders, +# listing envelopes or copying messages. backend = "imap" -imap-host = "imap.gmail.com" -imap-login = "display.name@gmail.local" -imap-auth = "passwd" -imap-passwd.cmd = "pass show gmail" -imap-port = 993 -imap-ssl = true -imap-starttls = false -imap-notify-cmd = """📫 "" """"" -imap-notify-query = "NOT SEEN" -imap-watch-cmds = ["echo \"received server changes!\""] -sender = "smtp" -smtp-host = "smtp.gmail.com" -smtp-login = "display.name@gmail.local" -smtp-auth = "passwd" -smtp-passwd.cmd = "pass show piana/gmail" -smtp-port = 465 -smtp-ssl = true -smtp-starttls = false +imap.host = "imap.gmail.com" +imap.port = 993 +imap.login = "example@localhost" +imap.auth = "passwd" +# imap.Some.passwd.cmd = "pass show gmail" -sync = true -sync-dir = "/tmp/sync/gmail" -sync-folders-strategy.include = ["INBOX"] - -[example.folder-aliases] -inbox = "INBOX" -drafts = "[Gmail]/Drafts" -sent = "[Gmail]/Sent Mail" -trash = "[Gmail]/Trash" - -[example.email-hooks] -pre-send = "echo $1" - -[example.email-reading-format] -type = "fixed" -width = 64 diff --git a/src/backend.rs b/src/backend.rs new file mode 100644 index 0000000..67d1882 --- /dev/null +++ b/src/backend.rs @@ -0,0 +1,237 @@ +use std::{collections::HashMap, ops::Deref}; + +use anyhow::{anyhow, Result}; +use async_trait::async_trait; + +#[cfg(feature = "imap-backend")] +use email::imap::{ImapSessionBuilder, ImapSessionSync}; +#[cfg(feature = "smtp-sender")] +use email::smtp::{SmtpClientBuilder, SmtpClientSync}; +use email::{ + account::AccountConfig, + config::Config, + folder::list::{imap::ListFoldersImap, maildir::ListFoldersMaildir}, + maildir::{MaildirSessionBuilder, MaildirSessionSync}, + sendmail::SendmailContext, +}; +use serde::{Deserialize, Serialize}; + +use crate::config::DeserializedConfig; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum BackendKind { + #[default] + None, + Maildir, + #[cfg(feature = "imap-backend")] + Imap, + #[cfg(feature = "notmuch-backend")] + Notmuch, + #[cfg(feature = "smtp-sender")] + Smtp, + Sendmail, +} + +#[derive(Clone, Default)] +pub struct BackendContextBuilder { + account_config: AccountConfig, + + #[cfg(feature = "imap-backend")] + imap: Option, + maildir: Option, + #[cfg(feature = "smtp-sender")] + smtp: Option, + sendmail: Option, +} + +#[async_trait] +impl email::backend::BackendContextBuilder for BackendContextBuilder { + type Context = BackendContext; + + async fn build(self) -> Result { + let mut ctx = BackendContext::default(); + + if let Some(maildir) = self.maildir { + ctx.maildir = Some(maildir.build().await?); + } + + #[cfg(feature = "imap-backend")] + if let Some(imap) = self.imap { + ctx.imap = Some(imap.build().await?); + } + + #[cfg(feature = "notmuch-backend")] + if let Some(notmuch) = self.notmuch { + ctx.notmuch = Some(notmuch.build().await?); + } + + #[cfg(feature = "smtp-sender")] + if let Some(smtp) = self.smtp { + ctx.smtp = Some(smtp.build().await?); + } + + if let Some(sendmail) = self.sendmail { + ctx.sendmail = Some(sendmail.build().await?); + } + + Ok(ctx) + } +} + +#[derive(Default)] +pub struct BackendContext { + #[cfg(feature = "imap-backend")] + pub imap: Option, + pub maildir: Option, + #[cfg(feature = "smtp-sender")] + pub smtp: Option, + pub sendmail: Option, +} + +pub struct BackendBuilder(pub email::backend::BackendBuilder); + +impl BackendBuilder { + pub async fn new(config: DeserializedConfig, account_name: Option<&str>) -> Result { + let (account_name, deserialized_account_config) = match account_name { + Some("default") | Some("") | None => config + .accounts + .iter() + .find_map(|(name, account)| { + account + .default + .filter(|default| *default == true) + .map(|_| (name.to_owned(), account.clone())) + }) + .ok_or_else(|| anyhow!("cannot find default account")), + Some(name) => config + .accounts + .get(name) + .map(|account| (name.to_owned(), account.clone())) + .ok_or_else(|| anyhow!("cannot find account {name}")), + }?; + + let config = Config { + display_name: config.display_name, + signature_delim: config.signature_delim, + signature: config.signature, + downloads_dir: config.downloads_dir, + + folder_listing_page_size: config.folder_listing_page_size, + folder_aliases: config.folder_aliases, + + email_listing_page_size: config.email_listing_page_size, + email_listing_datetime_fmt: config.email_listing_datetime_fmt, + email_listing_datetime_local_tz: config.email_listing_datetime_local_tz, + email_reading_headers: config.email_reading_headers, + email_reading_format: config.email_reading_format, + email_writing_headers: config.email_writing_headers, + email_sending_save_copy: config.email_sending_save_copy, + email_hooks: config.email_hooks, + + accounts: HashMap::from_iter(config.accounts.clone().into_iter().map( + |(name, config)| { + ( + name.clone(), + AccountConfig { + name, + email: config.email, + display_name: config.display_name, + signature_delim: config.signature_delim, + signature: config.signature, + downloads_dir: config.downloads_dir, + + folder_listing_page_size: config.folder_listing_page_size, + folder_aliases: config.folder_aliases.unwrap_or_default(), + + email_listing_page_size: config.email_listing_page_size, + email_listing_datetime_fmt: config.email_listing_datetime_fmt, + email_listing_datetime_local_tz: config.email_listing_datetime_local_tz, + + email_reading_headers: config.email_reading_headers, + email_reading_format: config.email_reading_format.unwrap_or_default(), + email_writing_headers: config.email_writing_headers, + email_sending_save_copy: config.email_sending_save_copy, + email_hooks: config.email_hooks.unwrap_or_default(), + + sync: config.sync, + sync_dir: config.sync_dir, + sync_folders_strategy: config.sync_folders_strategy.unwrap_or_default(), + + #[cfg(feature = "pgp")] + pgp: config.pgp, + }, + ) + }, + )), + }; + + let account_config = config.account(account_name)?; + + let backend_ctx_builder = BackendContextBuilder { + account_config: account_config.clone(), + maildir: deserialized_account_config + .maildir + .as_ref() + .map(|mdir_config| { + MaildirSessionBuilder::new(account_config.clone(), mdir_config.clone()) + }), + #[cfg(feature = "imap-backend")] + imap: deserialized_account_config + .imap + .as_ref() + .map(|imap_config| { + ImapSessionBuilder::new(account_config.clone(), imap_config.clone()) + }), + #[cfg(feature = "smtp-sender")] + smtp: deserialized_account_config + .smtp + .as_ref() + .map(|smtp_config| { + SmtpClientBuilder::new(account_config.clone(), smtp_config.clone()) + }), + sendmail: deserialized_account_config + .sendmail + .as_ref() + .map(|sendmail_config| { + SendmailContext::new(account_config.clone(), sendmail_config.clone()) + }), + ..Default::default() + }; + + let backend_builder = + email::backend::BackendBuilder::new(account_config.clone(), backend_ctx_builder) + .with_list_folders(move |ctx| { + println!( + "deserialized_account_config: {:#?}", + deserialized_account_config + ); + match deserialized_account_config.backend { + BackendKind::Maildir if ctx.maildir.is_some() => { + ListFoldersMaildir::new(ctx.maildir.as_ref().unwrap()) + } + #[cfg(feature = "imap-backend")] + BackendKind::Imap if ctx.imap.is_some() => { + ListFoldersImap::new(ctx.imap.as_ref().unwrap()) + } + #[cfg(feature = "notmuch-backend")] + BackendKind::Notmuch if ctx.notmuch.is_some() => { + ListFoldersNotmuch::new(ctx.notmuch.as_ref().unwrap()) + } + _ => None, + } + }); + + Ok(Self(backend_builder)) + } +} + +impl Deref for BackendBuilder { + type Target = email::backend::BackendBuilder; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub type Backend = email::backend::Backend; diff --git a/src/cache/id_mapper.rs b/src/cache/id_mapper.rs index 5533a4e..a022640 100644 --- a/src/cache/id_mapper.rs +++ b/src/cache/id_mapper.rs @@ -1,15 +1,12 @@ use anyhow::{anyhow, Context, Result}; -#[cfg(feature = "imap-backend")] -use email::backend::ImapBackend; +use email::account::AccountConfig; #[cfg(feature = "notmuch-backend")] use email::backend::NotmuchBackend; -use email::{ - account::AccountConfig, - backend::{Backend, MaildirBackend}, -}; use log::{debug, trace}; use std::path::{Path, PathBuf}; +use crate::backend::Backend; + const ID_MAPPER_DB_FILE_NAME: &str = ".id-mapper.sqlite"; #[derive(Debug)] @@ -39,47 +36,45 @@ impl IdMapper { db_path } - pub fn new( - backend: &dyn Backend, - account_config: &AccountConfig, - folder: &str, - ) -> Result { - #[cfg(feature = "imap-backend")] - if backend.as_any().is::() { - return Ok(IdMapper::Dummy); - } + pub fn new(backend: &Backend, account_config: &AccountConfig, folder: &str) -> Result { + Ok(IdMapper::Dummy) - let mut db_path = PathBuf::new(); + // #[cfg(feature = "imap-backend")] + // if backend.as_any().is::() { + // return Ok(IdMapper::Dummy); + // } - if let Some(backend) = backend.as_any().downcast_ref::() { - db_path = Self::find_closest_db_path(backend.path()) - } + // let mut db_path = PathBuf::new(); - #[cfg(feature = "notmuch-backend")] - if let Some(backend) = backend.as_any().downcast_ref::() { - db_path = Self::find_closest_db_path(backend.path()) - } + // if let Some(backend) = backend.as_any().downcast_ref::() { + // db_path = Self::find_closest_db_path(backend.path()) + // } - let folder = account_config.get_folder_alias(folder)?; - let digest = md5::compute(account_config.name.clone() + &folder); - let table = format!("id_mapper_{digest:x}"); - debug!("creating id mapper table {table} at {db_path:?}…"); + // #[cfg(feature = "notmuch-backend")] + // if let Some(backend) = backend.as_any().downcast_ref::() { + // db_path = Self::find_closest_db_path(backend.path()) + // } - let conn = rusqlite::Connection::open(&db_path) - .with_context(|| format!("cannot open id mapper database at {db_path:?}"))?; + // let folder = account_config.get_folder_alias(folder)?; + // let digest = md5::compute(account_config.name.clone() + &folder); + // let table = format!("id_mapper_{digest:x}"); + // debug!("creating id mapper table {table} at {db_path:?}…"); - let query = format!( - "CREATE TABLE IF NOT EXISTS {table} ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - internal_id TEXT UNIQUE - )", - ); - trace!("create table query: {query:#?}"); + // let conn = rusqlite::Connection::open(&db_path) + // .with_context(|| format!("cannot open id mapper database at {db_path:?}"))?; - conn.execute(&query, []) - .context("cannot create id mapper table")?; + // let query = format!( + // "CREATE TABLE IF NOT EXISTS {table} ( + // id INTEGER PRIMARY KEY AUTOINCREMENT, + // internal_id TEXT UNIQUE + // )", + // ); + // trace!("create table query: {query:#?}"); - Ok(Self::Mapper(table, conn)) + // conn.execute(&query, []) + // .context("cannot create id mapper table")?; + + // Ok(Self::Mapper(table, conn)) } pub fn create_alias(&self, id: I) -> Result diff --git a/src/config/config.rs b/src/config/config.rs index b36a9b2..4ece9b8 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -6,12 +6,8 @@ use anyhow::{anyhow, Context, Result}; use dialoguer::Confirm; use dirs::{config_dir, home_dir}; -use email::{ - account::AccountConfig, - email::{EmailHooks, EmailTextPlainFormat}, -}; +use email::email::{EmailHooks, EmailTextPlainFormat}; use log::{debug, trace}; -use process::Cmd; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs, path::PathBuf, process::exit}; use toml; @@ -39,44 +35,12 @@ pub struct DeserializedConfig { pub email_listing_datetime_fmt: Option, pub email_listing_datetime_local_tz: Option, pub email_reading_headers: Option>, - #[serde( - default, - with = "EmailTextPlainFormatDef", - skip_serializing_if = "EmailTextPlainFormat::is_default" - )] - pub email_reading_format: EmailTextPlainFormat, - #[serde( - default, - with = "OptionCmdDef", - skip_serializing_if = "Option::is_none" - )] - pub email_reading_verify_cmd: Option, - #[serde( - default, - with = "OptionCmdDef", - skip_serializing_if = "Option::is_none" - )] - pub email_reading_decrypt_cmd: Option, + #[serde(default, with = "OptionEmailTextPlainFormatDef")] + pub email_reading_format: Option, pub email_writing_headers: Option>, - #[serde( - default, - with = "OptionCmdDef", - skip_serializing_if = "Option::is_none" - )] - pub email_writing_sign_cmd: Option, - #[serde( - default, - with = "OptionCmdDef", - skip_serializing_if = "Option::is_none" - )] - pub email_writing_encrypt_cmd: Option, pub email_sending_save_copy: Option, - #[serde( - default, - with = "EmailHooksDef", - skip_serializing_if = "EmailHooks::is_empty" - )] - pub email_hooks: EmailHooks, + #[serde(default, with = "OptionEmailHooksDef")] + pub email_hooks: Option, #[serde(flatten)] pub accounts: HashMap, @@ -134,28 +98,6 @@ impl DeserializedConfig { .or_else(|| home_dir().map(|p| p.join(".himalayarc"))) .filter(|p| p.exists()) } - - pub fn to_account_config(&self, account_name: Option<&str>) -> Result { - let (account_name, deserialized_account_config) = match account_name { - Some("default") | Some("") | None => self - .accounts - .iter() - .find_map(|(name, account)| { - account - .default - .filter(|default| *default == true) - .map(|_| (name.clone(), account)) - }) - .ok_or_else(|| anyhow!("cannot find default account")), - Some(name) => self - .accounts - .get(name) - .map(|account| (name.to_string(), account)) - .ok_or_else(|| anyhow!(format!("cannot find account {}", name))), - }?; - - Ok(deserialized_account_config.to_account_config(account_name, self)) - } } #[cfg(test)] diff --git a/src/config/prelude.rs b/src/config/prelude.rs index 1e5f783..861fb0a 100644 --- a/src/config/prelude.rs +++ b/src/config/prelude.rs @@ -9,15 +9,15 @@ use email::account::{NativePgpConfig, NativePgpSecretKey, SignedSecretKey}; #[cfg(feature = "notmuch-backend")] use email::backend::NotmuchConfig; #[cfg(feature = "imap-backend")] -use email::backend::{ImapAuthConfig, ImapConfig}; +use email::imap::{ImapAuthConfig, ImapConfig}; #[cfg(feature = "smtp-sender")] -use email::sender::{SmtpAuthConfig, SmtpConfig}; +use email::smtp::{SmtpAuthConfig, SmtpConfig}; use email::{ account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig}, - backend::{BackendConfig, MaildirConfig}, email::{EmailHooks, EmailTextPlainFormat}, folder::sync::FolderSyncStrategy, - sender::{SenderConfig, SendmailConfig}, + maildir::MaildirConfig, + sendmail::SendmailConfig, }; use keyring::Entry; use process::{Cmd, Pipeline, SingleCmd}; @@ -108,49 +108,47 @@ pub enum OAuth2MethodDef { } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(remote = "BackendConfig", tag = "backend", rename_all = "kebab-case")] -pub enum BackendConfigDef { - #[default] - None, - #[cfg(feature = "imap-backend")] - #[serde(with = "ImapConfigDef")] - Imap(ImapConfig), - #[serde(with = "MaildirConfigDef")] - Maildir(MaildirConfig), - #[cfg(feature = "notmuch-backend")] - #[serde(with = "NotmuchConfigDef")] - Notmuch(NotmuchConfig), +#[serde(remote = "Option", from = "OptionImapConfig")] +pub struct OptionImapConfigDef; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct OptionImapConfig { + #[serde(default, skip)] + is_none: bool, + #[serde(flatten, with = "ImapConfigDef")] + inner: ImapConfig, +} + +impl From for Option { + fn from(config: OptionImapConfig) -> Option { + if config.is_none { + None + } else { + Some(config.inner) + } + } } #[cfg(feature = "imap-backend")] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(remote = "ImapConfig")] +#[serde(remote = "ImapConfig", rename_all = "kebab-case")] pub struct ImapConfigDef { - #[serde(rename = "imap-host")] pub host: String, - #[serde(rename = "imap-port")] pub port: u16, - #[serde(rename = "imap-ssl")] pub ssl: Option, - #[serde(rename = "imap-starttls")] pub starttls: Option, - #[serde(rename = "imap-insecure")] pub insecure: Option, - #[serde(rename = "imap-login")] pub login: String, #[serde(flatten, with = "ImapAuthConfigDef")] pub auth: ImapAuthConfig, - #[serde(rename = "imap-notify-cmd")] pub notify_cmd: Option, - #[serde(rename = "imap-notify-query")] pub notify_query: Option, - #[serde(rename = "imap-watch-cmds")] pub watch_cmds: Option>, } #[cfg(feature = "imap-backend")] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(remote = "ImapAuthConfig", tag = "imap-auth")] +#[serde(remote = "ImapAuthConfig", tag = "auth")] pub enum ImapAuthConfigDef { #[serde(rename = "passwd", alias = "password", with = "ImapPasswdConfigDef")] Passwd(#[serde(default)] PasswdConfig), @@ -227,6 +225,28 @@ pub enum ImapOAuth2ScopesDef { Scopes(Vec), } +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(remote = "Option", from = "OptionMaildirConfig")] +pub struct OptionMaildirConfigDef; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OptionMaildirConfig { + #[default] + #[serde(skip_serializing)] + None, + Some(#[serde(with = "MaildirConfigDef")] MaildirConfig), +} + +impl From for Option { + fn from(config: OptionMaildirConfig) -> Option { + match config { + OptionMaildirConfig::None => None, + OptionMaildirConfig::Some(config) => Some(config), + } + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde(remote = "MaildirConfig", rename_all = "kebab-case")] pub struct MaildirConfigDef { @@ -234,6 +254,31 @@ pub struct MaildirConfigDef { pub root_dir: PathBuf, } +#[cfg(feature = "notmuch-backend")] +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(remote = "Option", from = "OptionNotmuchConfig")] +pub struct OptionNotmuchConfigDef; + +#[cfg(feature = "notmuch-backend")] +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OptionNotmuchConfig { + #[default] + #[serde(skip_serializing)] + None, + Some(#[serde(with = "NotmuchConfigDef")] NotmuchConfig), +} + +#[cfg(feature = "notmuch-backend")] +impl From for Option { + fn from(config: OptionNotmuchConfig) -> Option { + match config { + OptionNotmuchConfig::None => None, + OptionNotmuchConfig::Some(config) => Some(config), + } + } +} + #[cfg(feature = "notmuch-backend")] #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde(remote = "NotmuchConfig", rename_all = "kebab-case")] @@ -242,6 +287,35 @@ pub struct NotmuchConfigDef { pub db_path: PathBuf, } +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde( + remote = "Option", + from = "OptionEmailTextPlainFormat" +)] +pub struct OptionEmailTextPlainFormatDef; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OptionEmailTextPlainFormat { + #[serde(skip_serializing)] + None, + #[default] + Auto, + Flowed, + Fixed(usize), +} + +impl From for Option { + fn from(fmt: OptionEmailTextPlainFormat) -> Option { + match fmt { + OptionEmailTextPlainFormat::None => None, + OptionEmailTextPlainFormat::Auto => Some(EmailTextPlainFormat::Auto), + OptionEmailTextPlainFormat::Flowed => Some(EmailTextPlainFormat::Flowed), + OptionEmailTextPlainFormat::Fixed(size) => Some(EmailTextPlainFormat::Fixed(size)), + } + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde( remote = "EmailTextPlainFormat", @@ -257,15 +331,25 @@ pub enum EmailTextPlainFormatDef { } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(remote = "SenderConfig", tag = "sender", rename_all = "kebab-case")] -pub enum SenderConfigDef { +#[serde(remote = "Option", from = "OptionSmtpConfig")] +pub struct OptionSmtpConfigDef; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OptionSmtpConfig { #[default] + #[serde(skip_serializing)] None, - #[cfg(feature = "smtp-sender")] - #[serde(with = "SmtpConfigDef")] - Smtp(SmtpConfig), - #[serde(with = "SendmailConfigDef")] - Sendmail(SendmailConfig), + Some(#[serde(with = "SmtpConfigDef")] SmtpConfig), +} + +impl From for Option { + fn from(config: OptionSmtpConfig) -> Option { + match config { + OptionSmtpConfig::None => None, + OptionSmtpConfig::Some(config) => Some(config), + } + } } #[cfg(feature = "smtp-sender")] @@ -367,6 +451,28 @@ pub enum SmtpOAuth2ScopesDef { Scopes(Vec), } +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(remote = "Option", from = "OptionSendmailConfig")] +pub struct OptionSendmailConfigDef; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OptionSendmailConfig { + #[default] + #[serde(skip_serializing)] + None, + Some(#[serde(with = "SendmailConfigDef")] SendmailConfig), +} + +impl From for Option { + fn from(config: OptionSendmailConfig) -> Option { + match config { + OptionSendmailConfig::None => None, + OptionSendmailConfig::Some(config) => Some(config), + } + } +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(remote = "SendmailConfig", rename_all = "kebab-case")] pub struct SendmailConfigDef { @@ -382,6 +488,28 @@ fn sendmail_default_cmd() -> Cmd { Cmd::from("/usr/sbin/sendmail") } +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(remote = "Option", from = "OptionEmailHooks")] +pub struct OptionEmailHooksDef; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OptionEmailHooks { + #[default] + #[serde(skip_serializing)] + None, + Some(#[serde(with = "EmailHooksDef")] EmailHooks), +} + +impl From for Option { + fn from(fmt: OptionEmailHooks) -> Option { + match fmt { + OptionEmailHooks::None => None, + OptionEmailHooks::Some(hooks) => Some(hooks), + } + } +} + /// Represents the email hooks. Useful for doing extra email /// processing before or after sending it. #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] @@ -392,6 +520,31 @@ pub struct EmailHooksDef { pub pre_send: Option, } +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde( + remote = "Option", + from = "OptionFolderSyncStrategy" +)] +pub struct OptionFolderSyncStrategyDef; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OptionFolderSyncStrategy { + #[default] + #[serde(skip_serializing)] + None, + Some(#[serde(with = "FolderSyncStrategyDef")] FolderSyncStrategy), +} + +impl From for Option { + fn from(config: OptionFolderSyncStrategy) -> Option { + match config { + OptionFolderSyncStrategy::None => None, + OptionFolderSyncStrategy::Some(config) => Some(config), + } + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde(remote = "FolderSyncStrategy", rename_all = "kebab-case")] pub enum FolderSyncStrategyDef { diff --git a/src/domain/account/accounts.rs b/src/domain/account/accounts.rs index d752e00..c1e9665 100644 --- a/src/domain/account/accounts.rs +++ b/src/domain/account/accounts.rs @@ -5,7 +5,6 @@ //! accounts from the config file. use anyhow::Result; -use email::backend::BackendConfig; use serde::Serialize; use std::{collections::hash_map::Iter, ops::Deref}; @@ -40,19 +39,45 @@ impl PrintTable for Accounts { impl From> for Accounts { fn from(map: Iter<'_, String, DeserializedAccountConfig>) -> Self { let mut accounts: Vec<_> = map - .map(|(name, account)| match &account.backend { - BackendConfig::None => Account::new(name, "none", false), - BackendConfig::Maildir(_) => { - Account::new(name, "maildir", account.default.unwrap_or_default()) - } + .map(|(name, account)| { + let mut backends = String::new(); + #[cfg(feature = "imap-backend")] - BackendConfig::Imap(_) => { - Account::new(name, "imap", account.default.unwrap_or_default()) + if account.imap.is_some() { + backends.push_str("imap"); } + + if account.maildir.is_some() { + if !backends.is_empty() { + backends.push_str(", ") + } + backends.push_str("maildir"); + } + #[cfg(feature = "notmuch-backend")] - BackendConfig::Notmuch(_) => { - Account::new(name, "notmuch", account.default.unwrap_or_default()) + if account.imap.is_some() { + if !backends.is_empty() { + backends.push_str(", ") + } + backends.push_str("notmuch"); } + + #[cfg(feature = "smtp-sender")] + if account.smtp.is_some() { + if !backends.is_empty() { + backends.push_str(", ") + } + backends.push_str("smtp"); + } + + if account.sendmail.is_some() { + if !backends.is_empty() { + backends.push_str(", ") + } + backends.push_str("sendmail"); + } + + Account::new(name, &backends, account.default.unwrap_or_default()) }) .collect(); accounts.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap()); diff --git a/src/domain/account/config.rs b/src/domain/account/config.rs index 05017a8..e574f4c 100644 --- a/src/domain/account/config.rs +++ b/src/domain/account/config.rs @@ -6,29 +6,27 @@ #[cfg(feature = "pgp")] use email::account::PgpConfig; #[cfg(feature = "imap-backend")] -use email::backend::ImapAuthConfig; +use email::imap::ImapConfig; #[cfg(feature = "smtp-sender")] -use email::sender::SmtpAuthConfig; +use email::smtp::SmtpConfig; use email::{ - account::AccountConfig, - backend::BackendConfig, email::{EmailHooks, EmailTextPlainFormat}, folder::sync::FolderSyncStrategy, - sender::SenderConfig, + maildir::MaildirConfig, + sendmail::SendmailConfig, }; - -use process::Cmd; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf}; -use crate::config::{prelude::*, DeserializedConfig}; +use crate::{backend::BackendKind, config::prelude::*}; /// Represents all existing kind of account config. #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[serde(tag = "backend", rename_all = "kebab-case")] pub struct DeserializedAccountConfig { - pub email: String, pub default: Option, + + pub email: String, pub display_name: Option, pub signature_delim: Option, pub signature: Option, @@ -41,192 +39,39 @@ pub struct DeserializedAccountConfig { pub email_listing_datetime_fmt: Option, pub email_listing_datetime_local_tz: Option, pub email_reading_headers: Option>, - #[serde( - default, - with = "EmailTextPlainFormatDef", - skip_serializing_if = "EmailTextPlainFormat::is_default" - )] - pub email_reading_format: EmailTextPlainFormat, - #[serde( - default, - with = "OptionCmdDef", - skip_serializing_if = "Option::is_none" - )] - pub email_reading_verify_cmd: Option, - #[serde( - default, - with = "OptionCmdDef", - skip_serializing_if = "Option::is_none" - )] - pub email_reading_decrypt_cmd: Option, + #[serde(default, with = "OptionEmailTextPlainFormatDef")] + pub email_reading_format: Option, pub email_writing_headers: Option>, - #[serde( - default, - with = "OptionCmdDef", - skip_serializing_if = "Option::is_none" - )] - pub email_writing_sign_cmd: Option, - #[serde( - default, - with = "OptionCmdDef", - skip_serializing_if = "Option::is_none" - )] - pub email_writing_encrypt_cmd: Option, pub email_sending_save_copy: Option, - #[serde( - default, - with = "EmailHooksDef", - skip_serializing_if = "EmailHooks::is_empty" - )] - pub email_hooks: EmailHooks, + #[serde(default, with = "OptionEmailHooksDef")] + pub email_hooks: Option, pub sync: Option, pub sync_dir: Option, - #[serde( - default, - with = "FolderSyncStrategyDef", - skip_serializing_if = "FolderSyncStrategy::is_default" - )] - pub sync_folders_strategy: FolderSyncStrategy, + #[serde(default, with = "OptionFolderSyncStrategyDef")] + pub sync_folders_strategy: Option, - #[serde(flatten, with = "BackendConfigDef")] - pub backend: BackendConfig, - #[serde(flatten, with = "SenderConfigDef")] - pub sender: SenderConfig, + pub backend: BackendKind, + + #[cfg(feature = "imap-backend")] + #[serde(default, with = "OptionImapConfigDef")] + pub imap: Option, + + #[serde(default, with = "OptionMaildirConfigDef")] + pub maildir: Option, + + #[cfg(feature = "notmuch-backend")] + #[serde(default, with = "OptionNotmuchConfigDef")] + pub notmuch: Option, + + #[cfg(feature = "smtp-sender")] + #[serde(default, with = "OptionSmtpConfigDef")] + pub smtp: Option, + + #[serde(default, with = "OptionSendmailConfigDef")] + pub sendmail: Option, #[cfg(feature = "pgp")] - #[serde(default, with = "PgpConfigDef")] - pub pgp: PgpConfig, -} - -impl DeserializedAccountConfig { - pub fn to_account_config(&self, name: String, config: &DeserializedConfig) -> AccountConfig { - let mut folder_aliases = config - .folder_aliases - .as_ref() - .map(ToOwned::to_owned) - .unwrap_or_default(); - folder_aliases.extend( - self.folder_aliases - .as_ref() - .map(ToOwned::to_owned) - .unwrap_or_default(), - ); - - AccountConfig { - name: name.clone(), - email: self.email.to_owned(), - display_name: self - .display_name - .as_ref() - .map(ToOwned::to_owned) - .or_else(|| config.display_name.as_ref().map(ToOwned::to_owned)), - signature_delim: self - .signature_delim - .as_ref() - .map(ToOwned::to_owned) - .or_else(|| config.signature_delim.as_ref().map(ToOwned::to_owned)), - signature: self - .signature - .as_ref() - .map(ToOwned::to_owned) - .or_else(|| config.signature.as_ref().map(ToOwned::to_owned)), - downloads_dir: self - .downloads_dir - .as_ref() - .map(ToOwned::to_owned) - .or_else(|| config.downloads_dir.as_ref().map(ToOwned::to_owned)), - folder_listing_page_size: self - .folder_listing_page_size - .or_else(|| config.folder_listing_page_size), - folder_aliases, - email_listing_page_size: self - .email_listing_page_size - .or_else(|| config.email_listing_page_size), - email_listing_datetime_fmt: self - .email_listing_datetime_fmt - .as_ref() - .map(ToOwned::to_owned) - .or_else(|| { - config - .email_listing_datetime_fmt - .as_ref() - .map(ToOwned::to_owned) - }), - email_listing_datetime_local_tz: self - .email_listing_datetime_local_tz - .or_else(|| config.email_listing_datetime_local_tz), - email_reading_headers: self - .email_reading_headers - .as_ref() - .map(ToOwned::to_owned) - .or_else(|| config.email_reading_headers.as_ref().map(ToOwned::to_owned)), - email_reading_format: self.email_reading_format.clone(), - email_writing_headers: self - .email_writing_headers - .as_ref() - .map(ToOwned::to_owned) - .or_else(|| config.email_writing_headers.as_ref().map(ToOwned::to_owned)), - email_sending_save_copy: self.email_sending_save_copy.unwrap_or(true), - email_hooks: EmailHooks { - pre_send: self.email_hooks.pre_send.clone(), - }, - sync: self.sync.unwrap_or_default(), - sync_dir: self.sync_dir.clone(), - sync_folders_strategy: self.sync_folders_strategy.clone(), - - backend: { - let mut backend = self.backend.clone(); - - #[cfg(feature = "imap-backend")] - if let BackendConfig::Imap(config) = &mut backend { - match &mut config.auth { - ImapAuthConfig::Passwd(secret) => { - secret.set_keyring_entry_if_undefined(format!("{name}-imap-passwd")); - } - ImapAuthConfig::OAuth2(config) => { - config.client_secret.set_keyring_entry_if_undefined(format!( - "{name}-imap-oauth2-client-secret" - )); - config.access_token.set_keyring_entry_if_undefined(format!( - "{name}-imap-oauth2-access-token" - )); - config.refresh_token.set_keyring_entry_if_undefined(format!( - "{name}-imap-oauth2-refresh-token" - )); - } - }; - } - - backend - }, - sender: { - let mut sender = self.sender.clone(); - - #[cfg(feature = "smtp-sender")] - if let SenderConfig::Smtp(config) = &mut sender { - match &mut config.auth { - SmtpAuthConfig::Passwd(secret) => { - secret.set_keyring_entry_if_undefined(format!("{name}-smtp-passwd")); - } - SmtpAuthConfig::OAuth2(config) => { - config.client_secret.set_keyring_entry_if_undefined(format!( - "{name}-smtp-oauth2-client-secret" - )); - config.access_token.set_keyring_entry_if_undefined(format!( - "{name}-smtp-oauth2-access-token" - )); - config.refresh_token.set_keyring_entry_if_undefined(format!( - "{name}-smtp-oauth2-refresh-token" - )); - } - }; - } - - sender - }, - #[cfg(feature = "pgp")] - pgp: self.pgp.clone(), - } - } + #[serde(default, with = "OptionPgpConfigDef")] + pub pgp: Option, } diff --git a/src/domain/account/handlers.rs b/src/domain/account/handlers.rs index 56c0a7e..bec3fc3 100644 --- a/src/domain/account/handlers.rs +++ b/src/domain/account/handlers.rs @@ -2,25 +2,22 @@ //! //! This module gathers all account actions triggered by the CLI. -use anyhow::Result; -#[cfg(feature = "imap-backend")] -use email::backend::ImapAuthConfig; -#[cfg(feature = "smtp-sender")] -use email::sender::SmtpAuthConfig; -use email::{ - account::{ - sync::{AccountSyncBuilder, AccountSyncProgressEvent}, - AccountConfig, - }, - backend::BackendConfig, - sender::SenderConfig, +use anyhow::{Context, Result}; +use email::account::{ + sync::{AccountSyncBuilder, AccountSyncProgressEvent}, + AccountConfig, }; +#[cfg(feature = "imap-backend")] +use email::imap::ImapAuthConfig; +#[cfg(feature = "smtp-sender")] +use email::smtp::SmtpAuthConfig; use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle}; use log::{info, trace, warn}; use once_cell::sync::Lazy; use std::{collections::HashMap, sync::Mutex}; use crate::{ + backend::BackendContextBuilder, config::{ wizard::{prompt_passwd, prompt_secret}, DeserializedConfig, @@ -48,68 +45,68 @@ const SUB_PROGRESS_DONE_STYLE: Lazy = Lazy::new(|| { pub async fn configure(config: &AccountConfig, reset: bool) -> Result<()> { info!("entering the configure account handler"); - if reset { - #[cfg(feature = "imap-backend")] - if let BackendConfig::Imap(imap_config) = &config.backend { - let reset = match &imap_config.auth { - ImapAuthConfig::Passwd(passwd) => passwd.reset(), - ImapAuthConfig::OAuth2(oauth2) => oauth2.reset(), - }; - if let Err(err) = reset { - warn!("error while resetting imap secrets, skipping it"); - warn!("{err}"); - } - } + // if reset { + // #[cfg(feature = "imap-backend")] + // if let BackendConfig::Imap(imap_config) = &config.backend { + // let reset = match &imap_config.auth { + // ImapAuthConfig::Passwd(passwd) => passwd.reset(), + // ImapAuthConfig::OAuth2(oauth2) => oauth2.reset(), + // }; + // if let Err(err) = reset { + // warn!("error while resetting imap secrets, skipping it"); + // warn!("{err}"); + // } + // } - #[cfg(feature = "smtp-sender")] - if let SenderConfig::Smtp(smtp_config) = &config.sender { - let reset = match &smtp_config.auth { - SmtpAuthConfig::Passwd(passwd) => passwd.reset(), - SmtpAuthConfig::OAuth2(oauth2) => oauth2.reset(), - }; - if let Err(err) = reset { - warn!("error while resetting smtp secrets, skipping it"); - warn!("{err}"); - } - } + // #[cfg(feature = "smtp-sender")] + // if let SenderConfig::Smtp(smtp_config) = &config.sender { + // let reset = match &smtp_config.auth { + // SmtpAuthConfig::Passwd(passwd) => passwd.reset(), + // SmtpAuthConfig::OAuth2(oauth2) => oauth2.reset(), + // }; + // if let Err(err) = reset { + // warn!("error while resetting smtp secrets, skipping it"); + // warn!("{err}"); + // } + // } - #[cfg(feature = "pgp")] - config.pgp.reset().await?; - } + // #[cfg(feature = "pgp")] + // config.pgp.reset().await?; + // } - #[cfg(feature = "imap-backend")] - if let BackendConfig::Imap(imap_config) = &config.backend { - match &imap_config.auth { - ImapAuthConfig::Passwd(passwd) => { - passwd.configure(|| prompt_passwd("IMAP password")).await - } - ImapAuthConfig::OAuth2(oauth2) => { - oauth2 - .configure(|| prompt_secret("IMAP OAuth 2.0 client secret")) - .await - } - }?; - } + // #[cfg(feature = "imap-backend")] + // if let BackendConfig::Imap(imap_config) = &config.backend { + // match &imap_config.auth { + // ImapAuthConfig::Passwd(passwd) => { + // passwd.configure(|| prompt_passwd("IMAP password")).await + // } + // ImapAuthConfig::OAuth2(oauth2) => { + // oauth2 + // .configure(|| prompt_secret("IMAP OAuth 2.0 client secret")) + // .await + // } + // }?; + // } - #[cfg(feature = "smtp-sender")] - if let SenderConfig::Smtp(smtp_config) = &config.sender { - match &smtp_config.auth { - SmtpAuthConfig::Passwd(passwd) => { - passwd.configure(|| prompt_passwd("SMTP password")).await - } - SmtpAuthConfig::OAuth2(oauth2) => { - oauth2 - .configure(|| prompt_secret("SMTP OAuth 2.0 client secret")) - .await - } - }?; - } + // #[cfg(feature = "smtp-sender")] + // if let SenderConfig::Smtp(smtp_config) = &config.sender { + // match &smtp_config.auth { + // SmtpAuthConfig::Passwd(passwd) => { + // passwd.configure(|| prompt_passwd("SMTP password")).await + // } + // SmtpAuthConfig::OAuth2(oauth2) => { + // oauth2 + // .configure(|| prompt_secret("SMTP OAuth 2.0 client secret")) + // .await + // } + // }?; + // } - #[cfg(feature = "pgp")] - config - .pgp - .configure(&config.email, || prompt_passwd("PGP secret key password")) - .await?; + // #[cfg(feature = "pgp")] + // config + // .pgp + // .configure(&config.email, || prompt_passwd("PGP secret key password")) + // .await?; println!( "Account successfully {}configured!", @@ -147,7 +144,7 @@ pub fn list<'a, P: Printer>( /// no account given, synchronizes the default one. pub async fn sync( printer: &mut P, - sync_builder: AccountSyncBuilder, + sync_builder: AccountSyncBuilder, dry_run: bool, ) -> Result<()> { info!("entering the sync accounts handler"); diff --git a/src/domain/account/wizard.rs b/src/domain/account/wizard.rs index 306b9fd..f64f394 100644 --- a/src/domain/account/wizard.rs +++ b/src/domain/account/wizard.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Result}; use dialoguer::Input; use email_address::EmailAddress; -use crate::{backend, config::wizard::THEME, sender}; +use crate::config::wizard::THEME; use super::DeserializedAccountConfig; @@ -31,9 +31,9 @@ pub(crate) async fn configure() -> Result Result<()> { imap.notify(keepalive, folder).await?; diff --git a/src/domain/backend/imap/mod.rs b/src/domain/backend/imap/mod.rs index 723c3b8..b6c153f 100644 --- a/src/domain/backend/imap/mod.rs +++ b/src/domain/backend/imap/mod.rs @@ -1,3 +1,3 @@ pub mod args; -pub mod handlers; -pub(crate) mod wizard; +// pub mod handlers; +// pub(crate) mod wizard; diff --git a/src/domain/backend/maildir/mod.rs b/src/domain/backend/maildir/mod.rs index 73818b4..d4b1a6e 100644 --- a/src/domain/backend/maildir/mod.rs +++ b/src/domain/backend/maildir/mod.rs @@ -1 +1 @@ -pub(crate) mod wizard; +// pub(crate) mod wizard; diff --git a/src/domain/backend/mod.rs b/src/domain/backend/mod.rs index 548ca7c..b33f50e 100644 --- a/src/domain/backend/mod.rs +++ b/src/domain/backend/mod.rs @@ -3,4 +3,4 @@ pub mod imap; pub mod maildir; #[cfg(feature = "notmuch-backend")] pub mod notmuch; -pub(crate) mod wizard; +// pub(crate) mod wizard; diff --git a/src/domain/email/handlers.rs b/src/domain/email/handlers.rs index cc6f276..3589ea7 100644 --- a/src/domain/email/handlers.rs +++ b/src/domain/email/handlers.rs @@ -2,9 +2,7 @@ use anyhow::{anyhow, Context, Result}; use atty::Stream; use email::{ account::AccountConfig, - backend::Backend, - email::{template::FilterParts, Flag, Flags, Message, MessageBuilder}, - sender::Sender, + email::{envelope::Id, template::FilterParts, Flag, Message, MessageBuilder}, }; use log::{debug, trace}; use std::{ @@ -15,6 +13,7 @@ use url::Url; use uuid::Uuid; use crate::{ + backend::Backend, printer::{PrintTableOpts, Printer}, ui::editor, Envelopes, IdMapper, @@ -24,20 +23,20 @@ pub async fn attachments( config: &AccountConfig, printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, + backend: &Backend, folder: &str, ids: Vec<&str>, ) -> Result<()> { - let ids = id_mapper.get_ids(ids)?; - let ids = ids.iter().map(String::as_str).collect::>(); - let emails = backend.get_emails(&folder, ids.clone()).await?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + let emails = backend.get_messages(&folder, &ids).await?; let mut index = 0; let mut emails_count = 0; let mut attachments_count = 0; + let mut ids = ids.iter(); for email in emails.to_vec() { - let id = ids.get(index).unwrap(); + let id = ids.next().unwrap(); let attachments = email.attachments()?; index = index + 1; @@ -79,27 +78,27 @@ pub async fn attachments( pub async fn copy( printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, + backend: &Backend, from_folder: &str, to_folder: &str, ids: Vec<&str>, ) -> Result<()> { - let ids = id_mapper.get_ids(ids)?; - let ids = ids.iter().map(String::as_str).collect::>(); - backend.copy_emails(&from_folder, &to_folder, ids).await?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + backend + .copy_messages(&from_folder, &to_folder, &ids) + .await?; printer.print("Email(s) successfully copied!") } pub async fn delete( printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, + backend: &Backend, folder: &str, ids: Vec<&str>, ) -> Result<()> { - let ids = id_mapper.get_ids(ids)?; - let ids = ids.iter().map(String::as_str).collect::>(); - backend.delete_emails(&folder, ids).await?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + backend.delete_messages(&folder, &ids).await?; printer.print("Email(s) successfully deleted!") } @@ -107,18 +106,15 @@ pub async fn forward( config: &AccountConfig, printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, - sender: &mut dyn Sender, + backend: &Backend, folder: &str, id: &str, headers: Option>, body: Option<&str>, ) -> Result<()> { - let ids = id_mapper.get_ids([id])?; - let ids = ids.iter().map(String::as_str).collect::>(); - + let id = Id::single(id_mapper.get_id(id)?); let tpl = backend - .get_emails(&folder, ids) + .get_messages(&folder, &id) .await? .first() .ok_or_else(|| anyhow!("cannot find email {}", id))? @@ -128,7 +124,7 @@ pub async fn forward( .build() .await?; trace!("initial template: {tpl}"); - editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await?; + editor::edit_tpl_with_editor(config, printer, backend, tpl).await?; Ok(()) } @@ -136,7 +132,7 @@ pub async fn list( config: &AccountConfig, printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, + backend: &Backend, folder: &str, max_width: Option, page_size: Option, @@ -166,8 +162,7 @@ pub async fn list( /// [mailto]: https://en.wikipedia.org/wiki/Mailto pub async fn mailto( config: &AccountConfig, - backend: &mut dyn Backend, - sender: &mut dyn Sender, + backend: &Backend, printer: &mut P, url: &Url, ) -> Result<()> { @@ -190,20 +185,21 @@ pub async fn mailto( .from_msg_builder(builder) .await?; - editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await + editor::edit_tpl_with_editor(config, printer, backend, tpl).await } pub async fn move_( printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, + backend: &Backend, from_folder: &str, to_folder: &str, ids: Vec<&str>, ) -> Result<()> { - let ids = id_mapper.get_ids(ids)?; - let ids = ids.iter().map(String::as_str).collect::>(); - backend.move_emails(&from_folder, &to_folder, ids).await?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + backend + .move_messages(&from_folder, &to_folder, &ids) + .await?; printer.print("Email(s) successfully moved!") } @@ -211,16 +207,15 @@ pub async fn read( config: &AccountConfig, printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, + backend: &Backend, folder: &str, ids: Vec<&str>, text_mime: &str, raw: bool, headers: Vec<&str>, ) -> Result<()> { - let ids = id_mapper.get_ids(ids)?; - let ids = ids.iter().map(String::as_str).collect::>(); - let emails = backend.get_emails(&folder, ids).await?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + let emails = backend.get_messages(&folder, &ids).await?; let mut glue = ""; let mut bodies = String::default(); @@ -255,19 +250,16 @@ pub async fn reply( config: &AccountConfig, printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, - sender: &mut dyn Sender, + backend: &Backend, folder: &str, id: &str, all: bool, headers: Option>, body: Option<&str>, ) -> Result<()> { - let ids = id_mapper.get_ids([id])?; - let ids = ids.iter().map(String::as_str).collect::>(); - + let id = Id::single(id_mapper.get_id(id)?); let tpl = backend - .get_emails(&folder, ids) + .get_messages(folder, &id) .await? .first() .ok_or_else(|| anyhow!("cannot find email {}", id))? @@ -278,17 +270,15 @@ pub async fn reply( .build() .await?; trace!("initial template: {tpl}"); - editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await?; - backend - .add_flags(&folder, vec![id], &Flags::from_iter([Flag::Answered])) - .await?; + editor::edit_tpl_with_editor(config, printer, backend, tpl).await?; + backend.add_flag(&folder, &id, Flag::Answered).await?; Ok(()) } pub async fn save( printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, + backend: &Backend, folder: &str, raw_email: String, ) -> Result<()> { @@ -306,73 +296,74 @@ pub async fn save( }; let id = backend - .add_email(&folder, raw_email.as_bytes(), &Flags::default()) + .add_raw_message(&folder, raw_email.as_bytes()) .await?; - id_mapper.create_alias(id)?; + id_mapper.create_alias(&*id)?; Ok(()) } pub async fn search( - config: &AccountConfig, - printer: &mut P, - id_mapper: &IdMapper, - backend: &mut dyn Backend, - folder: &str, - query: String, - max_width: Option, - page_size: Option, - page: usize, + _config: &AccountConfig, + _printer: &mut P, + _id_mapper: &IdMapper, + _backend: &Backend, + _folder: &str, + _query: String, + _max_width: Option, + _page_size: Option, + _page: usize, ) -> Result<()> { - let page_size = page_size.unwrap_or(config.email_listing_page_size()); - let envelopes = Envelopes::from_backend( - config, - id_mapper, - backend - .search_envelopes(&folder, &query, "", page_size, page) - .await?, - )?; - let opts = PrintTableOpts { - format: &config.email_reading_format, - max_width, - }; + todo!() + // let page_size = page_size.unwrap_or(config.email_listing_page_size()); + // let envelopes = Envelopes::from_backend( + // config, + // id_mapper, + // backend + // .search_envelopes(&folder, &query, "", page_size, page) + // .await?, + // )?; + // let opts = PrintTableOpts { + // format: &config.email_reading_format, + // max_width, + // }; - printer.print_table(Box::new(envelopes), opts) + // printer.print_table(Box::new(envelopes), opts) } pub async fn sort( - config: &AccountConfig, - printer: &mut P, - id_mapper: &IdMapper, - backend: &mut dyn Backend, - folder: &str, - sort: String, - query: String, - max_width: Option, - page_size: Option, - page: usize, + _config: &AccountConfig, + _printer: &mut P, + _id_mapper: &IdMapper, + _backend: &Backend, + _folder: &str, + _sort: String, + _query: String, + _max_width: Option, + _page_size: Option, + _page: usize, ) -> Result<()> { - let page_size = page_size.unwrap_or(config.email_listing_page_size()); - let envelopes = Envelopes::from_backend( - config, - id_mapper, - backend - .search_envelopes(&folder, &query, &sort, page_size, page) - .await?, - )?; - let opts = PrintTableOpts { - format: &config.email_reading_format, - max_width, - }; + todo!() + // let page_size = page_size.unwrap_or(config.email_listing_page_size()); + // let envelopes = Envelopes::from_backend( + // config, + // id_mapper, + // backend + // .search_envelopes(&folder, &query, &sort, page_size, page) + // .await?, + // )?; + // let opts = PrintTableOpts { + // format: &config.email_reading_format, + // max_width, + // }; - printer.print_table(Box::new(envelopes), opts) + // printer.print_table(Box::new(envelopes), opts) } pub async fn send( config: &AccountConfig, printer: &mut P, - backend: &mut dyn Backend, - sender: &mut dyn Sender, + backend: &Backend, raw_email: String, ) -> Result<()> { let folder = config.sent_folder_alias()?; @@ -389,14 +380,10 @@ pub async fn send( .join("\r\n") }; trace!("raw email: {:?}", raw_email); - sender.send(raw_email.as_bytes()).await?; - if config.email_sending_save_copy { + backend.send_raw_message(raw_email.as_bytes()).await?; + if config.email_sending_save_copy.unwrap_or_default() { backend - .add_email( - &folder, - raw_email.as_bytes(), - &Flags::from_iter([Flag::Seen]), - ) + .add_raw_message_with_flag(&folder, raw_email.as_bytes(), Flag::Seen) .await?; } Ok(()) @@ -405,8 +392,7 @@ pub async fn send( pub async fn write( config: &AccountConfig, printer: &mut P, - backend: &mut dyn Backend, - sender: &mut dyn Sender, + backend: &Backend, headers: Option>, body: Option<&str>, ) -> Result<()> { @@ -416,6 +402,6 @@ pub async fn write( .build() .await?; trace!("initial template: {tpl}"); - editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await?; + editor::edit_tpl_with_editor(config, printer, backend, tpl).await?; Ok(()) } diff --git a/src/domain/flag/handlers.rs b/src/domain/flag/handlers.rs index 04a5fde..875718f 100644 --- a/src/domain/flag/handlers.rs +++ b/src/domain/flag/handlers.rs @@ -1,46 +1,43 @@ use anyhow::Result; -use email::{backend::Backend, email::Flags}; +use email::email::{envelope::Id, Flags}; -use crate::{printer::Printer, IdMapper}; +use crate::{backend::Backend, printer::Printer, IdMapper}; pub async fn add( printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, + backend: &Backend, folder: &str, ids: Vec<&str>, flags: &Flags, ) -> Result<()> { - let ids = id_mapper.get_ids(ids)?; - let ids = ids.iter().map(String::as_str).collect::>(); - backend.add_flags(folder, ids, flags).await?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + backend.add_flags(folder, &ids, flags).await?; printer.print("Flag(s) successfully added!") } pub async fn set( printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, + backend: &Backend, folder: &str, ids: Vec<&str>, flags: &Flags, ) -> Result<()> { - let ids = id_mapper.get_ids(ids)?; - let ids = ids.iter().map(String::as_str).collect::>(); - backend.set_flags(folder, ids, flags).await?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + backend.set_flags(folder, &ids, flags).await?; printer.print("Flag(s) successfully set!") } pub async fn remove( printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, + backend: &Backend, folder: &str, ids: Vec<&str>, flags: &Flags, ) -> Result<()> { - let ids = id_mapper.get_ids(ids)?; - let ids = ids.iter().map(String::as_str).collect::>(); - backend.remove_flags(folder, ids, flags).await?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + backend.remove_flags(folder, &ids, flags).await?; printer.print("Flag(s) successfully removed!") } diff --git a/src/domain/folder/handlers.rs b/src/domain/folder/handlers.rs index 5664e07..9c6937e 100644 --- a/src/domain/folder/handlers.rs +++ b/src/domain/folder/handlers.rs @@ -4,19 +4,16 @@ use anyhow::Result; use dialoguer::Confirm; -use email::{account::AccountConfig, backend::Backend}; +use email::account::AccountConfig; use std::process; use crate::{ + backend::Backend, printer::{PrintTableOpts, Printer}, Folders, }; -pub async fn expunge( - printer: &mut P, - backend: &mut dyn Backend, - folder: &str, -) -> Result<()> { +pub async fn expunge(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> { backend.expunge_folder(folder).await?; printer.print(format!("Folder {folder} successfully expunged!")) } @@ -24,7 +21,7 @@ pub async fn expunge( pub async fn list( config: &AccountConfig, printer: &mut P, - backend: &mut dyn Backend, + backend: &Backend, max_width: Option, ) -> Result<()> { let folders: Folders = backend.list_folders().await?.into(); @@ -38,20 +35,12 @@ pub async fn list( ) } -pub async fn create( - printer: &mut P, - backend: &mut dyn Backend, - folder: &str, -) -> Result<()> { +pub async fn create(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> { backend.add_folder(folder).await?; printer.print("Folder successfully created!") } -pub async fn delete( - printer: &mut P, - backend: &mut dyn Backend, - folder: &str, -) -> Result<()> { +pub async fn delete(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> { if let Some(false) | None = Confirm::new() .with_prompt(format!("Confirm deletion of folder {folder}?")) .default(false) diff --git a/src/domain/sender/mod.rs b/src/domain/sender/mod.rs index 4a237dc..eab09f5 100644 --- a/src/domain/sender/mod.rs +++ b/src/domain/sender/mod.rs @@ -1,4 +1,4 @@ pub mod sendmail; #[cfg(feature = "smtp-sender")] pub mod smtp; -pub(crate) mod wizard; +// pub(crate) mod wizard; diff --git a/src/domain/sender/sendmail/mod.rs b/src/domain/sender/sendmail/mod.rs index 73818b4..d4b1a6e 100644 --- a/src/domain/sender/sendmail/mod.rs +++ b/src/domain/sender/sendmail/mod.rs @@ -1 +1 @@ -pub(crate) mod wizard; +// pub(crate) mod wizard; diff --git a/src/domain/sender/smtp/mod.rs b/src/domain/sender/smtp/mod.rs index 73818b4..d4b1a6e 100644 --- a/src/domain/sender/smtp/mod.rs +++ b/src/domain/sender/smtp/mod.rs @@ -1 +1 @@ -pub(crate) mod wizard; +// pub(crate) mod wizard; diff --git a/src/domain/sender/wizard.rs b/src/domain/sender/wizard.rs index d382df1..bb4b9d6 100644 --- a/src/domain/sender/wizard.rs +++ b/src/domain/sender/wizard.rs @@ -1,6 +1,5 @@ use anyhow::Result; use dialoguer::Select; -use email::sender::SenderConfig; use crate::config::wizard::THEME; diff --git a/src/domain/tpl/handlers.rs b/src/domain/tpl/handlers.rs index a682930..a7515e4 100644 --- a/src/domain/tpl/handlers.rs +++ b/src/domain/tpl/handlers.rs @@ -2,30 +2,27 @@ use anyhow::{anyhow, Result}; use atty::Stream; use email::{ account::AccountConfig, - backend::Backend, - email::{Flag, Flags, Message}, - sender::Sender, + email::{envelope::Id, Flag, Message}, }; use mml::MmlCompilerBuilder; use std::io::{stdin, BufRead}; -use crate::{printer::Printer, IdMapper}; +use crate::{backend::Backend, printer::Printer, IdMapper}; pub async fn forward( config: &AccountConfig, printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, + backend: &Backend, folder: &str, id: &str, headers: Option>, body: Option<&str>, ) -> Result<()> { - let ids = id_mapper.get_ids([id])?; - let ids = ids.iter().map(String::as_str).collect::>(); + let ids = Id::multiple(id_mapper.get_ids([id])?); let tpl: String = backend - .get_emails(folder, ids) + .get_messages(folder, &ids) .await? .first() .ok_or_else(|| anyhow!("cannot find email {}", id))? @@ -43,18 +40,17 @@ pub async fn reply( config: &AccountConfig, printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, + backend: &Backend, folder: &str, id: &str, all: bool, headers: Option>, body: Option<&str>, ) -> Result<()> { - let ids = id_mapper.get_ids([id])?; - let ids = ids.iter().map(String::as_str).collect::>(); + let ids = Id::multiple(id_mapper.get_ids([id])?); let tpl: String = backend - .get_emails(folder, ids) + .get_messages(folder, &ids) .await? .first() .ok_or_else(|| anyhow!("cannot find email {}", id))? @@ -73,7 +69,7 @@ pub async fn save( #[allow(unused_variables)] config: &AccountConfig, printer: &mut P, id_mapper: &IdMapper, - backend: &mut dyn Backend, + backend: &Backend, folder: &str, tpl: String, ) -> Result<()> { @@ -95,8 +91,8 @@ pub async fn save( let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; - let id = backend.add_email(folder, &email, &Flags::default()).await?; - id_mapper.create_alias(id)?; + let id = backend.add_raw_message(folder, &email).await?; + id_mapper.create_alias(&*id)?; printer.print("Template successfully saved!") } @@ -104,8 +100,7 @@ pub async fn save( pub async fn send( config: &AccountConfig, printer: &mut P, - backend: &mut dyn Backend, - sender: &mut dyn Sender, + backend: &Backend, tpl: String, ) -> Result<()> { let folder = config.sent_folder_alias()?; @@ -128,11 +123,11 @@ pub async fn send( let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; - sender.send(&email).await?; + backend.send_raw_message(&email).await?; - if config.email_sending_save_copy { + if config.email_sending_save_copy.unwrap_or_default() { backend - .add_email(&folder, &email, &Flags::from_iter([Flag::Seen])) + .add_raw_message_with_flag(&folder, &email, Flag::Seen) .await?; } diff --git a/src/lib.rs b/src/lib.rs index f22abe8..d1b00f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod backend; pub mod cache; pub mod compl; pub mod config; diff --git a/src/main.rs b/src/main.rs index 5a174dc..5256932 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,4 @@ -#[cfg(feature = "imap-backend")] -use ::email::backend::ImapBackend; -use ::email::{ - account::{sync::AccountSyncBuilder, DEFAULT_INBOX_FOLDER}, - backend::{BackendBuilder, BackendConfig}, - sender::SenderBuilder, -}; +use ::email::account::{sync::AccountSyncBuilder, DEFAULT_INBOX_FOLDER}; use anyhow::{anyhow, Context, Result}; use clap::Command; use log::{debug, warn}; @@ -14,7 +8,9 @@ use url::Url; #[cfg(feature = "imap-backend")] use himalaya::imap; use himalaya::{ - account, cache, compl, + account, + backend::BackendBuilder, + cache, compl, config::{self, DeserializedConfig}, email, flag, folder, man, output, printer::StdoutPrinter, @@ -60,21 +56,19 @@ async fn main() -> Result<()> { // checks mailto command before app initialization let raw_args: Vec = env::args().collect(); if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { - let url = Url::parse(&raw_args[1])?; - let config = DeserializedConfig::from_opt_path(None).await?; - let account_config = config.to_account_config(None)?; - let mut backend = BackendBuilder::new(account_config.clone()).build().await?; - let mut sender = SenderBuilder::new(account_config.clone()).build().await?; - let mut printer = StdoutPrinter::default(); + // let url = Url::parse(&raw_args[1])?; + // let config = DeserializedConfig::from_opt_path(None).await?; + // let account_config = config.to_account_config(None)?; + // let backend = BackendBuilder::new(account_config.clone()).build().await?; + // let mut printer = StdoutPrinter::default(); - email::handlers::mailto( - &account_config, - backend.as_mut(), - sender.as_mut(), - &mut printer, - &url, - ) - .await?; + // email::handlers::mailto( + // &account_config, + // &backend, + // &mut printer, + // &url, + // ) + // .await?; return Ok(()); } @@ -100,37 +94,33 @@ async fn main() -> Result<()> { } let config = DeserializedConfig::from_opt_path(config::args::parse_arg(&m)).await?; - let account_config = config.to_account_config(account::args::parse_arg(&m))?; + let maybe_account_name = account::args::parse_arg(&m); let folder = folder::args::parse_source_arg(&m); let disable_cache = cache::args::parse_disable_cache_flag(&m); - // FIXME: find why account config cannot be borrowed - // let backend_builder = - // BackendBuilder::new(Cow::Borrowed(&account_config)).with_cache_disabled(disable_cache); - let backend_builder = - BackendBuilder::new(account_config.clone()).with_cache_disabled(disable_cache); - let sender_builder = SenderBuilder::new(account_config.clone()); + let backend_builder = BackendBuilder::new(config.clone(), maybe_account_name).await?; + let account_config = &backend_builder.account_config; let mut printer = StdoutPrinter::try_from(&m)?; - #[cfg(feature = "imap-backend")] - if let BackendConfig::Imap(imap_config) = &account_config.backend { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - match imap::args::matches(&m)? { - Some(imap::args::Cmd::Notify(keepalive)) => { - let mut backend = - ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?; - imap::handlers::notify(&mut backend, &folder, keepalive).await?; - return Ok(()); - } - Some(imap::args::Cmd::Watch(keepalive)) => { - let mut backend = - ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?; - imap::handlers::watch(&mut backend, &folder, keepalive).await?; - return Ok(()); - } - _ => (), - } - } + // #[cfg(feature = "imap-backend")] + // if let BackendConfig::Imap(imap_config) = &account_config.backend { + // let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + // match imap::args::matches(&m)? { + // Some(imap::args::Cmd::Notify(keepalive)) => { + // let backend = + // ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?; + // imap::handlers::notify(&mut backend, &folder, keepalive).await?; + // return Ok(()); + // } + // Some(imap::args::Cmd::Watch(keepalive)) => { + // let backend = + // ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?; + // imap::handlers::watch(&mut backend, &folder, keepalive).await?; + // return Ok(()); + // } + // _ => (), + // } + // } match account::args::matches(&m)? { Some(account::args::Cmd::List(max_width)) => { @@ -138,7 +128,7 @@ async fn main() -> Result<()> { return Ok(()); } Some(account::args::Cmd::Sync(strategy, dry_run)) => { - let sync_builder = AccountSyncBuilder::new(account_config, backend_builder) + let sync_builder = AccountSyncBuilder::new(backend_builder.0) .await? .with_some_folders_strategy(strategy) .with_dry_run(dry_run); @@ -158,26 +148,25 @@ async fn main() -> Result<()> { let folder = folder .ok_or_else(|| anyhow!("the folder argument is missing")) .context("cannot create folder")?; - let mut backend = backend_builder.build().await?; - folder::handlers::create(&mut printer, backend.as_mut(), &folder).await?; + let backend = backend_builder.clone().build().await?; + folder::handlers::create(&mut printer, &backend, &folder).await?; return Ok(()); } Some(folder::args::Cmd::List(max_width)) => { - let mut backend = backend_builder.build().await?; - folder::handlers::list(&account_config, &mut printer, backend.as_mut(), max_width) - .await?; + let backend = backend_builder.clone().build().await?; + folder::handlers::list(&account_config, &mut printer, &backend, max_width).await?; return Ok(()); } Some(folder::args::Cmd::Expunge) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.build().await?; - folder::handlers::expunge(&mut printer, backend.as_mut(), &folder).await?; + let backend = backend_builder.clone().build().await?; + folder::handlers::expunge(&mut printer, &backend, &folder).await?; return Ok(()); } Some(folder::args::Cmd::Delete) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.build().await?; - folder::handlers::delete(&mut printer, backend.as_mut(), &folder).await?; + let backend = backend_builder.clone().build().await?; + folder::handlers::delete(&mut printer, &backend, &folder).await?; return Ok(()); } _ => (), @@ -187,13 +176,13 @@ async fn main() -> Result<()> { match email::args::matches(&m)? { Some(email::args::Cmd::Attachments(ids)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; email::handlers::attachments( &account_config, &mut printer, &id_mapper, - backend.as_mut(), + &backend, &folder, ids, ) @@ -202,43 +191,33 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Copy(ids, to_folder)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::copy( - &mut printer, - &id_mapper, - backend.as_mut(), - &folder, - to_folder, - ids, - ) - .await?; + email::handlers::copy(&mut printer, &id_mapper, &backend, &folder, to_folder, ids) + .await?; return Ok(()); } Some(email::args::Cmd::Delete(ids)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::delete(&mut printer, &id_mapper, backend.as_mut(), &folder, ids) - .await?; + email::handlers::delete(&mut printer, &id_mapper, &backend, &folder, ids).await?; return Ok(()); } Some(email::args::Cmd::Forward(id, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let mut sender = sender_builder.build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; email::handlers::forward( &account_config, &mut printer, &id_mapper, - backend.as_mut(), - sender.as_mut(), + &backend, &folder, id, headers, @@ -250,14 +229,14 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::List(max_width, page_size, page)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; email::handlers::list( &account_config, &mut printer, &id_mapper, - backend.as_mut(), + &backend, &folder, max_width, page_size, @@ -269,31 +248,24 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Move(ids, to_folder)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::move_( - &mut printer, - &id_mapper, - backend.as_mut(), - &folder, - to_folder, - ids, - ) - .await?; + email::handlers::move_(&mut printer, &id_mapper, &backend, &folder, to_folder, ids) + .await?; return Ok(()); } Some(email::args::Cmd::Read(ids, text_mime, raw, headers)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; email::handlers::read( &account_config, &mut printer, &id_mapper, - backend.as_mut(), + &backend, &folder, ids, text_mime, @@ -306,16 +278,14 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Reply(id, all, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let mut sender = sender_builder.build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; email::handlers::reply( &account_config, &mut printer, &id_mapper, - backend.as_mut(), - sender.as_mut(), + &backend, &folder, id, all, @@ -328,30 +298,23 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Save(raw_email)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::save( - &mut printer, - &id_mapper, - backend.as_mut(), - &folder, - raw_email, - ) - .await?; + email::handlers::save(&mut printer, &id_mapper, &backend, &folder, raw_email).await?; return Ok(()); } Some(email::args::Cmd::Search(query, max_width, page_size, page)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; email::handlers::search( &account_config, &mut printer, &id_mapper, - backend.as_mut(), + &backend, &folder, query, max_width, @@ -364,14 +327,14 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Sort(criteria, query, max_width, page_size, page)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; email::handlers::sort( &account_config, &mut printer, &id_mapper, - backend.as_mut(), + &backend, &folder, criteria, query, @@ -384,68 +347,39 @@ async fn main() -> Result<()> { return Ok(()); } Some(email::args::Cmd::Send(raw_email)) => { - let mut backend = backend_builder.build().await?; - let mut sender = sender_builder.build().await?; - email::handlers::send( - &account_config, - &mut printer, - backend.as_mut(), - sender.as_mut(), - raw_email, - ) - .await?; + let backend = backend_builder.clone().build().await?; + email::handlers::send(&account_config, &mut printer, &backend, raw_email).await?; return Ok(()); } Some(email::args::Cmd::Flag(m)) => match m { Some(flag::args::Cmd::Set(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - flag::handlers::set( - &mut printer, - &id_mapper, - backend.as_mut(), - &folder, - ids, - flags, - ) - .await?; + flag::handlers::set(&mut printer, &id_mapper, &backend, &folder, ids, flags) + .await?; return Ok(()); } Some(flag::args::Cmd::Add(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - flag::handlers::add( - &mut printer, - &id_mapper, - backend.as_mut(), - &folder, - ids, - flags, - ) - .await?; + flag::handlers::add(&mut printer, &id_mapper, &backend, &folder, ids, flags) + .await?; return Ok(()); } Some(flag::args::Cmd::Remove(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - flag::handlers::remove( - &mut printer, - &id_mapper, - backend.as_mut(), - &folder, - ids, - flags, - ) - .await?; + flag::handlers::remove(&mut printer, &id_mapper, &backend, &folder, ids, flags) + .await?; return Ok(()); } @@ -454,14 +388,14 @@ async fn main() -> Result<()> { Some(email::args::Cmd::Tpl(m)) => match m { Some(tpl::args::Cmd::Forward(id, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; tpl::handlers::forward( &account_config, &mut printer, &id_mapper, - backend.as_mut(), + &backend, &folder, id, headers, @@ -477,14 +411,14 @@ async fn main() -> Result<()> { } Some(tpl::args::Cmd::Reply(id, all, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; tpl::handlers::reply( &account_config, &mut printer, &id_mapper, - backend.as_mut(), + &backend, &folder, id, all, @@ -497,14 +431,14 @@ async fn main() -> Result<()> { } Some(tpl::args::Cmd::Save(tpl)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let mut backend = backend_builder.clone().into_build().await?; - let id_mapper = IdMapper::new(backend.as_ref(), &account_config, &folder)?; + let backend = backend_builder.clone().build().await?; + let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; tpl::handlers::save( &account_config, &mut printer, &id_mapper, - backend.as_mut(), + &backend, &folder, tpl, ) @@ -513,33 +447,16 @@ async fn main() -> Result<()> { return Ok(()); } Some(tpl::args::Cmd::Send(tpl)) => { - let mut backend = backend_builder.clone().into_build().await?; - let mut sender = sender_builder.build().await?; - tpl::handlers::send( - &account_config, - &mut printer, - backend.as_mut(), - sender.as_mut(), - tpl, - ) - .await?; + let backend = backend_builder.clone().build().await?; + tpl::handlers::send(&account_config, &mut printer, &backend, tpl).await?; return Ok(()); } _ => (), }, Some(email::args::Cmd::Write(headers, body)) => { - let mut backend = backend_builder.build().await?; - let mut sender = sender_builder.build().await?; - email::handlers::write( - &account_config, - &mut printer, - backend.as_mut(), - sender.as_mut(), - headers, - body, - ) - .await?; + let backend = backend_builder.clone().build().await?; + email::handlers::write(&account_config, &mut printer, &backend, headers, body).await?; return Ok(()); } diff --git a/src/ui/editor.rs b/src/ui/editor.rs index f9a7bda..34eb59d 100644 --- a/src/ui/editor.rs +++ b/src/ui/editor.rs @@ -1,9 +1,7 @@ use anyhow::{Context, Result}; use email::{ account::AccountConfig, - backend::Backend, email::{local_draft_path, remove_local_draft, Flag, Flags}, - sender::Sender, }; use log::debug; use mml::MmlCompilerBuilder; @@ -11,6 +9,7 @@ use process::Cmd; use std::{env, fs}; use crate::{ + backend::Backend, printer::Printer, ui::choice::{self, PostEditChoice, PreEditChoice}, }; @@ -45,8 +44,7 @@ pub async fn open_with_local_draft() -> Result { pub async fn edit_tpl_with_editor( config: &AccountConfig, printer: &mut P, - backend: &mut dyn Backend, - sender: &mut dyn Sender, + backend: &Backend, mut tpl: String, ) -> Result<()> { let draft = local_draft_path(); @@ -86,13 +84,13 @@ pub async fn edit_tpl_with_editor( let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; - sender.send(&email).await?; + backend.send_raw_message(&email).await?; - if config.email_sending_save_copy { + if config.email_sending_save_copy.unwrap_or_default() { let sent_folder = config.sent_folder_alias()?; printer.print_log(format!("Adding email to the {} folder…", sent_folder))?; backend - .add_email(&sent_folder, &email, &Flags::from_iter([Flag::Seen])) + .add_raw_message_with_flag(&sent_folder, &email, Flag::Seen) .await?; } @@ -117,7 +115,7 @@ pub async fn edit_tpl_with_editor( let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; backend - .add_email( + .add_raw_message_with_flags( "drafts", &email, &Flags::from_iter([Flag::Seen, Flag::Draft]), From 1f88b27468accf153d15cacefdb1b00b6007cf6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 26 Nov 2023 12:16:07 +0100 Subject: [PATCH 02/29] init backend override with list envelopes and send message --- config.sample.toml | 27 +++++-- src/backend.rs | 106 ++++++++++++++++++++-------- src/config/prelude.rs | 58 +++++---------- src/domain/account/config.rs | 10 ++- src/domain/email/envelope/config.rs | 13 ++++ src/domain/email/envelope/mod.rs | 1 + src/domain/email/message/config.rs | 13 ++++ src/domain/email/message/mod.rs | 1 + src/domain/email/mod.rs | 2 + 9 files changed, 154 insertions(+), 77 deletions(-) create mode 100644 src/domain/email/envelope/config.rs create mode 100644 src/domain/email/envelope/mod.rs create mode 100644 src/domain/email/message/config.rs create mode 100644 src/domain/email/message/mod.rs diff --git a/config.sample.toml b/config.sample.toml index 2fd521c..956e5e5 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -10,9 +10,26 @@ email = "example@localhost" # listing envelopes or copying messages. backend = "imap" -imap.host = "imap.gmail.com" -imap.port = 993 -imap.login = "example@localhost" -imap.auth = "passwd" -# imap.Some.passwd.cmd = "pass show gmail" +# Override the backend used for sending messages. +message.send.backend = "smtp" + +# IMAP config +imap.host = "localhost" +imap.port = 3143 +imap.login = "example@localhost" +imap.ssl = false +imap.starttls = false +imap.insecure = true +imap.auth = "passwd" +imap.passwd.raw = "example" + +# SMTP config +smtp.host = "localhost" +smtp.port = 3025 +smtp.login = "example@localhost" +smtp.ssl = false +smtp.starttls = false +smtp.insecure = true +smtp.auth = "passwd" +smtp.passwd.raw = "example" diff --git a/src/backend.rs b/src/backend.rs index 67d1882..ad2d19f 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -10,7 +10,10 @@ use email::smtp::{SmtpClientBuilder, SmtpClientSync}; use email::{ account::AccountConfig, config::Config, - folder::list::{imap::ListFoldersImap, maildir::ListFoldersMaildir}, + email::{ + envelope::list::{imap::ListEnvelopesImap, maildir::ListEnvelopesMaildir}, + message::send_raw::{sendmail::SendRawMessageSendmail, smtp::SendRawMessageSmtp}, + }, maildir::{MaildirSessionBuilder, MaildirSessionSync}, sendmail::SendmailContext, }; @@ -18,11 +21,9 @@ use serde::{Deserialize, Serialize}; use crate::config::DeserializedConfig; -#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum BackendKind { - #[default] - None, Maildir, #[cfg(feature = "imap-backend")] Imap, @@ -35,8 +36,6 @@ pub enum BackendKind { #[derive(Clone, Default)] pub struct BackendContextBuilder { - account_config: AccountConfig, - #[cfg(feature = "imap-backend")] imap: Option, maildir: Option, @@ -93,7 +92,7 @@ pub struct BackendBuilder(pub email::backend::BackendBuilder) -> Result { - let (account_name, deserialized_account_config) = match account_name { + let (account_name, mut deserialized_account_config) = match account_name { Some("default") | Some("") | None => config .accounts .iter() @@ -111,6 +110,25 @@ impl BackendBuilder { .ok_or_else(|| anyhow!("cannot find account {name}")), }?; + println!( + "deserialized_account_config: {:#?}", + deserialized_account_config + ); + + #[cfg(feature = "imap-backend")] + if let Some(imap_config) = deserialized_account_config.imap.as_mut() { + imap_config + .auth + .replace_undefined_keyring_entries(&account_name); + } + + #[cfg(feature = "smtp-sender")] + if let Some(smtp_config) = deserialized_account_config.smtp.as_mut() { + smtp_config + .auth + .replace_undefined_keyring_entries(&account_name); + } + let config = Config { display_name: config.display_name, signature_delim: config.signature_delim, @@ -166,10 +184,9 @@ impl BackendBuilder { )), }; - let account_config = config.account(account_name)?; + let account_config = config.account(&account_name)?; let backend_ctx_builder = BackendContextBuilder { - account_config: account_config.clone(), maildir: deserialized_account_config .maildir .as_ref() @@ -199,28 +216,57 @@ impl BackendBuilder { ..Default::default() }; - let backend_builder = - email::backend::BackendBuilder::new(account_config.clone(), backend_ctx_builder) - .with_list_folders(move |ctx| { - println!( - "deserialized_account_config: {:#?}", - deserialized_account_config - ); - match deserialized_account_config.backend { - BackendKind::Maildir if ctx.maildir.is_some() => { - ListFoldersMaildir::new(ctx.maildir.as_ref().unwrap()) - } - #[cfg(feature = "imap-backend")] - BackendKind::Imap if ctx.imap.is_some() => { - ListFoldersImap::new(ctx.imap.as_ref().unwrap()) - } - #[cfg(feature = "notmuch-backend")] - BackendKind::Notmuch if ctx.notmuch.is_some() => { - ListFoldersNotmuch::new(ctx.notmuch.as_ref().unwrap()) - } - _ => None, - } + let mut backend_builder = + email::backend::BackendBuilder::new(account_config.clone(), backend_ctx_builder); + + let list_envelopes = deserialized_account_config + .envelope + .as_ref() + .and_then(|envelope| envelope.list.as_ref()) + .and_then(|send| send.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match list_envelopes { + Some(BackendKind::Maildir) => { + backend_builder = backend_builder.with_list_envelopes(|ctx| { + ctx.maildir.as_ref().and_then(ListEnvelopesMaildir::new) }); + } + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_list_envelopes(|ctx| ctx.imap.as_ref().and_then(ListEnvelopesImap::new)); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder.with_list_envelopes(|ctx| { + ctx.notmuch.as_ref().and_then(ListEnvelopesNotmuch::new) + }); + } + _ => (), + } + + let send_msg = deserialized_account_config + .message + .as_ref() + .and_then(|msg| msg.send.as_ref()) + .and_then(|send| send.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match send_msg { + #[cfg(feature = "smtp-sender")] + Some(BackendKind::Smtp) => { + backend_builder = backend_builder.with_send_raw_message(|ctx| { + ctx.smtp.as_ref().and_then(SendRawMessageSmtp::new) + }); + } + Some(BackendKind::Sendmail) => { + backend_builder = backend_builder.with_send_raw_message(|ctx| { + ctx.sendmail.as_ref().and_then(SendRawMessageSendmail::new) + }); + } + _ => (), + } Ok(Self(backend_builder)) } diff --git a/src/config/prelude.rs b/src/config/prelude.rs index 861fb0a..0b24daa 100644 --- a/src/config/prelude.rs +++ b/src/config/prelude.rs @@ -160,7 +160,7 @@ pub enum ImapAuthConfigDef { #[serde(remote = "PasswdConfig")] pub struct ImapPasswdConfigDef { #[serde( - rename = "imap-passwd", + rename = "passwd", with = "SecretDef", default, skip_serializing_if = "Secret::is_undefined" @@ -335,19 +335,19 @@ pub enum EmailTextPlainFormatDef { pub struct OptionSmtpConfigDef; #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum OptionSmtpConfig { - #[default] - #[serde(skip_serializing)] - None, - Some(#[serde(with = "SmtpConfigDef")] SmtpConfig), +pub struct OptionSmtpConfig { + #[serde(default, skip)] + is_none: bool, + #[serde(flatten, with = "SmtpConfigDef")] + inner: SmtpConfig, } impl From for Option { fn from(config: OptionSmtpConfig) -> Option { - match config { - OptionSmtpConfig::None => None, - OptionSmtpConfig::Some(config) => Some(config), + if config.is_none { + None + } else { + Some(config.inner) } } } @@ -356,17 +356,11 @@ impl From for Option { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(remote = "SmtpConfig")] struct SmtpConfigDef { - #[serde(rename = "smtp-host")] pub host: String, - #[serde(rename = "smtp-port")] pub port: u16, - #[serde(rename = "smtp-ssl")] pub ssl: Option, - #[serde(rename = "smtp-starttls")] pub starttls: Option, - #[serde(rename = "smtp-insecure")] pub insecure: Option, - #[serde(rename = "smtp-login")] pub login: String, #[serde(flatten, with = "SmtpAuthConfigDef")] pub auth: SmtpAuthConfig, @@ -374,7 +368,7 @@ struct SmtpConfigDef { #[cfg(feature = "smtp-sender")] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(remote = "SmtpAuthConfig", tag = "smtp-auth")] +#[serde(remote = "SmtpAuthConfig", tag = "auth")] pub enum SmtpAuthConfigDef { #[serde(rename = "passwd", alias = "password", with = "SmtpPasswdConfigDef")] Passwd(#[serde(default)] PasswdConfig), @@ -386,7 +380,7 @@ pub enum SmtpAuthConfigDef { #[serde(remote = "PasswdConfig", default)] pub struct SmtpPasswdConfigDef { #[serde( - rename = "smtp-passwd", + rename = "passwd", with = "SecretDef", default, skip_serializing_if = "Secret::is_undefined" @@ -395,32 +389,26 @@ pub struct SmtpPasswdConfigDef { } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(remote = "OAuth2Config")] +#[serde(remote = "OAuth2Config", rename_all = "kebab-case")] pub struct SmtpOAuth2ConfigDef { - #[serde(rename = "smtp-oauth2-method", with = "OAuth2MethodDef", default)] + #[serde(with = "OAuth2MethodDef", default)] pub method: OAuth2Method, - #[serde(rename = "smtp-oauth2-client-id")] pub client_id: String, #[serde( - rename = "smtp-oauth2-client-secret", with = "SecretDef", default, skip_serializing_if = "Secret::is_undefined" )] pub client_secret: Secret, - #[serde(rename = "smtp-oauth2-auth-url")] pub auth_url: String, - #[serde(rename = "smtp-oauth2-token-url")] pub token_url: String, #[serde( - rename = "smtp-oauth2-access-token", with = "SecretDef", default, skip_serializing_if = "Secret::is_undefined" )] pub access_token: Secret, #[serde( - rename = "smtp-oauth2-refresh-token", with = "SecretDef", default, skip_serializing_if = "Secret::is_undefined" @@ -428,17 +416,11 @@ pub struct SmtpOAuth2ConfigDef { pub refresh_token: Secret, #[serde(flatten, with = "SmtpOAuth2ScopesDef")] pub scopes: OAuth2Scopes, - #[serde(rename = "smtp-oauth2-pkce", default)] + #[serde(default)] pub pkce: bool, - #[serde( - rename = "imap-oauth2-redirect-host", - default = "OAuth2Config::default_redirect_host" - )] + #[serde(default = "OAuth2Config::default_redirect_host")] pub redirect_host: String, - #[serde( - rename = "imap-oauth2-redirect-port", - default = "OAuth2Config::default_redirect_port" - )] + #[serde(default = "OAuth2Config::default_redirect_port")] pub redirect_port: u16, } @@ -476,11 +458,7 @@ impl From for Option { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(remote = "SendmailConfig", rename_all = "kebab-case")] pub struct SendmailConfigDef { - #[serde( - rename = "sendmail-cmd", - with = "CmdDef", - default = "sendmail_default_cmd" - )] + #[serde(with = "CmdDef", default = "sendmail_default_cmd")] cmd: Cmd, } diff --git a/src/domain/account/config.rs b/src/domain/account/config.rs index e574f4c..5639306 100644 --- a/src/domain/account/config.rs +++ b/src/domain/account/config.rs @@ -18,7 +18,10 @@ use email::{ use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf}; -use crate::{backend::BackendKind, config::prelude::*}; +use crate::{ + backend::BackendKind, config::prelude::*, email::envelope::config::EnvelopeConfig, + message::config::MessageConfig, +}; /// Represents all existing kind of account config. #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] @@ -51,7 +54,10 @@ pub struct DeserializedAccountConfig { #[serde(default, with = "OptionFolderSyncStrategyDef")] pub sync_folders_strategy: Option, - pub backend: BackendKind, + pub backend: Option, + + pub envelope: Option, + pub message: Option, #[cfg(feature = "imap-backend")] #[serde(default, with = "OptionImapConfigDef")] diff --git a/src/domain/email/envelope/config.rs b/src/domain/email/envelope/config.rs new file mode 100644 index 0000000..a13476e --- /dev/null +++ b/src/domain/email/envelope/config.rs @@ -0,0 +1,13 @@ +use ::serde::{Deserialize, Serialize}; + +use crate::backend::BackendKind; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct EnvelopeConfig { + pub list: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct EnvelopeListConfig { + pub backend: Option, +} diff --git a/src/domain/email/envelope/mod.rs b/src/domain/email/envelope/mod.rs new file mode 100644 index 0000000..ef68c36 --- /dev/null +++ b/src/domain/email/envelope/mod.rs @@ -0,0 +1 @@ +pub mod config; diff --git a/src/domain/email/message/config.rs b/src/domain/email/message/config.rs new file mode 100644 index 0000000..16eb198 --- /dev/null +++ b/src/domain/email/message/config.rs @@ -0,0 +1,13 @@ +use ::serde::{Deserialize, Serialize}; + +use crate::backend::BackendKind; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct MessageConfig { + pub send: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct MessageSendConfig { + pub backend: Option, +} diff --git a/src/domain/email/message/mod.rs b/src/domain/email/message/mod.rs new file mode 100644 index 0000000..ef68c36 --- /dev/null +++ b/src/domain/email/message/mod.rs @@ -0,0 +1 @@ +pub mod config; diff --git a/src/domain/email/mod.rs b/src/domain/email/mod.rs index b0b957b..5de2dfe 100644 --- a/src/domain/email/mod.rs +++ b/src/domain/email/mod.rs @@ -1,2 +1,4 @@ pub mod args; +pub mod envelope; pub mod handlers; +pub mod message; From 9f6a9a13335494ae84cee0413401bbf8ef494d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 26 Nov 2023 14:16:55 +0100 Subject: [PATCH 03/29] plug folder features --- src/backend.rs | 153 +++++++++++++++++++++++++++++++++-- src/domain/account/config.rs | 5 +- src/domain/folder/config.rs | 37 +++++++++ src/domain/folder/mod.rs | 12 +-- 4 files changed, 193 insertions(+), 14 deletions(-) create mode 100644 src/domain/folder/config.rs diff --git a/src/backend.rs b/src/backend.rs index ad2d19f..588af4e 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -14,6 +14,13 @@ use email::{ envelope::list::{imap::ListEnvelopesImap, maildir::ListEnvelopesMaildir}, message::send_raw::{sendmail::SendRawMessageSendmail, smtp::SendRawMessageSmtp}, }, + folder::{ + add::{imap::AddFolderImap, maildir::AddFolderMaildir}, + delete::{imap::DeleteFolderImap, maildir::DeleteFolderMaildir}, + expunge::{imap::ExpungeFolderImap, maildir::ExpungeFolderMaildir}, + list::{imap::ListFoldersImap, maildir::ListFoldersMaildir}, + purge::imap::PurgeFolderImap, + }, maildir::{MaildirSessionBuilder, MaildirSessionSync}, sendmail::SendmailContext, }; @@ -110,11 +117,6 @@ impl BackendBuilder { .ok_or_else(|| anyhow!("cannot find account {name}")), }?; - println!( - "deserialized_account_config: {:#?}", - deserialized_account_config - ); - #[cfg(feature = "imap-backend")] if let Some(imap_config) = deserialized_account_config.imap.as_mut() { imap_config @@ -200,6 +202,13 @@ impl BackendBuilder { .map(|imap_config| { ImapSessionBuilder::new(account_config.clone(), imap_config.clone()) }), + #[cfg(feature = "notmuch-backend")] + notmuch: deserialized_account_config + .notmuch + .as_ref() + .map(|notmuch_config| { + NotmuchSessionBuilder::new(account_config.clone(), notmuch_config.clone()) + }), #[cfg(feature = "smtp-sender")] smtp: deserialized_account_config .smtp @@ -213,12 +222,144 @@ impl BackendBuilder { .map(|sendmail_config| { SendmailContext::new(account_config.clone(), sendmail_config.clone()) }), - ..Default::default() }; let mut backend_builder = email::backend::BackendBuilder::new(account_config.clone(), backend_ctx_builder); + let add_folder = deserialized_account_config + .folder + .as_ref() + .and_then(|folder| folder.add.as_ref()) + .and_then(|add| add.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match add_folder { + Some(BackendKind::Maildir) => { + backend_builder = backend_builder + .with_add_folder(|ctx| ctx.maildir.as_ref().and_then(AddFolderMaildir::new)); + } + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_add_folder(|ctx| ctx.imap.as_ref().and_then(AddFolderImap::new)); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder + .with_add_folder(|ctx| ctx.notmuch.as_ref().and_then(AddFolderNotmuch::new)); + } + _ => (), + } + + let list_folders = deserialized_account_config + .folder + .as_ref() + .and_then(|folder| folder.list.as_ref()) + .and_then(|list| list.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match list_folders { + Some(BackendKind::Maildir) => { + backend_builder = backend_builder.with_list_folders(|ctx| { + ctx.maildir.as_ref().and_then(ListFoldersMaildir::new) + }); + } + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_list_folders(|ctx| ctx.imap.as_ref().and_then(ListFoldersImap::new)); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder.with_list_folders(|ctx| { + ctx.notmuch.as_ref().and_then(ListFoldersNotmuch::new) + }); + } + _ => (), + } + + let expunge_folder = deserialized_account_config + .folder + .as_ref() + .and_then(|folder| folder.expunge.as_ref()) + .and_then(|expunge| expunge.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match expunge_folder { + Some(BackendKind::Maildir) => { + backend_builder = backend_builder.with_expunge_folder(|ctx| { + ctx.maildir.as_ref().and_then(ExpungeFolderMaildir::new) + }); + } + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_expunge_folder(|ctx| ctx.imap.as_ref().and_then(ExpungeFolderImap::new)); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder.with_expunge_folder(|ctx| { + ctx.notmuch.as_ref().and_then(ExpungeFolderNotmuch::new) + }); + } + _ => (), + } + + let purge_folder = deserialized_account_config + .folder + .as_ref() + .and_then(|folder| folder.purge.as_ref()) + .and_then(|purge| purge.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match purge_folder { + // TODO + // Some(BackendKind::Maildir) => { + // backend_builder = backend_builder + // .with_purge_folder(|ctx| ctx.maildir.as_ref().and_then(PurgeFolderMaildir::new)); + // } + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_purge_folder(|ctx| ctx.imap.as_ref().and_then(PurgeFolderImap::new)); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder.with_purge_folder(|ctx| { + ctx.notmuch.as_ref().and_then(PurgeFolderNotmuch::new) + }); + } + _ => (), + } + + let delete_folder = deserialized_account_config + .folder + .as_ref() + .and_then(|folder| folder.delete.as_ref()) + .and_then(|delete| delete.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match delete_folder { + Some(BackendKind::Maildir) => { + backend_builder = backend_builder.with_delete_folder(|ctx| { + ctx.maildir.as_ref().and_then(DeleteFolderMaildir::new) + }); + } + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_delete_folder(|ctx| ctx.imap.as_ref().and_then(DeleteFolderImap::new)); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder.with_delete_folder(|ctx| { + ctx.notmuch.as_ref().and_then(DeleteFolderNotmuch::new) + }); + } + _ => (), + } + let list_envelopes = deserialized_account_config .envelope .as_ref() diff --git a/src/domain/account/config.rs b/src/domain/account/config.rs index 5639306..ca8f841 100644 --- a/src/domain/account/config.rs +++ b/src/domain/account/config.rs @@ -19,8 +19,8 @@ use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf}; use crate::{ - backend::BackendKind, config::prelude::*, email::envelope::config::EnvelopeConfig, - message::config::MessageConfig, + backend::BackendKind, config::prelude::*, domain::config::FolderConfig, + email::envelope::config::EnvelopeConfig, message::config::MessageConfig, }; /// Represents all existing kind of account config. @@ -56,6 +56,7 @@ pub struct DeserializedAccountConfig { pub backend: Option, + pub folder: Option, pub envelope: Option, pub message: Option, diff --git a/src/domain/folder/config.rs b/src/domain/folder/config.rs new file mode 100644 index 0000000..105db57 --- /dev/null +++ b/src/domain/folder/config.rs @@ -0,0 +1,37 @@ +use ::serde::{Deserialize, Serialize}; + +use crate::backend::BackendKind; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct FolderConfig { + pub add: Option, + pub list: Option, + pub expunge: Option, + pub purge: Option, + pub delete: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct FolderAddConfig { + pub backend: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct FolderListConfig { + pub backend: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct FolderExpungeConfig { + pub backend: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct FolderPurgeConfig { + pub backend: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct FolderDeleteConfig { + pub backend: Option, +} diff --git a/src/domain/folder/mod.rs b/src/domain/folder/mod.rs index 45a1338..321a172 100644 --- a/src/domain/folder/mod.rs +++ b/src/domain/folder/mod.rs @@ -1,8 +1,8 @@ -pub mod folder; -pub use folder::*; - -pub mod folders; -pub use folders::*; - pub mod args; +pub mod config; +pub mod folder; +pub mod folders; pub mod handlers; + +pub use folder::*; +pub use folders::*; From 20f6973c55528f34e07ba20432c6219da9c4917e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Mon, 27 Nov 2023 15:49:55 +0100 Subject: [PATCH 04/29] plug missing other backend features --- src/backend.rs | 261 ++++++++++++++++++++++- src/domain/account/config.rs | 8 +- src/domain/email/envelope/config.rs | 6 + src/domain/email/envelope/flag/config.rs | 25 +++ src/domain/email/envelope/flag/mod.rs | 1 + src/domain/email/envelope/mod.rs | 1 + src/domain/email/message/config.rs | 31 +++ 7 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 src/domain/email/envelope/flag/config.rs create mode 100644 src/domain/email/envelope/flag/mod.rs diff --git a/src/backend.rs b/src/backend.rs index 588af4e..c30805a 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -11,8 +11,26 @@ use email::{ account::AccountConfig, config::Config, email::{ - envelope::list::{imap::ListEnvelopesImap, maildir::ListEnvelopesMaildir}, - message::send_raw::{sendmail::SendRawMessageSendmail, smtp::SendRawMessageSmtp}, + envelope::{ + get::{imap::GetEnvelopeImap, maildir::GetEnvelopeMaildir}, + list::{imap::ListEnvelopesImap, maildir::ListEnvelopesMaildir}, + }, + flag::{ + add::{imap::AddFlagsImap, maildir::AddFlagsMaildir}, + remove::{imap::RemoveFlagsImap, maildir::RemoveFlagsMaildir}, + set::{imap::SetFlagsImap, maildir::SetFlagsMaildir}, + }, + message::{ + add_raw::imap::AddRawMessageImap, + add_raw_with_flags::{ + imap::AddRawMessageWithFlagsImap, maildir::AddRawMessageWithFlagsMaildir, + }, + copy::{imap::CopyMessagesImap, maildir::CopyMessagesMaildir}, + get::imap::GetMessagesImap, + move_::{imap::MoveMessagesImap, maildir::MoveMessagesMaildir}, + peek::{imap::PeekMessagesImap, maildir::PeekMessagesMaildir}, + send_raw::{sendmail::SendRawMessageSendmail, smtp::SendRawMessageSmtp}, + }, }, folder::{ add::{imap::AddFolderImap, maildir::AddFolderMaildir}, @@ -360,6 +378,33 @@ impl BackendBuilder { _ => (), } + let get_envelope = deserialized_account_config + .envelope + .as_ref() + .and_then(|envelope| envelope.get.as_ref()) + .and_then(|get| get.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match get_envelope { + Some(BackendKind::Maildir) => { + backend_builder = backend_builder.with_get_envelope(|ctx| { + ctx.maildir.as_ref().and_then(GetEnvelopeMaildir::new) + }); + } + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_get_envelope(|ctx| ctx.imap.as_ref().and_then(GetEnvelopeImap::new)); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder.with_get_envelope(|ctx| { + ctx.notmuch.as_ref().and_then(GetEnvelopeNotmuch::new) + }); + } + _ => (), + } + let list_envelopes = deserialized_account_config .envelope .as_ref() @@ -387,6 +432,83 @@ impl BackendBuilder { _ => (), } + let add_flags = deserialized_account_config + .flag + .as_ref() + .and_then(|flag| flag.add.as_ref()) + .and_then(|add| add.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match add_flags { + Some(BackendKind::Maildir) => { + backend_builder = backend_builder + .with_add_flags(|ctx| ctx.maildir.as_ref().and_then(AddFlagsMaildir::new)); + } + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_add_flags(|ctx| ctx.imap.as_ref().and_then(AddFlagsImap::new)); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder + .with_add_flags(|ctx| ctx.notmuch.as_ref().and_then(AddFlagsNotmuch::new)); + } + _ => (), + } + + let set_flags = deserialized_account_config + .flag + .as_ref() + .and_then(|flag| flag.set.as_ref()) + .and_then(|set| set.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match set_flags { + Some(BackendKind::Maildir) => { + backend_builder = backend_builder + .with_set_flags(|ctx| ctx.maildir.as_ref().and_then(SetFlagsMaildir::new)); + } + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_set_flags(|ctx| ctx.imap.as_ref().and_then(SetFlagsImap::new)); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder + .with_set_flags(|ctx| ctx.notmuch.as_ref().and_then(SetFlagsNotmuch::new)); + } + _ => (), + } + + let remove_flags = deserialized_account_config + .flag + .as_ref() + .and_then(|flag| flag.remove.as_ref()) + .and_then(|remove| remove.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match remove_flags { + Some(BackendKind::Maildir) => { + backend_builder = backend_builder.with_remove_flags(|ctx| { + ctx.maildir.as_ref().and_then(RemoveFlagsMaildir::new) + }); + } + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_remove_flags(|ctx| ctx.imap.as_ref().and_then(RemoveFlagsImap::new)); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder.with_remove_flags(|ctx| { + ctx.notmuch.as_ref().and_then(RemoveFlagsNotmuch::new) + }); + } + _ => (), + } + let send_msg = deserialized_account_config .message .as_ref() @@ -409,6 +531,141 @@ impl BackendBuilder { _ => (), } + let add_msg = deserialized_account_config + .message + .as_ref() + .and_then(|msg| msg.add.as_ref()) + .and_then(|add| add.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match add_msg { + Some(BackendKind::Maildir) => { + backend_builder = backend_builder.with_add_raw_message_with_flags(|ctx| { + ctx.maildir + .as_ref() + .and_then(AddRawMessageWithFlagsMaildir::new) + }); + } + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_add_raw_message(|ctx| ctx.imap.as_ref().and_then(AddRawMessageImap::new)) + .with_add_raw_message_with_flags(|ctx| { + ctx.imap.as_ref().and_then(AddRawMessageWithFlagsImap::new) + }); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder.with_add_raw_message(|ctx| { + ctx.notmuch.as_ref().and_then(AddRawMessageNotmuch::new) + }); + } + _ => (), + } + + let peek_msgs = deserialized_account_config + .message + .as_ref() + .and_then(|msg| msg.peek.as_ref()) + .and_then(|peek| peek.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match peek_msgs { + Some(BackendKind::Maildir) => { + backend_builder = backend_builder.with_peek_messages(|ctx| { + ctx.maildir.as_ref().and_then(PeekMessagesMaildir::new) + }); + } + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_peek_messages(|ctx| ctx.imap.as_ref().and_then(PeekMessagesImap::new)); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder.with_peek_messages(|ctx| { + ctx.notmuch.as_ref().and_then(PeekMessagesNotmuch::new) + }); + } + _ => (), + } + + let get_msgs = deserialized_account_config + .message + .as_ref() + .and_then(|msg| msg.get.as_ref()) + .and_then(|get| get.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match get_msgs { + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_get_messages(|ctx| ctx.imap.as_ref().and_then(GetMessagesImap::new)); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder.with_get_messages(|ctx| { + ctx.notmuch.as_ref().and_then(GetMessagesNotmuch::new) + }); + } + _ => (), + } + + let copy_msgs = deserialized_account_config + .message + .as_ref() + .and_then(|msg| msg.copy.as_ref()) + .and_then(|copy| copy.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match copy_msgs { + Some(BackendKind::Maildir) => { + backend_builder = backend_builder.with_copy_messages(|ctx| { + ctx.maildir.as_ref().and_then(CopyMessagesMaildir::new) + }); + } + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_copy_messages(|ctx| ctx.imap.as_ref().and_then(CopyMessagesImap::new)); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder.with_copy_messages(|ctx| { + ctx.notmuch.as_ref().and_then(CopyMessagesNotmuch::new) + }); + } + _ => (), + } + + let move_msgs = deserialized_account_config + .message + .as_ref() + .and_then(|msg| msg.move_.as_ref()) + .and_then(|move_| move_.backend.as_ref()) + .or_else(|| deserialized_account_config.backend.as_ref()); + + match move_msgs { + Some(BackendKind::Maildir) => { + backend_builder = backend_builder.with_move_messages(|ctx| { + ctx.maildir.as_ref().and_then(MoveMessagesMaildir::new) + }); + } + #[cfg(feature = "imap-backend")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_move_messages(|ctx| ctx.imap.as_ref().and_then(MoveMessagesImap::new)); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder.with_move_messages(|ctx| { + ctx.notmuch.as_ref().and_then(MoveMessagesNotmuch::new) + }); + } + _ => (), + } + Ok(Self(backend_builder)) } } diff --git a/src/domain/account/config.rs b/src/domain/account/config.rs index ca8f841..529d9cb 100644 --- a/src/domain/account/config.rs +++ b/src/domain/account/config.rs @@ -19,8 +19,11 @@ use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf}; use crate::{ - backend::BackendKind, config::prelude::*, domain::config::FolderConfig, - email::envelope::config::EnvelopeConfig, message::config::MessageConfig, + backend::BackendKind, + config::prelude::*, + domain::config::FolderConfig, + email::envelope::{config::EnvelopeConfig, flag::config::FlagConfig}, + message::config::MessageConfig, }; /// Represents all existing kind of account config. @@ -58,6 +61,7 @@ pub struct DeserializedAccountConfig { pub folder: Option, pub envelope: Option, + pub flag: Option, pub message: Option, #[cfg(feature = "imap-backend")] diff --git a/src/domain/email/envelope/config.rs b/src/domain/email/envelope/config.rs index a13476e..35a5629 100644 --- a/src/domain/email/envelope/config.rs +++ b/src/domain/email/envelope/config.rs @@ -5,9 +5,15 @@ use crate::backend::BackendKind; #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct EnvelopeConfig { pub list: Option, + pub get: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct EnvelopeListConfig { pub backend: Option, } + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct EnvelopeGetConfig { + pub backend: Option, +} diff --git a/src/domain/email/envelope/flag/config.rs b/src/domain/email/envelope/flag/config.rs new file mode 100644 index 0000000..af01500 --- /dev/null +++ b/src/domain/email/envelope/flag/config.rs @@ -0,0 +1,25 @@ +use ::serde::{Deserialize, Serialize}; + +use crate::backend::BackendKind; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct FlagConfig { + pub add: Option, + pub set: Option, + pub remove: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct FlagAddConfig { + pub backend: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct FlagSetConfig { + pub backend: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct FlagRemoveConfig { + pub backend: Option, +} diff --git a/src/domain/email/envelope/flag/mod.rs b/src/domain/email/envelope/flag/mod.rs new file mode 100644 index 0000000..ef68c36 --- /dev/null +++ b/src/domain/email/envelope/flag/mod.rs @@ -0,0 +1 @@ +pub mod config; diff --git a/src/domain/email/envelope/mod.rs b/src/domain/email/envelope/mod.rs index ef68c36..abaed0b 100644 --- a/src/domain/email/envelope/mod.rs +++ b/src/domain/email/envelope/mod.rs @@ -1 +1,2 @@ pub mod config; +pub mod flag; diff --git a/src/domain/email/message/config.rs b/src/domain/email/message/config.rs index 16eb198..abb2072 100644 --- a/src/domain/email/message/config.rs +++ b/src/domain/email/message/config.rs @@ -4,10 +4,41 @@ use crate::backend::BackendKind; #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct MessageConfig { + pub add: Option, pub send: Option, + pub peek: Option, + pub get: Option, + pub copy: Option, + #[serde(rename = "move")] + pub move_: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct MessageAddConfig { + pub backend: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct MessageSendConfig { pub backend: Option, } + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct MessagePeekConfig { + pub backend: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct MessageGetConfig { + pub backend: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct MessageCopyConfig { + pub backend: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct MessageMoveConfig { + pub backend: Option, +} From 7629a66c9c79708861b97b0fc14b7d7bc5808e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Mon, 27 Nov 2023 17:15:34 +0100 Subject: [PATCH 05/29] use email-lib git instead of path --- Cargo.lock | 48 +++++++++++++++++++++++------------------- Cargo.toml | 8 +++---- src/cache/id_mapper.rs | 1 + 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3bfaa3..d220ddf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -984,6 +984,7 @@ dependencies = [ [[package]] name = "email-lib" version = "0.15.3" +source = "git+https://git.sr.ht/~soywod/pimalaya#012d31179908063affe3eec77365e5ac86c436fe" dependencies = [ "advisory-lock", "anyhow", @@ -2069,15 +2070,15 @@ dependencies = [ "keyring-lib", "log", "md5", - "mml-lib 1.0.1", - "oauth-lib 0.1.0", + "mml-lib 1.0.1 (git+https://git.sr.ht/~soywod/pimalaya)", + "oauth-lib 0.1.0 (git+https://git.sr.ht/~soywod/pimalaya)", "once_cell", "process-lib", "rusqlite", "secret-lib", "serde", "serde_json", - "shellexpand-utils 0.1.0", + "shellexpand-utils 0.1.0 (git+https://git.sr.ht/~soywod/pimalaya)", "tempfile", "termcolor", "terminal_size", @@ -2684,21 +2685,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "mml-lib" -version = "1.0.1" -dependencies = [ - "async-recursion", - "chumsky", - "log", - "mail-builder", - "mail-parser", - "nanohtml2text", - "shellexpand-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror", - "tree_magic_mini", -] - [[package]] name = "mml-lib" version = "1.0.1" @@ -2721,6 +2707,22 @@ dependencies = [ "tree_magic_mini", ] +[[package]] +name = "mml-lib" +version = "1.0.1" +source = "git+https://git.sr.ht/~soywod/pimalaya#012d31179908063affe3eec77365e5ac86c436fe" +dependencies = [ + "async-recursion", + "chumsky", + "log", + "mail-builder", + "mail-parser", + "nanohtml2text", + "shellexpand-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror", + "tree_magic_mini", +] + [[package]] name = "nanohtml2text" version = "0.1.4" @@ -2846,6 +2848,8 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "oauth-lib" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1484d9864dbf6b55b3785380631a253fa0ff7f8e1bbb078bfd7effd11283d61" dependencies = [ "log", "oauth2", @@ -2858,8 +2862,7 @@ dependencies = [ [[package]] name = "oauth-lib" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1484d9864dbf6b55b3785380631a253fa0ff7f8e1bbb078bfd7effd11283d61" +source = "git+https://git.sr.ht/~soywod/pimalaya#012d31179908063affe3eec77365e5ac86c436fe" dependencies = [ "log", "oauth2", @@ -3856,6 +3859,8 @@ dependencies = [ [[package]] name = "shellexpand-utils" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b44114d15a72740d3049e739929d0f4af335ba2eb855ea0f1fa2bfb4d7066215" dependencies = [ "log", "shellexpand", @@ -3865,8 +3870,7 @@ dependencies = [ [[package]] name = "shellexpand-utils" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b44114d15a72740d3049e739929d0f4af335ba2eb855ea0f1fa2bfb4d7066215" +source = "git+https://git.sr.ht/~soywod/pimalaya#012d31179908063affe3eec77365e5ac86c436fe" dependencies = [ "log", "shellexpand", diff --git a/Cargo.toml b/Cargo.toml index e8f25e9..2686546 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,21 +93,21 @@ version = "1.16.0" [dependencies.email-lib] # version = "=0.15.3" default-features = false -path = "/home/soywod/sourcehut/pimalaya/email" +git = "https://git.sr.ht/~soywod/pimalaya" [dependencies.keyring-lib] version = "=0.1.0" [dependencies.oauth-lib] # version = "=0.1.0" -path = "/home/soywod/sourcehut/pimalaya/oauth" +git = "https://git.sr.ht/~soywod/pimalaya" [dependencies.process-lib] version = "=0.1.0" [dependencies.mml-lib] # version = "=1.0.1" -path = "/home/soywod/sourcehut/pimalaya/mml" +git = "https://git.sr.ht/~soywod/pimalaya" [dependencies.secret-lib] version = "=0.1.0" @@ -121,7 +121,7 @@ version = "1.0" [dependencies.shellexpand-utils] # version = "=0.1.0" -path = "/home/soywod/sourcehut/pimalaya/shellexpand-utils" +git = "https://git.sr.ht/~soywod/pimalaya" [dependencies.termcolor] version = "1.1" diff --git a/src/cache/id_mapper.rs b/src/cache/id_mapper.rs index a022640..5193e46 100644 --- a/src/cache/id_mapper.rs +++ b/src/cache/id_mapper.rs @@ -36,6 +36,7 @@ impl IdMapper { db_path } + // FIXME pub fn new(backend: &Backend, account_config: &AccountConfig, folder: &str) -> Result { Ok(IdMapper::Dummy) From a0888067dafcc2f2ae5deccd5ecf555bcab547fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 28 Nov 2023 12:30:50 +0100 Subject: [PATCH 06/29] fix sync cache --- Cargo.lock | 48 +++---- Cargo.toml | 8 +- config.sample.toml | 10 +- src/backend.rs | 201 +++++++++++++------------- src/config/config.rs | 102 +++++++++++++- src/main.rs | 325 ++++++++++++++++++++++++++----------------- 6 files changed, 428 insertions(+), 266 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d220ddf..b3bfaa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -984,7 +984,6 @@ dependencies = [ [[package]] name = "email-lib" version = "0.15.3" -source = "git+https://git.sr.ht/~soywod/pimalaya#012d31179908063affe3eec77365e5ac86c436fe" dependencies = [ "advisory-lock", "anyhow", @@ -2070,15 +2069,15 @@ dependencies = [ "keyring-lib", "log", "md5", - "mml-lib 1.0.1 (git+https://git.sr.ht/~soywod/pimalaya)", - "oauth-lib 0.1.0 (git+https://git.sr.ht/~soywod/pimalaya)", + "mml-lib 1.0.1", + "oauth-lib 0.1.0", "once_cell", "process-lib", "rusqlite", "secret-lib", "serde", "serde_json", - "shellexpand-utils 0.1.0 (git+https://git.sr.ht/~soywod/pimalaya)", + "shellexpand-utils 0.1.0", "tempfile", "termcolor", "terminal_size", @@ -2685,6 +2684,21 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mml-lib" +version = "1.0.1" +dependencies = [ + "async-recursion", + "chumsky", + "log", + "mail-builder", + "mail-parser", + "nanohtml2text", + "shellexpand-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror", + "tree_magic_mini", +] + [[package]] name = "mml-lib" version = "1.0.1" @@ -2707,22 +2721,6 @@ dependencies = [ "tree_magic_mini", ] -[[package]] -name = "mml-lib" -version = "1.0.1" -source = "git+https://git.sr.ht/~soywod/pimalaya#012d31179908063affe3eec77365e5ac86c436fe" -dependencies = [ - "async-recursion", - "chumsky", - "log", - "mail-builder", - "mail-parser", - "nanohtml2text", - "shellexpand-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror", - "tree_magic_mini", -] - [[package]] name = "nanohtml2text" version = "0.1.4" @@ -2848,8 +2846,6 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "oauth-lib" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1484d9864dbf6b55b3785380631a253fa0ff7f8e1bbb078bfd7effd11283d61" dependencies = [ "log", "oauth2", @@ -2862,7 +2858,8 @@ dependencies = [ [[package]] name = "oauth-lib" version = "0.1.0" -source = "git+https://git.sr.ht/~soywod/pimalaya#012d31179908063affe3eec77365e5ac86c436fe" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1484d9864dbf6b55b3785380631a253fa0ff7f8e1bbb078bfd7effd11283d61" dependencies = [ "log", "oauth2", @@ -3859,8 +3856,6 @@ dependencies = [ [[package]] name = "shellexpand-utils" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b44114d15a72740d3049e739929d0f4af335ba2eb855ea0f1fa2bfb4d7066215" dependencies = [ "log", "shellexpand", @@ -3870,7 +3865,8 @@ dependencies = [ [[package]] name = "shellexpand-utils" version = "0.1.0" -source = "git+https://git.sr.ht/~soywod/pimalaya#012d31179908063affe3eec77365e5ac86c436fe" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b44114d15a72740d3049e739929d0f4af335ba2eb855ea0f1fa2bfb4d7066215" dependencies = [ "log", "shellexpand", diff --git a/Cargo.toml b/Cargo.toml index 2686546..e8f25e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,21 +93,21 @@ version = "1.16.0" [dependencies.email-lib] # version = "=0.15.3" default-features = false -git = "https://git.sr.ht/~soywod/pimalaya" +path = "/home/soywod/sourcehut/pimalaya/email" [dependencies.keyring-lib] version = "=0.1.0" [dependencies.oauth-lib] # version = "=0.1.0" -git = "https://git.sr.ht/~soywod/pimalaya" +path = "/home/soywod/sourcehut/pimalaya/oauth" [dependencies.process-lib] version = "=0.1.0" [dependencies.mml-lib] # version = "=1.0.1" -git = "https://git.sr.ht/~soywod/pimalaya" +path = "/home/soywod/sourcehut/pimalaya/mml" [dependencies.secret-lib] version = "=0.1.0" @@ -121,7 +121,7 @@ version = "1.0" [dependencies.shellexpand-utils] # version = "=0.1.0" -git = "https://git.sr.ht/~soywod/pimalaya" +path = "/home/soywod/sourcehut/pimalaya/shellexpand-utils" [dependencies.termcolor] version = "1.1" diff --git a/config.sample.toml b/config.sample.toml index 956e5e5..b59b07e 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -6,13 +6,13 @@ default = true display-name = "My example account" email = "example@localhost" +sync = true +sync-dir = "./.sync" + # The default backend used for all the features like adding folders, # listing envelopes or copying messages. backend = "imap" -# Override the backend used for sending messages. -message.send.backend = "smtp" - # IMAP config imap.host = "localhost" imap.port = 3143 @@ -23,6 +23,9 @@ imap.insecure = true imap.auth = "passwd" imap.passwd.raw = "example" +# Override the backend used for sending messages. +message.send.backend = "smtp" + # SMTP config smtp.host = "localhost" smtp.port = 3025 @@ -32,4 +35,3 @@ smtp.starttls = false smtp.insecure = true smtp.auth = "passwd" smtp.passwd.raw = "example" - diff --git a/src/backend.rs b/src/backend.rs index c30805a..aed145c 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -39,12 +39,12 @@ use email::{ list::{imap::ListFoldersImap, maildir::ListFoldersMaildir}, purge::imap::PurgeFolderImap, }, - maildir::{MaildirSessionBuilder, MaildirSessionSync}, + maildir::{MaildirConfig, MaildirSessionBuilder, MaildirSessionSync}, sendmail::SendmailContext, }; use serde::{Deserialize, Serialize}; -use crate::config::DeserializedConfig; +use crate::{account::DeserializedAccountConfig, config::DeserializedConfig}; #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] @@ -61,9 +61,10 @@ pub enum BackendKind { #[derive(Clone, Default)] pub struct BackendContextBuilder { + sync_cache: Option, + maildir: Option, #[cfg(feature = "imap-backend")] imap: Option, - maildir: Option, #[cfg(feature = "smtp-sender")] smtp: Option, sendmail: Option, @@ -80,6 +81,10 @@ impl email::backend::BackendContextBuilder for BackendContextBuilder { ctx.maildir = Some(maildir.build().await?); } + if let Some(maildir) = self.sync_cache { + ctx.sync_cache = Some(maildir.build().await?); + } + #[cfg(feature = "imap-backend")] if let Some(imap) = self.imap { ctx.imap = Some(imap.build().await?); @@ -105,9 +110,10 @@ impl email::backend::BackendContextBuilder for BackendContextBuilder { #[derive(Default)] pub struct BackendContext { + pub sync_cache: Option, + pub maildir: Option, #[cfg(feature = "imap-backend")] pub imap: Option, - pub maildir: Option, #[cfg(feature = "smtp-sender")] pub smtp: Option, pub sendmail: Option, @@ -115,97 +121,14 @@ pub struct BackendContext { pub struct BackendBuilder(pub email::backend::BackendBuilder); +pub type Backend = email::backend::Backend; + impl BackendBuilder { - pub async fn new(config: DeserializedConfig, account_name: Option<&str>) -> Result { - let (account_name, mut deserialized_account_config) = match account_name { - Some("default") | Some("") | None => config - .accounts - .iter() - .find_map(|(name, account)| { - account - .default - .filter(|default| *default == true) - .map(|_| (name.to_owned(), account.clone())) - }) - .ok_or_else(|| anyhow!("cannot find default account")), - Some(name) => config - .accounts - .get(name) - .map(|account| (name.to_owned(), account.clone())) - .ok_or_else(|| anyhow!("cannot find account {name}")), - }?; - - #[cfg(feature = "imap-backend")] - if let Some(imap_config) = deserialized_account_config.imap.as_mut() { - imap_config - .auth - .replace_undefined_keyring_entries(&account_name); - } - - #[cfg(feature = "smtp-sender")] - if let Some(smtp_config) = deserialized_account_config.smtp.as_mut() { - smtp_config - .auth - .replace_undefined_keyring_entries(&account_name); - } - - let config = Config { - display_name: config.display_name, - signature_delim: config.signature_delim, - signature: config.signature, - downloads_dir: config.downloads_dir, - - folder_listing_page_size: config.folder_listing_page_size, - folder_aliases: config.folder_aliases, - - email_listing_page_size: config.email_listing_page_size, - email_listing_datetime_fmt: config.email_listing_datetime_fmt, - email_listing_datetime_local_tz: config.email_listing_datetime_local_tz, - email_reading_headers: config.email_reading_headers, - email_reading_format: config.email_reading_format, - email_writing_headers: config.email_writing_headers, - email_sending_save_copy: config.email_sending_save_copy, - email_hooks: config.email_hooks, - - accounts: HashMap::from_iter(config.accounts.clone().into_iter().map( - |(name, config)| { - ( - name.clone(), - AccountConfig { - name, - email: config.email, - display_name: config.display_name, - signature_delim: config.signature_delim, - signature: config.signature, - downloads_dir: config.downloads_dir, - - folder_listing_page_size: config.folder_listing_page_size, - folder_aliases: config.folder_aliases.unwrap_or_default(), - - email_listing_page_size: config.email_listing_page_size, - email_listing_datetime_fmt: config.email_listing_datetime_fmt, - email_listing_datetime_local_tz: config.email_listing_datetime_local_tz, - - email_reading_headers: config.email_reading_headers, - email_reading_format: config.email_reading_format.unwrap_or_default(), - email_writing_headers: config.email_writing_headers, - email_sending_save_copy: config.email_sending_save_copy, - email_hooks: config.email_hooks.unwrap_or_default(), - - sync: config.sync, - sync_dir: config.sync_dir, - sync_folders_strategy: config.sync_folders_strategy.unwrap_or_default(), - - #[cfg(feature = "pgp")] - pgp: config.pgp, - }, - ) - }, - )), - }; - - let account_config = config.account(&account_name)?; - + pub async fn new( + deserialized_account_config: DeserializedAccountConfig, + account_config: AccountConfig, + disable_cache: bool, + ) -> Result { let backend_ctx_builder = BackendContextBuilder { maildir: deserialized_account_config .maildir @@ -213,6 +136,16 @@ impl BackendBuilder { .map(|mdir_config| { MaildirSessionBuilder::new(account_config.clone(), mdir_config.clone()) }), + sync_cache: if account_config.sync && !disable_cache { + Some(MaildirSessionBuilder::new( + account_config.clone(), + MaildirConfig { + root_dir: account_config.sync_dir()?, + }, + )) + } else { + None + }, #[cfg(feature = "imap-backend")] imap: deserialized_account_config .imap @@ -253,6 +186,10 @@ impl BackendBuilder { .or_else(|| deserialized_account_config.backend.as_ref()); match add_folder { + Some(_) if account_config.sync && !disable_cache => { + backend_builder = backend_builder + .with_add_folder(|ctx| ctx.sync_cache.as_ref().and_then(AddFolderMaildir::new)); + } Some(BackendKind::Maildir) => { backend_builder = backend_builder .with_add_folder(|ctx| ctx.maildir.as_ref().and_then(AddFolderMaildir::new)); @@ -278,6 +215,11 @@ impl BackendBuilder { .or_else(|| deserialized_account_config.backend.as_ref()); match list_folders { + Some(_) if account_config.sync && !disable_cache => { + backend_builder = backend_builder.with_list_folders(|ctx| { + ctx.sync_cache.as_ref().and_then(ListFoldersMaildir::new) + }); + } Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_list_folders(|ctx| { ctx.maildir.as_ref().and_then(ListFoldersMaildir::new) @@ -305,6 +247,11 @@ impl BackendBuilder { .or_else(|| deserialized_account_config.backend.as_ref()); match expunge_folder { + Some(_) if account_config.sync && !disable_cache => { + backend_builder = backend_builder.with_expunge_folder(|ctx| { + ctx.sync_cache.as_ref().and_then(ExpungeFolderMaildir::new) + }); + } Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_expunge_folder(|ctx| { ctx.maildir.as_ref().and_then(ExpungeFolderMaildir::new) @@ -332,6 +279,12 @@ impl BackendBuilder { .or_else(|| deserialized_account_config.backend.as_ref()); match purge_folder { + // TODO + // Some(_) if account_config.sync && !disable_cache => { + // backend_builder = backend_builder.with_purge_folder(|ctx| { + // ctx.sync_cache.as_ref().and_then(PurgeFolderMaildir::new) + // }); + // } // TODO // Some(BackendKind::Maildir) => { // backend_builder = backend_builder @@ -359,6 +312,11 @@ impl BackendBuilder { .or_else(|| deserialized_account_config.backend.as_ref()); match delete_folder { + Some(_) if account_config.sync && !disable_cache => { + backend_builder = backend_builder.with_delete_folder(|ctx| { + ctx.sync_cache.as_ref().and_then(DeleteFolderMaildir::new) + }); + } Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_delete_folder(|ctx| { ctx.maildir.as_ref().and_then(DeleteFolderMaildir::new) @@ -386,6 +344,11 @@ impl BackendBuilder { .or_else(|| deserialized_account_config.backend.as_ref()); match get_envelope { + Some(_) if account_config.sync && !disable_cache => { + backend_builder = backend_builder.with_get_envelope(|ctx| { + ctx.sync_cache.as_ref().and_then(GetEnvelopeMaildir::new) + }); + } Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_get_envelope(|ctx| { ctx.maildir.as_ref().and_then(GetEnvelopeMaildir::new) @@ -413,6 +376,11 @@ impl BackendBuilder { .or_else(|| deserialized_account_config.backend.as_ref()); match list_envelopes { + Some(_) if account_config.sync && !disable_cache => { + backend_builder = backend_builder.with_list_envelopes(|ctx| { + ctx.sync_cache.as_ref().and_then(ListEnvelopesMaildir::new) + }); + } Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_list_envelopes(|ctx| { ctx.maildir.as_ref().and_then(ListEnvelopesMaildir::new) @@ -440,6 +408,10 @@ impl BackendBuilder { .or_else(|| deserialized_account_config.backend.as_ref()); match add_flags { + Some(_) if account_config.sync && !disable_cache => { + backend_builder = backend_builder + .with_add_flags(|ctx| ctx.sync_cache.as_ref().and_then(AddFlagsMaildir::new)); + } Some(BackendKind::Maildir) => { backend_builder = backend_builder .with_add_flags(|ctx| ctx.maildir.as_ref().and_then(AddFlagsMaildir::new)); @@ -465,6 +437,10 @@ impl BackendBuilder { .or_else(|| deserialized_account_config.backend.as_ref()); match set_flags { + Some(_) if account_config.sync && !disable_cache => { + backend_builder = backend_builder + .with_set_flags(|ctx| ctx.sync_cache.as_ref().and_then(SetFlagsMaildir::new)); + } Some(BackendKind::Maildir) => { backend_builder = backend_builder .with_set_flags(|ctx| ctx.maildir.as_ref().and_then(SetFlagsMaildir::new)); @@ -490,6 +466,11 @@ impl BackendBuilder { .or_else(|| deserialized_account_config.backend.as_ref()); match remove_flags { + Some(_) if account_config.sync && !disable_cache => { + backend_builder = backend_builder.with_remove_flags(|ctx| { + ctx.sync_cache.as_ref().and_then(RemoveFlagsMaildir::new) + }); + } Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_remove_flags(|ctx| { ctx.maildir.as_ref().and_then(RemoveFlagsMaildir::new) @@ -539,6 +520,13 @@ impl BackendBuilder { .or_else(|| deserialized_account_config.backend.as_ref()); match add_msg { + Some(_) if account_config.sync && !disable_cache => { + backend_builder = backend_builder.with_add_raw_message_with_flags(|ctx| { + ctx.sync_cache + .as_ref() + .and_then(AddRawMessageWithFlagsMaildir::new) + }); + } Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_add_raw_message_with_flags(|ctx| { ctx.maildir @@ -571,6 +559,11 @@ impl BackendBuilder { .or_else(|| deserialized_account_config.backend.as_ref()); match peek_msgs { + Some(_) if account_config.sync && !disable_cache => { + backend_builder = backend_builder.with_peek_messages(|ctx| { + ctx.sync_cache.as_ref().and_then(PeekMessagesMaildir::new) + }); + } Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_peek_messages(|ctx| { ctx.maildir.as_ref().and_then(PeekMessagesMaildir::new) @@ -620,6 +613,11 @@ impl BackendBuilder { .or_else(|| deserialized_account_config.backend.as_ref()); match copy_msgs { + Some(_) if account_config.sync && !disable_cache => { + backend_builder = backend_builder.with_copy_messages(|ctx| { + ctx.sync_cache.as_ref().and_then(CopyMessagesMaildir::new) + }); + } Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_copy_messages(|ctx| { ctx.maildir.as_ref().and_then(CopyMessagesMaildir::new) @@ -647,6 +645,11 @@ impl BackendBuilder { .or_else(|| deserialized_account_config.backend.as_ref()); match move_msgs { + Some(_) if account_config.sync && !disable_cache => { + backend_builder = backend_builder.with_move_messages(|ctx| { + ctx.sync_cache.as_ref().and_then(MoveMessagesMaildir::new) + }); + } Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_move_messages(|ctx| { ctx.maildir.as_ref().and_then(MoveMessagesMaildir::new) @@ -668,14 +671,8 @@ impl BackendBuilder { Ok(Self(backend_builder)) } -} -impl Deref for BackendBuilder { - type Target = email::backend::BackendBuilder; - - fn deref(&self) -> &Self::Target { - &self.0 + pub async fn build(self) -> Result { + self.0.build().await } } - -pub type Backend = email::backend::Backend; diff --git a/src/config/config.rs b/src/config/config.rs index 4ece9b8..a62aaa0 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -6,7 +6,11 @@ use anyhow::{anyhow, Context, Result}; use dialoguer::Confirm; use dirs::{config_dir, home_dir}; -use email::email::{EmailHooks, EmailTextPlainFormat}; +use email::{ + account::AccountConfig, + config::Config, + email::{EmailHooks, EmailTextPlainFormat}, +}; use log::{debug, trace}; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs, path::PathBuf, process::exit}; @@ -98,6 +102,102 @@ impl DeserializedConfig { .or_else(|| home_dir().map(|p| p.join(".himalayarc"))) .filter(|p| p.exists()) } + + pub fn into_account_configs( + self, + account_name: Option<&str>, + ) -> Result<(DeserializedAccountConfig, AccountConfig)> { + let (account_name, mut toml_account_config) = match account_name { + Some("default") | Some("") | None => self + .accounts + .iter() + .find_map(|(name, account)| { + account + .default + .filter(|default| *default == true) + .map(|_| (name.to_owned(), account.clone())) + }) + .ok_or_else(|| anyhow!("cannot find default account")), + Some(name) => self + .accounts + .get(name) + .map(|account| (name.to_owned(), account.clone())) + .ok_or_else(|| anyhow!("cannot find account {name}")), + }?; + + #[cfg(feature = "imap-backend")] + if let Some(imap_config) = toml_account_config.imap.as_mut() { + imap_config + .auth + .replace_undefined_keyring_entries(&account_name); + } + + #[cfg(feature = "smtp-sender")] + if let Some(smtp_config) = toml_account_config.smtp.as_mut() { + smtp_config + .auth + .replace_undefined_keyring_entries(&account_name); + } + + let config = Config { + display_name: self.display_name, + signature_delim: self.signature_delim, + signature: self.signature, + downloads_dir: self.downloads_dir, + + folder_listing_page_size: self.folder_listing_page_size, + folder_aliases: self.folder_aliases, + + email_listing_page_size: self.email_listing_page_size, + email_listing_datetime_fmt: self.email_listing_datetime_fmt, + email_listing_datetime_local_tz: self.email_listing_datetime_local_tz, + email_reading_headers: self.email_reading_headers, + email_reading_format: self.email_reading_format, + email_writing_headers: self.email_writing_headers, + email_sending_save_copy: self.email_sending_save_copy, + email_hooks: self.email_hooks, + + accounts: HashMap::from_iter(self.accounts.clone().into_iter().map( + |(name, config)| { + ( + name.clone(), + AccountConfig { + name, + email: config.email, + display_name: config.display_name, + signature_delim: config.signature_delim, + signature: config.signature, + downloads_dir: config.downloads_dir, + + folder_listing_page_size: config.folder_listing_page_size, + folder_aliases: config.folder_aliases.unwrap_or_default(), + + email_listing_page_size: config.email_listing_page_size, + email_listing_datetime_fmt: config.email_listing_datetime_fmt, + email_listing_datetime_local_tz: config.email_listing_datetime_local_tz, + + email_reading_headers: config.email_reading_headers, + email_reading_format: config.email_reading_format.unwrap_or_default(), + email_writing_headers: config.email_writing_headers, + email_sending_save_copy: config.email_sending_save_copy, + email_hooks: config.email_hooks.unwrap_or_default(), + + sync: config.sync.unwrap_or_default(), + sync_dir: config.sync_dir, + sync_folders_strategy: config.sync_folders_strategy.unwrap_or_default(), + + #[cfg(feature = "pgp")] + pgp: config.pgp, + }, + ) + }, + )), + }; + + let account_config = config.account(&account_name)?; + + Ok((toml_account_config, account_config)) + } } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index 5256932..3eed799 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,21 +56,16 @@ async fn main() -> Result<()> { // checks mailto command before app initialization let raw_args: Vec = env::args().collect(); if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { - // let url = Url::parse(&raw_args[1])?; - // let config = DeserializedConfig::from_opt_path(None).await?; - // let account_config = config.to_account_config(None)?; - // let backend = BackendBuilder::new(account_config.clone()).build().await?; - // let mut printer = StdoutPrinter::default(); + let url = Url::parse(&raw_args[1])?; + let (toml_account_config, account_config) = DeserializedConfig::from_opt_path(None) + .await? + .into_account_configs(None)?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), false).await?; + let backend = backend_builder.build().await?; + let mut printer = StdoutPrinter::default(); - // email::handlers::mailto( - // &account_config, - // &backend, - // &mut printer, - // &url, - // ) - // .await?; - - return Ok(()); + return email::handlers::mailto(&account_config, &backend, &mut printer, &url).await; } let app = create_app(); @@ -93,15 +88,18 @@ async fn main() -> Result<()> { _ => (), } - let config = DeserializedConfig::from_opt_path(config::args::parse_arg(&m)).await?; let maybe_account_name = account::args::parse_arg(&m); + let toml_config = DeserializedConfig::from_opt_path(config::args::parse_arg(&m)).await?; + let (toml_account_config, account_config) = toml_config + .clone() + .into_account_configs(maybe_account_name)?; + let folder = folder::args::parse_source_arg(&m); let disable_cache = cache::args::parse_disable_cache_flag(&m); - let backend_builder = BackendBuilder::new(config.clone(), maybe_account_name).await?; - let account_config = &backend_builder.account_config; let mut printer = StdoutPrinter::try_from(&m)?; + // FIXME // #[cfg(feature = "imap-backend")] // if let BackendConfig::Imap(imap_config) = &account_config.backend { // let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); @@ -124,10 +122,12 @@ async fn main() -> Result<()> { match account::args::matches(&m)? { Some(account::args::Cmd::List(max_width)) => { - account::handlers::list(max_width, &account_config, &config, &mut printer)?; + account::handlers::list(max_width, &account_config, &toml_config, &mut printer)?; return Ok(()); } Some(account::args::Cmd::Sync(strategy, dry_run)) => { + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), true).await?; let sync_builder = AccountSyncBuilder::new(backend_builder.0) .await? .with_some_folders_strategy(strategy) @@ -148,26 +148,39 @@ async fn main() -> Result<()> { let folder = folder .ok_or_else(|| anyhow!("the folder argument is missing")) .context("cannot create folder")?; - let backend = backend_builder.clone().build().await?; - folder::handlers::create(&mut printer, &backend, &folder).await?; - return Ok(()); + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; + + return folder::handlers::create(&mut printer, &backend, &folder).await; } Some(folder::args::Cmd::List(max_width)) => { - let backend = backend_builder.clone().build().await?; - folder::handlers::list(&account_config, &mut printer, &backend, max_width).await?; - return Ok(()); + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; + + return folder::handlers::list(&account_config, &mut printer, &backend, max_width) + .await; } Some(folder::args::Cmd::Expunge) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; - folder::handlers::expunge(&mut printer, &backend, &folder).await?; - return Ok(()); + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; + + return folder::handlers::expunge(&mut printer, &backend, &folder).await; } Some(folder::args::Cmd::Delete) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; - folder::handlers::delete(&mut printer, &backend, &folder).await?; - return Ok(()); + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; + + return folder::handlers::delete(&mut printer, &backend, &folder).await; } _ => (), } @@ -176,9 +189,13 @@ async fn main() -> Result<()> { match email::args::matches(&m)? { Some(email::args::Cmd::Attachments(ids)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::attachments( + + return email::handlers::attachments( &account_config, &mut printer, &id_mapper, @@ -186,34 +203,45 @@ async fn main() -> Result<()> { &folder, ids, ) - .await?; - return Ok(()); + .await; } Some(email::args::Cmd::Copy(ids, to_folder)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::copy(&mut printer, &id_mapper, &backend, &folder, to_folder, ids) - .await?; - - return Ok(()); + return email::handlers::copy( + &mut printer, + &id_mapper, + &backend, + &folder, + to_folder, + ids, + ) + .await; } Some(email::args::Cmd::Delete(ids)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::delete(&mut printer, &id_mapper, &backend, &folder, ids).await?; - - return Ok(()); + return email::handlers::delete(&mut printer, &id_mapper, &backend, &folder, ids).await; } Some(email::args::Cmd::Forward(id, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::forward( + return email::handlers::forward( &account_config, &mut printer, &id_mapper, @@ -223,16 +251,17 @@ async fn main() -> Result<()> { headers, body, ) - .await?; - - return Ok(()); + .await; } Some(email::args::Cmd::List(max_width, page_size, page)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::list( + return email::handlers::list( &account_config, &mut printer, &id_mapper, @@ -242,26 +271,35 @@ async fn main() -> Result<()> { page_size, page, ) - .await?; - - return Ok(()); + .await; } Some(email::args::Cmd::Move(ids, to_folder)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::move_(&mut printer, &id_mapper, &backend, &folder, to_folder, ids) - .await?; - - return Ok(()); + return email::handlers::move_( + &mut printer, + &id_mapper, + &backend, + &folder, + to_folder, + ids, + ) + .await; } Some(email::args::Cmd::Read(ids, text_mime, raw, headers)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::read( + return email::handlers::read( &account_config, &mut printer, &id_mapper, @@ -272,16 +310,17 @@ async fn main() -> Result<()> { raw, headers, ) - .await?; - - return Ok(()); + .await; } Some(email::args::Cmd::Reply(id, all, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::reply( + return email::handlers::reply( &account_config, &mut printer, &id_mapper, @@ -292,25 +331,28 @@ async fn main() -> Result<()> { headers, body, ) - .await?; - - return Ok(()); + .await; } Some(email::args::Cmd::Save(raw_email)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::save(&mut printer, &id_mapper, &backend, &folder, raw_email).await?; - - return Ok(()); + return email::handlers::save(&mut printer, &id_mapper, &backend, &folder, raw_email) + .await; } Some(email::args::Cmd::Search(query, max_width, page_size, page)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::search( + return email::handlers::search( &account_config, &mut printer, &id_mapper, @@ -321,16 +363,17 @@ async fn main() -> Result<()> { page_size, page, ) - .await?; - - return Ok(()); + .await; } Some(email::args::Cmd::Sort(criteria, query, max_width, page_size, page)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - email::handlers::sort( + return email::handlers::sort( &account_config, &mut printer, &id_mapper, @@ -342,56 +385,79 @@ async fn main() -> Result<()> { page_size, page, ) - .await?; - - return Ok(()); + .await; } Some(email::args::Cmd::Send(raw_email)) => { - let backend = backend_builder.clone().build().await?; - email::handlers::send(&account_config, &mut printer, &backend, raw_email).await?; - - return Ok(()); + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; + return email::handlers::send(&account_config, &mut printer, &backend, raw_email).await; } Some(email::args::Cmd::Flag(m)) => match m { Some(flag::args::Cmd::Set(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - - flag::handlers::set(&mut printer, &id_mapper, &backend, &folder, ids, flags) - .await?; - - return Ok(()); + return flag::handlers::set( + &mut printer, + &id_mapper, + &backend, + &folder, + ids, + flags, + ) + .await; } Some(flag::args::Cmd::Add(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - - flag::handlers::add(&mut printer, &id_mapper, &backend, &folder, ids, flags) - .await?; - - return Ok(()); + return flag::handlers::add( + &mut printer, + &id_mapper, + &backend, + &folder, + ids, + flags, + ) + .await; } Some(flag::args::Cmd::Remove(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - flag::handlers::remove(&mut printer, &id_mapper, &backend, &folder, ids, flags) - .await?; - - return Ok(()); + return flag::handlers::remove( + &mut printer, + &id_mapper, + &backend, + &folder, + ids, + flags, + ) + .await; } _ => (), }, Some(email::args::Cmd::Tpl(m)) => match m { Some(tpl::args::Cmd::Forward(id, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - - tpl::handlers::forward( + return tpl::handlers::forward( &account_config, &mut printer, &id_mapper, @@ -401,9 +467,7 @@ async fn main() -> Result<()> { headers, body, ) - .await?; - - return Ok(()); + .await; } Some(tpl::args::Cmd::Write(headers, body)) => { tpl::handlers::write(&account_config, &mut printer, headers, body).await?; @@ -411,10 +475,12 @@ async fn main() -> Result<()> { } Some(tpl::args::Cmd::Reply(id, all, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - - tpl::handlers::reply( + return tpl::handlers::reply( &account_config, &mut printer, &id_mapper, @@ -425,16 +491,16 @@ async fn main() -> Result<()> { headers, body, ) - .await?; - - return Ok(()); + .await; } Some(tpl::args::Cmd::Save(tpl)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = backend_builder.clone().build().await?; + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - - tpl::handlers::save( + return tpl::handlers::save( &account_config, &mut printer, &id_mapper, @@ -442,23 +508,24 @@ async fn main() -> Result<()> { &folder, tpl, ) - .await?; - - return Ok(()); + .await; } Some(tpl::args::Cmd::Send(tpl)) => { - let backend = backend_builder.clone().build().await?; - tpl::handlers::send(&account_config, &mut printer, &backend, tpl).await?; - - return Ok(()); + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; + return tpl::handlers::send(&account_config, &mut printer, &backend, tpl).await; } _ => (), }, Some(email::args::Cmd::Write(headers, body)) => { - let backend = backend_builder.clone().build().await?; - email::handlers::write(&account_config, &mut printer, &backend, headers, body).await?; - - return Ok(()); + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) + .await?; + let backend = backend_builder.build().await?; + return email::handlers::write(&account_config, &mut printer, &backend, headers, body) + .await; } _ => (), } From fb8f356e8c14a6c419abcb22ace7c0070c082016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 28 Nov 2023 22:28:28 +0100 Subject: [PATCH 07/29] fix id mapper --- src/backend.rs | 546 +++++++++++++++++++---------------- src/cache/id_mapper.rs | 58 ++-- src/config/config.rs | 8 + src/domain/account/config.rs | 138 +++++++++ src/domain/email/handlers.rs | 36 +-- src/domain/flag/handlers.rs | 16 +- src/domain/tpl/handlers.rs | 18 +- src/main.rs | 248 +++------------- 8 files changed, 518 insertions(+), 550 deletions(-) diff --git a/src/backend.rs b/src/backend.rs index aed145c..eef8843 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -1,7 +1,6 @@ -use std::{collections::HashMap, ops::Deref}; - -use anyhow::{anyhow, Result}; +use anyhow::Result; use async_trait::async_trait; +use std::ops::Deref; #[cfg(feature = "imap-backend")] use email::imap::{ImapSessionBuilder, ImapSessionSync}; @@ -9,7 +8,6 @@ use email::imap::{ImapSessionBuilder, ImapSessionSync}; use email::smtp::{SmtpClientBuilder, SmtpClientSync}; use email::{ account::AccountConfig, - config::Config, email::{ envelope::{ get::{imap::GetEnvelopeImap, maildir::GetEnvelopeMaildir}, @@ -32,6 +30,8 @@ use email::{ send_raw::{sendmail::SendRawMessageSendmail, smtp::SendRawMessageSmtp}, }, }, + envelope::{Id, SingleId}, + flag::Flags, folder::{ add::{imap::AddFolderImap, maildir::AddFolderMaildir}, delete::{imap::DeleteFolderImap, maildir::DeleteFolderMaildir}, @@ -40,16 +40,19 @@ use email::{ purge::imap::PurgeFolderImap, }, maildir::{MaildirConfig, MaildirSessionBuilder, MaildirSessionSync}, + message::Messages, sendmail::SendmailContext, }; use serde::{Deserialize, Serialize}; -use crate::{account::DeserializedAccountConfig, config::DeserializedConfig}; +use crate::{account::DeserializedAccountConfig, Envelopes, IdMapper}; #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum BackendKind { Maildir, + #[serde(skip_deserializing)] + MaildirForSync, #[cfg(feature = "imap-backend")] Imap, #[cfg(feature = "notmuch-backend")] @@ -61,8 +64,8 @@ pub enum BackendKind { #[derive(Clone, Default)] pub struct BackendContextBuilder { - sync_cache: Option, maildir: Option, + maildir_for_sync: Option, #[cfg(feature = "imap-backend")] imap: Option, #[cfg(feature = "smtp-sender")] @@ -81,8 +84,8 @@ impl email::backend::BackendContextBuilder for BackendContextBuilder { ctx.maildir = Some(maildir.build().await?); } - if let Some(maildir) = self.sync_cache { - ctx.sync_cache = Some(maildir.build().await?); + if let Some(maildir) = self.maildir_for_sync { + ctx.maildir_for_sync = Some(maildir.build().await?); } #[cfg(feature = "imap-backend")] @@ -110,8 +113,8 @@ impl email::backend::BackendContextBuilder for BackendContextBuilder { #[derive(Default)] pub struct BackendContext { - pub sync_cache: Option, pub maildir: Option, + pub maildir_for_sync: Option, #[cfg(feature = "imap-backend")] pub imap: Option, #[cfg(feature = "smtp-sender")] @@ -119,55 +122,178 @@ pub struct BackendContext { pub sendmail: Option, } -pub struct BackendBuilder(pub email::backend::BackendBuilder); +pub struct Backend { + toml_account_config: DeserializedAccountConfig, + backend: email::backend::Backend, +} -pub type Backend = email::backend::Backend; +impl Backend { + fn build_id_mapper( + &self, + folder: &str, + backend_kind: Option<&BackendKind>, + ) -> Result { + let mut id_mapper = IdMapper::Dummy; + + match backend_kind { + Some(BackendKind::Maildir) => { + if let Some(mdir_config) = &self.toml_account_config.maildir { + id_mapper = IdMapper::new( + &self.backend.account_config, + folder, + mdir_config.root_dir.clone(), + )?; + } + } + Some(BackendKind::MaildirForSync) => { + id_mapper = IdMapper::new( + &self.backend.account_config, + folder, + self.backend.account_config.sync_dir()?, + )?; + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + if let Some(notmuch_config) = &self.toml_account_config.notmuch { + id_mapper = IdMapper::new( + &self.backend.account_config, + folder, + mdir_config.root_dir.clone(), + )?; + } + } + _ => (), + }; + + Ok(id_mapper) + } + + pub async fn list_envelopes( + &self, + folder: &str, + page_size: usize, + page: usize, + ) -> Result { + let backend_kind = self.toml_account_config.list_envelopes_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let envelopes = self.backend.list_envelopes(folder, page_size, page).await?; + let envelopes = Envelopes::from_backend(&self.account_config, &id_mapper, envelopes)?; + Ok(envelopes) + } + + pub async fn add_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { + let backend_kind = self.toml_account_config.add_flags_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.add_flags(folder, &ids, flags).await + } + + pub async fn set_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { + let backend_kind = self.toml_account_config.set_flags_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.set_flags(folder, &ids, flags).await + } + + pub async fn remove_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { + let backend_kind = self.toml_account_config.remove_flags_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.remove_flags(folder, &ids, flags).await + } + + pub async fn get_messages(&self, folder: &str, ids: &[&str]) -> Result { + let backend_kind = self.toml_account_config.get_messages_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.get_messages(folder, &ids).await + } + + pub async fn copy_messages( + &self, + from_folder: &str, + to_folder: &str, + ids: &[&str], + ) -> Result<()> { + let backend_kind = self.toml_account_config.move_messages_kind(); + let id_mapper = self.build_id_mapper(from_folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend + .copy_messages(from_folder, to_folder, &ids) + .await + } + + pub async fn move_messages( + &self, + from_folder: &str, + to_folder: &str, + ids: &[&str], + ) -> Result<()> { + let backend_kind = self.toml_account_config.move_messages_kind(); + let id_mapper = self.build_id_mapper(from_folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend + .move_messages(from_folder, to_folder, &ids) + .await + } + + pub async fn delete_messages(&self, folder: &str, ids: &[&str]) -> Result<()> { + let backend_kind = self.toml_account_config.delete_messages_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.delete_messages(folder, &ids).await + } + + pub async fn add_raw_message(&self, folder: &str, email: &[u8]) -> Result { + let backend_kind = self.toml_account_config.add_raw_message_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let id = self.backend.add_raw_message(folder, email).await?; + id_mapper.create_alias(&*id)?; + Ok(id) + } +} + +impl Deref for Backend { + type Target = email::backend::Backend; + + fn deref(&self) -> &Self::Target { + &self.backend + } +} + +pub struct BackendBuilder { + toml_account_config: DeserializedAccountConfig, + builder: email::backend::BackendBuilder, +} impl BackendBuilder { pub async fn new( - deserialized_account_config: DeserializedAccountConfig, + toml_account_config: DeserializedAccountConfig, account_config: AccountConfig, - disable_cache: bool, ) -> Result { let backend_ctx_builder = BackendContextBuilder { - maildir: deserialized_account_config - .maildir - .as_ref() - .map(|mdir_config| { - MaildirSessionBuilder::new(account_config.clone(), mdir_config.clone()) - }), - sync_cache: if account_config.sync && !disable_cache { - Some(MaildirSessionBuilder::new( - account_config.clone(), - MaildirConfig { - root_dir: account_config.sync_dir()?, - }, - )) - } else { - None - }, + maildir: toml_account_config.maildir.as_ref().map(|mdir_config| { + MaildirSessionBuilder::new(account_config.clone(), mdir_config.clone()) + }), + maildir_for_sync: Some(MaildirSessionBuilder::new( + account_config.clone(), + MaildirConfig { + root_dir: account_config.sync_dir()?, + }, + )), #[cfg(feature = "imap-backend")] - imap: deserialized_account_config - .imap - .as_ref() - .map(|imap_config| { - ImapSessionBuilder::new(account_config.clone(), imap_config.clone()) - }), + imap: toml_account_config.imap.as_ref().map(|imap_config| { + ImapSessionBuilder::new(account_config.clone(), imap_config.clone()) + }), #[cfg(feature = "notmuch-backend")] - notmuch: deserialized_account_config - .notmuch - .as_ref() - .map(|notmuch_config| { - NotmuchSessionBuilder::new(account_config.clone(), notmuch_config.clone()) - }), + notmuch: toml_account_config.notmuch.as_ref().map(|notmuch_config| { + NotmuchSessionBuilder::new(account_config.clone(), notmuch_config.clone()) + }), #[cfg(feature = "smtp-sender")] - smtp: deserialized_account_config - .smtp - .as_ref() - .map(|smtp_config| { - SmtpClientBuilder::new(account_config.clone(), smtp_config.clone()) - }), - sendmail: deserialized_account_config + smtp: toml_account_config.smtp.as_ref().map(|smtp_config| { + SmtpClientBuilder::new(account_config.clone(), smtp_config.clone()) + }), + sendmail: toml_account_config .sendmail .as_ref() .map(|sendmail_config| { @@ -178,22 +304,18 @@ impl BackendBuilder { let mut backend_builder = email::backend::BackendBuilder::new(account_config.clone(), backend_ctx_builder); - let add_folder = deserialized_account_config - .folder - .as_ref() - .and_then(|folder| folder.add.as_ref()) - .and_then(|add| add.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match add_folder { - Some(_) if account_config.sync && !disable_cache => { - backend_builder = backend_builder - .with_add_folder(|ctx| ctx.sync_cache.as_ref().and_then(AddFolderMaildir::new)); - } + match toml_account_config.add_folder_kind() { Some(BackendKind::Maildir) => { backend_builder = backend_builder .with_add_folder(|ctx| ctx.maildir.as_ref().and_then(AddFolderMaildir::new)); } + Some(BackendKind::MaildirForSync) => { + backend_builder = backend_builder.with_add_folder(|ctx| { + ctx.maildir_for_sync + .as_ref() + .and_then(AddFolderMaildir::new) + }); + } #[cfg(feature = "imap-backend")] Some(BackendKind::Imap) => { backend_builder = backend_builder @@ -207,24 +329,19 @@ impl BackendBuilder { _ => (), } - let list_folders = deserialized_account_config - .folder - .as_ref() - .and_then(|folder| folder.list.as_ref()) - .and_then(|list| list.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match list_folders { - Some(_) if account_config.sync && !disable_cache => { - backend_builder = backend_builder.with_list_folders(|ctx| { - ctx.sync_cache.as_ref().and_then(ListFoldersMaildir::new) - }); - } + match toml_account_config.list_folders_kind() { Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_list_folders(|ctx| { ctx.maildir.as_ref().and_then(ListFoldersMaildir::new) }); } + Some(BackendKind::MaildirForSync) => { + backend_builder = backend_builder.with_list_folders(|ctx| { + ctx.maildir_for_sync + .as_ref() + .and_then(ListFoldersMaildir::new) + }); + } #[cfg(feature = "imap-backend")] Some(BackendKind::Imap) => { backend_builder = backend_builder @@ -239,24 +356,19 @@ impl BackendBuilder { _ => (), } - let expunge_folder = deserialized_account_config - .folder - .as_ref() - .and_then(|folder| folder.expunge.as_ref()) - .and_then(|expunge| expunge.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match expunge_folder { - Some(_) if account_config.sync && !disable_cache => { - backend_builder = backend_builder.with_expunge_folder(|ctx| { - ctx.sync_cache.as_ref().and_then(ExpungeFolderMaildir::new) - }); - } + match toml_account_config.expunge_folder_kind() { Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_expunge_folder(|ctx| { ctx.maildir.as_ref().and_then(ExpungeFolderMaildir::new) }); } + Some(BackendKind::MaildirForSync) => { + backend_builder = backend_builder.with_expunge_folder(|ctx| { + ctx.maildir_for_sync + .as_ref() + .and_then(ExpungeFolderMaildir::new) + }); + } #[cfg(feature = "imap-backend")] Some(BackendKind::Imap) => { backend_builder = backend_builder @@ -271,25 +383,17 @@ impl BackendBuilder { _ => (), } - let purge_folder = deserialized_account_config - .folder - .as_ref() - .and_then(|folder| folder.purge.as_ref()) - .and_then(|purge| purge.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match purge_folder { - // TODO - // Some(_) if account_config.sync && !disable_cache => { - // backend_builder = backend_builder.with_purge_folder(|ctx| { - // ctx.sync_cache.as_ref().and_then(PurgeFolderMaildir::new) - // }); - // } + match toml_account_config.purge_folder_kind() { // TODO // Some(BackendKind::Maildir) => { // backend_builder = backend_builder // .with_purge_folder(|ctx| ctx.maildir.as_ref().and_then(PurgeFolderMaildir::new)); // } + // TODO + // Some(BackendKind::MaildirForSync) => { + // backend_builder = backend_builder + // .with_purge_folder(|ctx| ctx.maildir_for_sync.as_ref().and_then(PurgeFolderMaildir::new)); + // } #[cfg(feature = "imap-backend")] Some(BackendKind::Imap) => { backend_builder = backend_builder @@ -304,24 +408,19 @@ impl BackendBuilder { _ => (), } - let delete_folder = deserialized_account_config - .folder - .as_ref() - .and_then(|folder| folder.delete.as_ref()) - .and_then(|delete| delete.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match delete_folder { - Some(_) if account_config.sync && !disable_cache => { - backend_builder = backend_builder.with_delete_folder(|ctx| { - ctx.sync_cache.as_ref().and_then(DeleteFolderMaildir::new) - }); - } + match toml_account_config.delete_folder_kind() { Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_delete_folder(|ctx| { ctx.maildir.as_ref().and_then(DeleteFolderMaildir::new) }); } + Some(BackendKind::MaildirForSync) => { + backend_builder = backend_builder.with_delete_folder(|ctx| { + ctx.maildir_for_sync + .as_ref() + .and_then(DeleteFolderMaildir::new) + }); + } #[cfg(feature = "imap-backend")] Some(BackendKind::Imap) => { backend_builder = backend_builder @@ -336,24 +435,19 @@ impl BackendBuilder { _ => (), } - let get_envelope = deserialized_account_config - .envelope - .as_ref() - .and_then(|envelope| envelope.get.as_ref()) - .and_then(|get| get.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match get_envelope { - Some(_) if account_config.sync && !disable_cache => { - backend_builder = backend_builder.with_get_envelope(|ctx| { - ctx.sync_cache.as_ref().and_then(GetEnvelopeMaildir::new) - }); - } + match toml_account_config.get_envelope_kind() { Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_get_envelope(|ctx| { ctx.maildir.as_ref().and_then(GetEnvelopeMaildir::new) }); } + Some(BackendKind::MaildirForSync) => { + backend_builder = backend_builder.with_get_envelope(|ctx| { + ctx.maildir_for_sync + .as_ref() + .and_then(GetEnvelopeMaildir::new) + }); + } #[cfg(feature = "imap-backend")] Some(BackendKind::Imap) => { backend_builder = backend_builder @@ -368,24 +462,19 @@ impl BackendBuilder { _ => (), } - let list_envelopes = deserialized_account_config - .envelope - .as_ref() - .and_then(|envelope| envelope.list.as_ref()) - .and_then(|send| send.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match list_envelopes { - Some(_) if account_config.sync && !disable_cache => { - backend_builder = backend_builder.with_list_envelopes(|ctx| { - ctx.sync_cache.as_ref().and_then(ListEnvelopesMaildir::new) - }); - } + match toml_account_config.list_envelopes_kind() { Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_list_envelopes(|ctx| { ctx.maildir.as_ref().and_then(ListEnvelopesMaildir::new) }); } + Some(BackendKind::MaildirForSync) => { + backend_builder = backend_builder.with_list_envelopes(|ctx| { + ctx.maildir_for_sync + .as_ref() + .and_then(ListEnvelopesMaildir::new) + }); + } #[cfg(feature = "imap-backend")] Some(BackendKind::Imap) => { backend_builder = backend_builder @@ -400,22 +489,16 @@ impl BackendBuilder { _ => (), } - let add_flags = deserialized_account_config - .flag - .as_ref() - .and_then(|flag| flag.add.as_ref()) - .and_then(|add| add.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match add_flags { - Some(_) if account_config.sync && !disable_cache => { - backend_builder = backend_builder - .with_add_flags(|ctx| ctx.sync_cache.as_ref().and_then(AddFlagsMaildir::new)); - } + match toml_account_config.add_flags_kind() { Some(BackendKind::Maildir) => { backend_builder = backend_builder .with_add_flags(|ctx| ctx.maildir.as_ref().and_then(AddFlagsMaildir::new)); } + Some(BackendKind::MaildirForSync) => { + backend_builder = backend_builder.with_add_flags(|ctx| { + ctx.maildir_for_sync.as_ref().and_then(AddFlagsMaildir::new) + }); + } #[cfg(feature = "imap-backend")] Some(BackendKind::Imap) => { backend_builder = backend_builder @@ -429,22 +512,16 @@ impl BackendBuilder { _ => (), } - let set_flags = deserialized_account_config - .flag - .as_ref() - .and_then(|flag| flag.set.as_ref()) - .and_then(|set| set.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match set_flags { - Some(_) if account_config.sync && !disable_cache => { - backend_builder = backend_builder - .with_set_flags(|ctx| ctx.sync_cache.as_ref().and_then(SetFlagsMaildir::new)); - } + match toml_account_config.set_flags_kind() { Some(BackendKind::Maildir) => { backend_builder = backend_builder .with_set_flags(|ctx| ctx.maildir.as_ref().and_then(SetFlagsMaildir::new)); } + Some(BackendKind::MaildirForSync) => { + backend_builder = backend_builder.with_set_flags(|ctx| { + ctx.maildir_for_sync.as_ref().and_then(SetFlagsMaildir::new) + }); + } #[cfg(feature = "imap-backend")] Some(BackendKind::Imap) => { backend_builder = backend_builder @@ -458,24 +535,19 @@ impl BackendBuilder { _ => (), } - let remove_flags = deserialized_account_config - .flag - .as_ref() - .and_then(|flag| flag.remove.as_ref()) - .and_then(|remove| remove.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match remove_flags { - Some(_) if account_config.sync && !disable_cache => { - backend_builder = backend_builder.with_remove_flags(|ctx| { - ctx.sync_cache.as_ref().and_then(RemoveFlagsMaildir::new) - }); - } + match toml_account_config.remove_flags_kind() { Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_remove_flags(|ctx| { ctx.maildir.as_ref().and_then(RemoveFlagsMaildir::new) }); } + Some(BackendKind::MaildirForSync) => { + backend_builder = backend_builder.with_remove_flags(|ctx| { + ctx.maildir_for_sync + .as_ref() + .and_then(RemoveFlagsMaildir::new) + }); + } #[cfg(feature = "imap-backend")] Some(BackendKind::Imap) => { backend_builder = backend_builder @@ -490,14 +562,7 @@ impl BackendBuilder { _ => (), } - let send_msg = deserialized_account_config - .message - .as_ref() - .and_then(|msg| msg.send.as_ref()) - .and_then(|send| send.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match send_msg { + match toml_account_config.send_raw_message_kind() { #[cfg(feature = "smtp-sender")] Some(BackendKind::Smtp) => { backend_builder = backend_builder.with_send_raw_message(|ctx| { @@ -512,24 +577,17 @@ impl BackendBuilder { _ => (), } - let add_msg = deserialized_account_config - .message - .as_ref() - .and_then(|msg| msg.add.as_ref()) - .and_then(|add| add.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match add_msg { - Some(_) if account_config.sync && !disable_cache => { + match toml_account_config.add_raw_message_kind() { + Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_add_raw_message_with_flags(|ctx| { - ctx.sync_cache + ctx.maildir .as_ref() .and_then(AddRawMessageWithFlagsMaildir::new) }); } - Some(BackendKind::Maildir) => { + Some(BackendKind::MaildirForSync) => { backend_builder = backend_builder.with_add_raw_message_with_flags(|ctx| { - ctx.maildir + ctx.maildir_for_sync .as_ref() .and_then(AddRawMessageWithFlagsMaildir::new) }); @@ -551,24 +609,19 @@ impl BackendBuilder { _ => (), } - let peek_msgs = deserialized_account_config - .message - .as_ref() - .and_then(|msg| msg.peek.as_ref()) - .and_then(|peek| peek.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match peek_msgs { - Some(_) if account_config.sync && !disable_cache => { - backend_builder = backend_builder.with_peek_messages(|ctx| { - ctx.sync_cache.as_ref().and_then(PeekMessagesMaildir::new) - }); - } + match toml_account_config.peek_messages_kind() { Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_peek_messages(|ctx| { ctx.maildir.as_ref().and_then(PeekMessagesMaildir::new) }); } + Some(BackendKind::MaildirForSync) => { + backend_builder = backend_builder.with_peek_messages(|ctx| { + ctx.maildir_for_sync + .as_ref() + .and_then(PeekMessagesMaildir::new) + }); + } #[cfg(feature = "imap-backend")] Some(BackendKind::Imap) => { backend_builder = backend_builder @@ -583,14 +636,7 @@ impl BackendBuilder { _ => (), } - let get_msgs = deserialized_account_config - .message - .as_ref() - .and_then(|msg| msg.get.as_ref()) - .and_then(|get| get.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match get_msgs { + match toml_account_config.get_messages_kind() { #[cfg(feature = "imap-backend")] Some(BackendKind::Imap) => { backend_builder = backend_builder @@ -605,24 +651,19 @@ impl BackendBuilder { _ => (), } - let copy_msgs = deserialized_account_config - .message - .as_ref() - .and_then(|msg| msg.copy.as_ref()) - .and_then(|copy| copy.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match copy_msgs { - Some(_) if account_config.sync && !disable_cache => { - backend_builder = backend_builder.with_copy_messages(|ctx| { - ctx.sync_cache.as_ref().and_then(CopyMessagesMaildir::new) - }); - } + match toml_account_config.copy_messages_kind() { Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_copy_messages(|ctx| { ctx.maildir.as_ref().and_then(CopyMessagesMaildir::new) }); } + Some(BackendKind::MaildirForSync) => { + backend_builder = backend_builder.with_copy_messages(|ctx| { + ctx.maildir_for_sync + .as_ref() + .and_then(CopyMessagesMaildir::new) + }); + } #[cfg(feature = "imap-backend")] Some(BackendKind::Imap) => { backend_builder = backend_builder @@ -637,24 +678,19 @@ impl BackendBuilder { _ => (), } - let move_msgs = deserialized_account_config - .message - .as_ref() - .and_then(|msg| msg.move_.as_ref()) - .and_then(|move_| move_.backend.as_ref()) - .or_else(|| deserialized_account_config.backend.as_ref()); - - match move_msgs { - Some(_) if account_config.sync && !disable_cache => { - backend_builder = backend_builder.with_move_messages(|ctx| { - ctx.sync_cache.as_ref().and_then(MoveMessagesMaildir::new) - }); - } + match toml_account_config.move_messages_kind() { Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_move_messages(|ctx| { ctx.maildir.as_ref().and_then(MoveMessagesMaildir::new) }); } + Some(BackendKind::MaildirForSync) => { + backend_builder = backend_builder.with_move_messages(|ctx| { + ctx.maildir_for_sync + .as_ref() + .and_then(MoveMessagesMaildir::new) + }); + } #[cfg(feature = "imap-backend")] Some(BackendKind::Imap) => { backend_builder = backend_builder @@ -669,10 +705,30 @@ impl BackendBuilder { _ => (), } - Ok(Self(backend_builder)) + Ok(Self { + toml_account_config, + builder: backend_builder, + }) } pub async fn build(self) -> Result { - self.0.build().await + Ok(Backend { + toml_account_config: self.toml_account_config, + backend: self.builder.build().await?, + }) + } +} + +impl Deref for BackendBuilder { + type Target = email::backend::BackendBuilder; + + fn deref(&self) -> &Self::Target { + &self.builder + } +} + +impl Into> for BackendBuilder { + fn into(self) -> email::backend::BackendBuilder { + self.builder } } diff --git a/src/cache/id_mapper.rs b/src/cache/id_mapper.rs index 5193e46..fb2ff86 100644 --- a/src/cache/id_mapper.rs +++ b/src/cache/id_mapper.rs @@ -1,12 +1,8 @@ use anyhow::{anyhow, Context, Result}; use email::account::AccountConfig; -#[cfg(feature = "notmuch-backend")] -use email::backend::NotmuchBackend; use log::{debug, trace}; use std::path::{Path, PathBuf}; -use crate::backend::Backend; - const ID_MAPPER_DB_FILE_NAME: &str = ".id-mapper.sqlite"; #[derive(Debug)] @@ -36,46 +32,28 @@ impl IdMapper { db_path } - // FIXME - pub fn new(backend: &Backend, account_config: &AccountConfig, folder: &str) -> Result { - Ok(IdMapper::Dummy) + pub fn new(account_config: &AccountConfig, folder: &str, db_path: PathBuf) -> Result { + let folder = account_config.get_folder_alias(folder)?; + let digest = md5::compute(account_config.name.clone() + &folder); + let table = format!("id_mapper_{digest:x}"); + debug!("creating id mapper table {table} at {db_path:?}…"); - // #[cfg(feature = "imap-backend")] - // if backend.as_any().is::() { - // return Ok(IdMapper::Dummy); - // } + let db_path = Self::find_closest_db_path(db_path); + let conn = rusqlite::Connection::open(&db_path) + .with_context(|| format!("cannot open id mapper database at {db_path:?}"))?; - // let mut db_path = PathBuf::new(); + let query = format!( + "CREATE TABLE IF NOT EXISTS {table} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + internal_id TEXT UNIQUE + )", + ); + trace!("create table query: {query:#?}"); - // if let Some(backend) = backend.as_any().downcast_ref::() { - // db_path = Self::find_closest_db_path(backend.path()) - // } + conn.execute(&query, []) + .context("cannot create id mapper table")?; - // #[cfg(feature = "notmuch-backend")] - // if let Some(backend) = backend.as_any().downcast_ref::() { - // db_path = Self::find_closest_db_path(backend.path()) - // } - - // let folder = account_config.get_folder_alias(folder)?; - // let digest = md5::compute(account_config.name.clone() + &folder); - // let table = format!("id_mapper_{digest:x}"); - // debug!("creating id mapper table {table} at {db_path:?}…"); - - // let conn = rusqlite::Connection::open(&db_path) - // .with_context(|| format!("cannot open id mapper database at {db_path:?}"))?; - - // let query = format!( - // "CREATE TABLE IF NOT EXISTS {table} ( - // id INTEGER PRIMARY KEY AUTOINCREMENT, - // internal_id TEXT UNIQUE - // )", - // ); - // trace!("create table query: {query:#?}"); - - // conn.execute(&query, []) - // .context("cannot create id mapper table")?; - - // Ok(Self::Mapper(table, conn)) + Ok(Self::Mapper(table, conn)) } pub fn create_alias(&self, id: I) -> Result diff --git a/src/config/config.rs b/src/config/config.rs index a62aaa0..c45f159 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -18,6 +18,7 @@ use toml; use crate::{ account::DeserializedAccountConfig, + backend::BackendKind, config::{prelude::*, wizard}, wizard_prompt, wizard_warn, }; @@ -106,6 +107,7 @@ impl DeserializedConfig { pub fn into_account_configs( self, account_name: Option<&str>, + disable_cache: bool, ) -> Result<(DeserializedAccountConfig, AccountConfig)> { let (account_name, mut toml_account_config) = match account_name { Some("default") | Some("") | None => self @@ -139,6 +141,12 @@ impl DeserializedConfig { .replace_undefined_keyring_entries(&account_name); } + if let Some(true) = toml_account_config.sync { + if !disable_cache { + toml_account_config.backend = Some(BackendKind::MaildirForSync); + } + } + let config = Config { display_name: self.display_name, signature_delim: self.signature_delim, diff --git a/src/domain/account/config.rs b/src/domain/account/config.rs index 529d9cb..df915e7 100644 --- a/src/domain/account/config.rs +++ b/src/domain/account/config.rs @@ -86,3 +86,141 @@ pub struct DeserializedAccountConfig { #[serde(default, with = "OptionPgpConfigDef")] pub pgp: Option, } + +impl DeserializedAccountConfig { + pub fn add_folder_kind(&self) -> Option<&BackendKind> { + self.folder + .as_ref() + .and_then(|folder| folder.add.as_ref()) + .and_then(|add| add.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn list_folders_kind(&self) -> Option<&BackendKind> { + self.folder + .as_ref() + .and_then(|folder| folder.list.as_ref()) + .and_then(|list| list.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn expunge_folder_kind(&self) -> Option<&BackendKind> { + self.folder + .as_ref() + .and_then(|folder| folder.expunge.as_ref()) + .and_then(|expunge| expunge.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn purge_folder_kind(&self) -> Option<&BackendKind> { + self.folder + .as_ref() + .and_then(|folder| folder.purge.as_ref()) + .and_then(|purge| purge.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn delete_folder_kind(&self) -> Option<&BackendKind> { + self.folder + .as_ref() + .and_then(|folder| folder.delete.as_ref()) + .and_then(|delete| delete.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn get_envelope_kind(&self) -> Option<&BackendKind> { + self.envelope + .as_ref() + .and_then(|envelope| envelope.get.as_ref()) + .and_then(|get| get.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn list_envelopes_kind(&self) -> Option<&BackendKind> { + self.envelope + .as_ref() + .and_then(|envelope| envelope.list.as_ref()) + .and_then(|list| list.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn add_flags_kind(&self) -> Option<&BackendKind> { + self.flag + .as_ref() + .and_then(|flag| flag.add.as_ref()) + .and_then(|add| add.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn set_flags_kind(&self) -> Option<&BackendKind> { + self.flag + .as_ref() + .and_then(|flag| flag.set.as_ref()) + .and_then(|set| set.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn remove_flags_kind(&self) -> Option<&BackendKind> { + self.flag + .as_ref() + .and_then(|flag| flag.remove.as_ref()) + .and_then(|remove| remove.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn add_raw_message_kind(&self) -> Option<&BackendKind> { + self.message + .as_ref() + .and_then(|msg| msg.add.as_ref()) + .and_then(|add| add.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn peek_messages_kind(&self) -> Option<&BackendKind> { + self.message + .as_ref() + .and_then(|message| message.peek.as_ref()) + .and_then(|peek| peek.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn get_messages_kind(&self) -> Option<&BackendKind> { + self.message + .as_ref() + .and_then(|message| message.get.as_ref()) + .and_then(|get| get.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn copy_messages_kind(&self) -> Option<&BackendKind> { + self.message + .as_ref() + .and_then(|message| message.copy.as_ref()) + .and_then(|copy| copy.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn move_messages_kind(&self) -> Option<&BackendKind> { + self.message + .as_ref() + .and_then(|message| message.move_.as_ref()) + .and_then(|move_| move_.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn delete_messages_kind(&self) -> Option<&BackendKind> { + self.flag + .as_ref() + .and_then(|flag| flag.remove.as_ref()) + .and_then(|remove| remove.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + + pub fn send_raw_message_kind(&self) -> Option<&BackendKind> { + self.message + .as_ref() + .and_then(|msg| msg.send.as_ref()) + .and_then(|send| send.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } +} diff --git a/src/domain/email/handlers.rs b/src/domain/email/handlers.rs index 3589ea7..fb15a2b 100644 --- a/src/domain/email/handlers.rs +++ b/src/domain/email/handlers.rs @@ -16,18 +16,15 @@ use crate::{ backend::Backend, printer::{PrintTableOpts, Printer}, ui::editor, - Envelopes, IdMapper, }; pub async fn attachments( config: &AccountConfig, printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, folder: &str, ids: Vec<&str>, ) -> Result<()> { - let ids = Id::multiple(id_mapper.get_ids(ids)?); let emails = backend.get_messages(&folder, &ids).await?; let mut index = 0; @@ -77,13 +74,11 @@ pub async fn attachments( pub async fn copy( printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, from_folder: &str, to_folder: &str, ids: Vec<&str>, ) -> Result<()> { - let ids = Id::multiple(id_mapper.get_ids(ids)?); backend .copy_messages(&from_folder, &to_folder, &ids) .await?; @@ -92,12 +87,10 @@ pub async fn copy( pub async fn delete( printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, folder: &str, ids: Vec<&str>, ) -> Result<()> { - let ids = Id::multiple(id_mapper.get_ids(ids)?); backend.delete_messages(&folder, &ids).await?; printer.print("Email(s) successfully deleted!") } @@ -105,16 +98,14 @@ pub async fn delete( pub async fn forward( config: &AccountConfig, printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, folder: &str, id: &str, headers: Option>, body: Option<&str>, ) -> Result<()> { - let id = Id::single(id_mapper.get_id(id)?); let tpl = backend - .get_messages(&folder, &id) + .get_messages(&folder, &[id]) .await? .first() .ok_or_else(|| anyhow!("cannot find email {}", id))? @@ -131,7 +122,6 @@ pub async fn forward( pub async fn list( config: &AccountConfig, printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, folder: &str, max_width: Option, @@ -141,11 +131,7 @@ pub async fn list( let page_size = page_size.unwrap_or(config.email_listing_page_size()); debug!("page size: {}", page_size); - let envelopes = Envelopes::from_backend( - config, - id_mapper, - backend.list_envelopes(&folder, page_size, page).await?, - )?; + let envelopes = backend.list_envelopes(&folder, page_size, page).await?; trace!("envelopes: {:?}", envelopes); printer.print_table( @@ -190,13 +176,11 @@ pub async fn mailto( pub async fn move_( printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, from_folder: &str, to_folder: &str, ids: Vec<&str>, ) -> Result<()> { - let ids = Id::multiple(id_mapper.get_ids(ids)?); backend .move_messages(&from_folder, &to_folder, &ids) .await?; @@ -206,7 +190,6 @@ pub async fn move_( pub async fn read( config: &AccountConfig, printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, folder: &str, ids: Vec<&str>, @@ -214,7 +197,6 @@ pub async fn read( raw: bool, headers: Vec<&str>, ) -> Result<()> { - let ids = Id::multiple(id_mapper.get_ids(ids)?); let emails = backend.get_messages(&folder, &ids).await?; let mut glue = ""; @@ -249,7 +231,6 @@ pub async fn read( pub async fn reply( config: &AccountConfig, printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, folder: &str, id: &str, @@ -257,9 +238,8 @@ pub async fn reply( headers: Option>, body: Option<&str>, ) -> Result<()> { - let id = Id::single(id_mapper.get_id(id)?); let tpl = backend - .get_messages(folder, &id) + .get_messages(folder, &[id]) .await? .first() .ok_or_else(|| anyhow!("cannot find email {}", id))? @@ -271,13 +251,14 @@ pub async fn reply( .await?; trace!("initial template: {tpl}"); editor::edit_tpl_with_editor(config, printer, backend, tpl).await?; - backend.add_flag(&folder, &id, Flag::Answered).await?; + backend + .add_flag(&folder, &Id::single(id), Flag::Answered) + .await?; Ok(()) } pub async fn save( printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, folder: &str, raw_email: String, @@ -295,10 +276,9 @@ pub async fn save( .join("\r\n") }; - let id = backend + backend .add_raw_message(&folder, raw_email.as_bytes()) .await?; - id_mapper.create_alias(&*id)?; Ok(()) } @@ -306,7 +286,6 @@ pub async fn save( pub async fn search( _config: &AccountConfig, _printer: &mut P, - _id_mapper: &IdMapper, _backend: &Backend, _folder: &str, _query: String, @@ -334,7 +313,6 @@ pub async fn search( pub async fn sort( _config: &AccountConfig, _printer: &mut P, - _id_mapper: &IdMapper, _backend: &Backend, _folder: &str, _sort: String, diff --git a/src/domain/flag/handlers.rs b/src/domain/flag/handlers.rs index 875718f..5316316 100644 --- a/src/domain/flag/handlers.rs +++ b/src/domain/flag/handlers.rs @@ -1,43 +1,37 @@ use anyhow::Result; -use email::email::{envelope::Id, Flags}; +use email::email::Flags; -use crate::{backend::Backend, printer::Printer, IdMapper}; +use crate::{backend::Backend, printer::Printer}; pub async fn add( printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, folder: &str, ids: Vec<&str>, flags: &Flags, ) -> Result<()> { - let ids = Id::multiple(id_mapper.get_ids(ids)?); backend.add_flags(folder, &ids, flags).await?; - printer.print("Flag(s) successfully added!") + printer.print(format!("Flag(s) {flags} successfully added!")) } pub async fn set( printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, folder: &str, ids: Vec<&str>, flags: &Flags, ) -> Result<()> { - let ids = Id::multiple(id_mapper.get_ids(ids)?); backend.set_flags(folder, &ids, flags).await?; - printer.print("Flag(s) successfully set!") + printer.print(format!("Flag(s) {flags} successfully set!")) } pub async fn remove( printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, folder: &str, ids: Vec<&str>, flags: &Flags, ) -> Result<()> { - let ids = Id::multiple(id_mapper.get_ids(ids)?); backend.remove_flags(folder, &ids, flags).await?; - printer.print("Flag(s) successfully removed!") + printer.print(format!("Flag(s) {flags} successfully removed!")) } diff --git a/src/domain/tpl/handlers.rs b/src/domain/tpl/handlers.rs index a7515e4..f59b2f7 100644 --- a/src/domain/tpl/handlers.rs +++ b/src/domain/tpl/handlers.rs @@ -2,27 +2,24 @@ use anyhow::{anyhow, Result}; use atty::Stream; use email::{ account::AccountConfig, - email::{envelope::Id, Flag, Message}, + email::{Flag, Message}, }; use mml::MmlCompilerBuilder; use std::io::{stdin, BufRead}; -use crate::{backend::Backend, printer::Printer, IdMapper}; +use crate::{backend::Backend, printer::Printer}; pub async fn forward( config: &AccountConfig, printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, folder: &str, id: &str, headers: Option>, body: Option<&str>, ) -> Result<()> { - let ids = Id::multiple(id_mapper.get_ids([id])?); - let tpl: String = backend - .get_messages(folder, &ids) + .get_messages(folder, &[id]) .await? .first() .ok_or_else(|| anyhow!("cannot find email {}", id))? @@ -39,7 +36,6 @@ pub async fn forward( pub async fn reply( config: &AccountConfig, printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, folder: &str, id: &str, @@ -47,10 +43,8 @@ pub async fn reply( headers: Option>, body: Option<&str>, ) -> Result<()> { - let ids = Id::multiple(id_mapper.get_ids([id])?); - let tpl: String = backend - .get_messages(folder, &ids) + .get_messages(folder, &[id]) .await? .first() .ok_or_else(|| anyhow!("cannot find email {}", id))? @@ -68,7 +62,6 @@ pub async fn reply( pub async fn save( #[allow(unused_variables)] config: &AccountConfig, printer: &mut P, - id_mapper: &IdMapper, backend: &Backend, folder: &str, tpl: String, @@ -91,8 +84,7 @@ pub async fn save( let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; - let id = backend.add_raw_message(folder, &email).await?; - id_mapper.create_alias(&*id)?; + backend.add_raw_message(folder, &email).await?; printer.print("Template successfully saved!") } diff --git a/src/main.rs b/src/main.rs index 3eed799..43c6fcb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ use himalaya::{ config::{self, DeserializedConfig}, email, flag, folder, man, output, printer::StdoutPrinter, - tpl, IdMapper, + tpl, }; fn create_app() -> Command { @@ -59,9 +59,9 @@ async fn main() -> Result<()> { let url = Url::parse(&raw_args[1])?; let (toml_account_config, account_config) = DeserializedConfig::from_opt_path(None) .await? - .into_account_configs(None)?; + .into_account_configs(None, false)?; let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), false).await?; + BackendBuilder::new(toml_account_config, account_config.clone()).await?; let backend = backend_builder.build().await?; let mut printer = StdoutPrinter::default(); @@ -88,14 +88,11 @@ async fn main() -> Result<()> { _ => (), } - let maybe_account_name = account::args::parse_arg(&m); - let toml_config = DeserializedConfig::from_opt_path(config::args::parse_arg(&m)).await?; - let (toml_account_config, account_config) = toml_config - .clone() - .into_account_configs(maybe_account_name)?; - let folder = folder::args::parse_source_arg(&m); let disable_cache = cache::args::parse_disable_cache_flag(&m); + let maybe_account_name = account::args::parse_arg(&m); + + let toml_config = DeserializedConfig::from_opt_path(config::args::parse_arg(&m)).await?; let mut printer = StdoutPrinter::try_from(&m)?; @@ -122,64 +119,57 @@ async fn main() -> Result<()> { match account::args::matches(&m)? { Some(account::args::Cmd::List(max_width)) => { - account::handlers::list(max_width, &account_config, &toml_config, &mut printer)?; - return Ok(()); + let (_, account_config) = toml_config + .clone() + .into_account_configs(maybe_account_name, disable_cache)?; + return account::handlers::list(max_width, &account_config, &toml_config, &mut printer); } Some(account::args::Cmd::Sync(strategy, dry_run)) => { + let (toml_account_config, account_config) = toml_config + .clone() + .into_account_configs(maybe_account_name, true)?; let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), true).await?; - let sync_builder = AccountSyncBuilder::new(backend_builder.0) + BackendBuilder::new(toml_account_config, account_config.clone()).await?; + let sync_builder = AccountSyncBuilder::new(backend_builder.into()) .await? .with_some_folders_strategy(strategy) .with_dry_run(dry_run); - account::handlers::sync(&mut printer, sync_builder, dry_run).await?; - return Ok(()); + return account::handlers::sync(&mut printer, sync_builder, dry_run).await; } Some(account::args::Cmd::Configure(reset)) => { - account::handlers::configure(&account_config, reset).await?; - return Ok(()); + let (_, account_config) = toml_config + .clone() + .into_account_configs(maybe_account_name, disable_cache)?; + return account::handlers::configure(&account_config, reset).await; } _ => (), } + let (toml_account_config, account_config) = toml_config + .clone() + .into_account_configs(maybe_account_name, disable_cache)?; + println!("toml_account_config: {:#?}", toml_account_config); + let backend_builder = BackendBuilder::new(toml_account_config, account_config.clone()).await?; + let backend = backend_builder.build().await?; + // checks folder commands match folder::args::matches(&m)? { Some(folder::args::Cmd::Create) => { let folder = folder .ok_or_else(|| anyhow!("the folder argument is missing")) .context("cannot create folder")?; - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - return folder::handlers::create(&mut printer, &backend, &folder).await; } Some(folder::args::Cmd::List(max_width)) => { - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - return folder::handlers::list(&account_config, &mut printer, &backend, max_width) .await; } Some(folder::args::Cmd::Expunge) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - return folder::handlers::expunge(&mut printer, &backend, &folder).await; } Some(folder::args::Cmd::Delete) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - return folder::handlers::delete(&mut printer, &backend, &folder).await; } _ => (), @@ -189,16 +179,9 @@ async fn main() -> Result<()> { match email::args::matches(&m)? { Some(email::args::Cmd::Attachments(ids)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - return email::handlers::attachments( &account_config, &mut printer, - &id_mapper, &backend, &folder, ids, @@ -207,44 +190,17 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Copy(ids, to_folder)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - - return email::handlers::copy( - &mut printer, - &id_mapper, - &backend, - &folder, - to_folder, - ids, - ) - .await; + return email::handlers::copy(&mut printer, &backend, &folder, to_folder, ids).await; } Some(email::args::Cmd::Delete(ids)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - - return email::handlers::delete(&mut printer, &id_mapper, &backend, &folder, ids).await; + return email::handlers::delete(&mut printer, &backend, &folder, ids).await; } Some(email::args::Cmd::Forward(id, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - return email::handlers::forward( &account_config, &mut printer, - &id_mapper, &backend, &folder, id, @@ -255,16 +211,9 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::List(max_width, page_size, page)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - return email::handlers::list( &account_config, &mut printer, - &id_mapper, &backend, &folder, max_width, @@ -275,34 +224,13 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Move(ids, to_folder)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - - return email::handlers::move_( - &mut printer, - &id_mapper, - &backend, - &folder, - to_folder, - ids, - ) - .await; + return email::handlers::move_(&mut printer, &backend, &folder, to_folder, ids).await; } Some(email::args::Cmd::Read(ids, text_mime, raw, headers)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - return email::handlers::read( &account_config, &mut printer, - &id_mapper, &backend, &folder, ids, @@ -314,16 +242,9 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Reply(id, all, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - return email::handlers::reply( &account_config, &mut printer, - &id_mapper, &backend, &folder, id, @@ -335,27 +256,13 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Save(raw_email)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - - return email::handlers::save(&mut printer, &id_mapper, &backend, &folder, raw_email) - .await; + return email::handlers::save(&mut printer, &backend, &folder, raw_email).await; } Some(email::args::Cmd::Search(query, max_width, page_size, page)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - return email::handlers::search( &account_config, &mut printer, - &id_mapper, &backend, &folder, query, @@ -367,16 +274,9 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Sort(criteria, query, max_width, page_size, page)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - return email::handlers::sort( &account_config, &mut printer, - &id_mapper, &backend, &folder, criteria, @@ -388,79 +288,29 @@ async fn main() -> Result<()> { .await; } Some(email::args::Cmd::Send(raw_email)) => { - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; return email::handlers::send(&account_config, &mut printer, &backend, raw_email).await; } Some(email::args::Cmd::Flag(m)) => match m { Some(flag::args::Cmd::Set(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - return flag::handlers::set( - &mut printer, - &id_mapper, - &backend, - &folder, - ids, - flags, - ) - .await; + return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await; } Some(flag::args::Cmd::Add(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - return flag::handlers::add( - &mut printer, - &id_mapper, - &backend, - &folder, - ids, - flags, - ) - .await; + return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await; } Some(flag::args::Cmd::Remove(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - - return flag::handlers::remove( - &mut printer, - &id_mapper, - &backend, - &folder, - ids, - flags, - ) - .await; + return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await; } _ => (), }, Some(email::args::Cmd::Tpl(m)) => match m { Some(tpl::args::Cmd::Forward(id, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; return tpl::handlers::forward( &account_config, &mut printer, - &id_mapper, &backend, &folder, id, @@ -475,15 +325,9 @@ async fn main() -> Result<()> { } Some(tpl::args::Cmd::Reply(id, all, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; return tpl::handlers::reply( &account_config, &mut printer, - &id_mapper, &backend, &folder, id, @@ -495,35 +339,15 @@ async fn main() -> Result<()> { } Some(tpl::args::Cmd::Save(tpl)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; - let id_mapper = IdMapper::new(&backend, &account_config, &folder)?; - return tpl::handlers::save( - &account_config, - &mut printer, - &id_mapper, - &backend, - &folder, - tpl, - ) - .await; + return tpl::handlers::save(&account_config, &mut printer, &backend, &folder, tpl) + .await; } Some(tpl::args::Cmd::Send(tpl)) => { - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; return tpl::handlers::send(&account_config, &mut printer, &backend, tpl).await; } _ => (), }, Some(email::args::Cmd::Write(headers, body)) => { - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), disable_cache) - .await?; - let backend = backend_builder.build().await?; return email::handlers::write(&account_config, &mut printer, &backend, headers, body) .await; } From 41a2f0269973561edfbb020ff32027b8d1817297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 29 Nov 2023 07:52:08 +0100 Subject: [PATCH 08/29] rename config and account config --- src/backend.rs | 12 ++++--- src/config/config.rs | 64 +++++++++++++++++----------------- src/config/wizard.rs | 6 ++-- src/domain/account/accounts.rs | 6 ++-- src/domain/account/config.rs | 4 +-- src/domain/account/handlers.rs | 14 ++++---- src/domain/account/wizard.rs | 6 ++-- src/main.rs | 13 +++---- 8 files changed, 64 insertions(+), 61 deletions(-) diff --git a/src/backend.rs b/src/backend.rs index eef8843..9284a77 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -45,11 +45,13 @@ use email::{ }; use serde::{Deserialize, Serialize}; -use crate::{account::DeserializedAccountConfig, Envelopes, IdMapper}; +use crate::{account::TomlAccountConfig, Envelopes, IdMapper}; -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum BackendKind { + #[default] + None, Maildir, #[serde(skip_deserializing)] MaildirForSync, @@ -123,7 +125,7 @@ pub struct BackendContext { } pub struct Backend { - toml_account_config: DeserializedAccountConfig, + toml_account_config: TomlAccountConfig, backend: email::backend::Backend, } @@ -262,13 +264,13 @@ impl Deref for Backend { } pub struct BackendBuilder { - toml_account_config: DeserializedAccountConfig, + toml_account_config: TomlAccountConfig, builder: email::backend::BackendBuilder, } impl BackendBuilder { pub async fn new( - toml_account_config: DeserializedAccountConfig, + toml_account_config: TomlAccountConfig, account_config: AccountConfig, ) -> Result { let backend_ctx_builder = BackendContextBuilder { diff --git a/src/config/config.rs b/src/config/config.rs index c45f159..9c618d7 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -17,7 +17,7 @@ use std::{collections::HashMap, fs, path::PathBuf, process::exit}; use toml; use crate::{ - account::DeserializedAccountConfig, + account::TomlAccountConfig, backend::BackendKind, config::{prelude::*, wizard}, wizard_prompt, wizard_warn, @@ -26,7 +26,7 @@ use crate::{ /// Represents the user config file. #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] -pub struct DeserializedConfig { +pub struct TomlConfig { #[serde(alias = "name")] pub display_name: Option, pub signature_delim: Option, @@ -48,12 +48,12 @@ pub struct DeserializedConfig { pub email_hooks: Option, #[serde(flatten)] - pub accounts: HashMap, + pub accounts: HashMap, } -impl DeserializedConfig { +impl TomlConfig { /// Tries to create a config from an optional path. - pub async fn from_opt_path(path: Option<&str>) -> Result { + pub async fn from_maybe_path(path: Option<&str>) -> Result { debug!("path: {:?}", path); let config = if let Some(path) = path.map(PathBuf::from).or_else(Self::path) { @@ -108,7 +108,7 @@ impl DeserializedConfig { self, account_name: Option<&str>, disable_cache: bool, - ) -> Result<(DeserializedAccountConfig, AccountConfig)> { + ) -> Result<(TomlAccountConfig, AccountConfig)> { let (account_name, mut toml_account_config) = match account_name { Some("default") | Some("") | None => self .accounts @@ -229,10 +229,10 @@ mod tests { use super::*; - async fn make_config(config: &str) -> Result { + async fn make_config(config: &str) -> Result { let mut file = NamedTempFile::new().unwrap(); write!(file, "{}", config).unwrap(); - DeserializedConfig::from_opt_path(file.into_temp_path().to_str()).await + TomlConfig::from_maybe_path(file.into_temp_path().to_str()).await } #[tokio::test] @@ -515,18 +515,18 @@ mod tests { assert_eq!( config.unwrap(), - DeserializedConfig { + TomlConfig { accounts: HashMap::from_iter([( "account".into(), - DeserializedAccountConfig { + TomlAccountConfig { email: "test@localhost".into(), sender: SenderConfig::Sendmail(SendmailConfig { cmd: "/usr/sbin/sendmail".into() }), - ..DeserializedAccountConfig::default() + ..TomlAccountConfig::default() } )]), - ..DeserializedConfig::default() + ..TomlConfig::default() } ) } @@ -551,10 +551,10 @@ mod tests { assert_eq!( config.unwrap(), - DeserializedConfig { + TomlConfig { accounts: HashMap::from_iter([( "account".into(), - DeserializedAccountConfig { + TomlAccountConfig { email: "test@localhost".into(), sender: SenderConfig::Smtp(SmtpConfig { host: "localhost".into(), @@ -565,10 +565,10 @@ mod tests { }), ..SmtpConfig::default() }), - ..DeserializedAccountConfig::default() + ..TomlAccountConfig::default() } )]), - ..DeserializedConfig::default() + ..TomlConfig::default() } ) } @@ -586,18 +586,18 @@ mod tests { assert_eq!( config.unwrap(), - DeserializedConfig { + TomlConfig { accounts: HashMap::from_iter([( "account".into(), - DeserializedAccountConfig { + TomlAccountConfig { email: "test@localhost".into(), sender: SenderConfig::Sendmail(SendmailConfig { cmd: Cmd::from("echo send") }), - ..DeserializedAccountConfig::default() + ..TomlAccountConfig::default() } )]), - ..DeserializedConfig::default() + ..TomlConfig::default() } ) } @@ -619,10 +619,10 @@ mod tests { assert_eq!( config.unwrap(), - DeserializedConfig { + TomlConfig { accounts: HashMap::from_iter([( "account".into(), - DeserializedAccountConfig { + TomlAccountConfig { email: "test@localhost".into(), backend: BackendConfig::Imap(ImapConfig { host: "localhost".into(), @@ -633,10 +633,10 @@ mod tests { }), ..ImapConfig::default() }), - ..DeserializedAccountConfig::default() + ..TomlAccountConfig::default() } )]), - ..DeserializedConfig::default() + ..TomlConfig::default() } ) } @@ -654,18 +654,18 @@ mod tests { assert_eq!( config.unwrap(), - DeserializedConfig { + TomlConfig { accounts: HashMap::from_iter([( "account".into(), - DeserializedAccountConfig { + TomlAccountConfig { email: "test@localhost".into(), backend: BackendConfig::Maildir(MaildirConfig { root_dir: "/tmp/maildir".into(), }), - ..DeserializedAccountConfig::default() + ..TomlAccountConfig::default() } )]), - ..DeserializedConfig::default() + ..TomlConfig::default() } ) } @@ -684,18 +684,18 @@ mod tests { assert_eq!( config.unwrap(), - DeserializedConfig { + TomlConfig { accounts: HashMap::from_iter([( "account".into(), - DeserializedAccountConfig { + TomlAccountConfig { email: "test@localhost".into(), backend: BackendConfig::Notmuch(NotmuchConfig { db_path: "/tmp/notmuch.db".into(), }), - ..DeserializedAccountConfig::default() + ..TomlAccountConfig::default() } )]), - ..DeserializedConfig::default() + ..TomlConfig::default() } ); } diff --git a/src/config/wizard.rs b/src/config/wizard.rs index f185f53..f6add5e 100644 --- a/src/config/wizard.rs +++ b/src/config/wizard.rs @@ -1,4 +1,4 @@ -use super::DeserializedConfig; +use super::TomlConfig; use crate::account; use anyhow::Result; use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select}; @@ -31,10 +31,10 @@ macro_rules! wizard_log { pub(crate) static THEME: Lazy = Lazy::new(ColorfulTheme::default); -pub(crate) async fn configure() -> Result { +pub(crate) async fn configure() -> Result { wizard_log!("Configuring your first account:"); - let mut config = DeserializedConfig::default(); + let mut config = TomlConfig::default(); while let Some((name, account_config)) = account::wizard::configure().await? { config.accounts.insert(name, account_config); diff --git a/src/domain/account/accounts.rs b/src/domain/account/accounts.rs index c1e9665..59a4579 100644 --- a/src/domain/account/accounts.rs +++ b/src/domain/account/accounts.rs @@ -13,7 +13,7 @@ use crate::{ ui::Table, }; -use super::{Account, DeserializedAccountConfig}; +use super::{Account, TomlAccountConfig}; /// Represents the list of printable accounts. #[derive(Debug, Default, Serialize)] @@ -36,8 +36,8 @@ impl PrintTable for Accounts { } } -impl From> for Accounts { - fn from(map: Iter<'_, String, DeserializedAccountConfig>) -> Self { +impl From> for Accounts { + fn from(map: Iter<'_, String, TomlAccountConfig>) -> Self { let mut accounts: Vec<_> = map .map(|(name, account)| { let mut backends = String::new(); diff --git a/src/domain/account/config.rs b/src/domain/account/config.rs index df915e7..f64396f 100644 --- a/src/domain/account/config.rs +++ b/src/domain/account/config.rs @@ -29,7 +29,7 @@ use crate::{ /// Represents all existing kind of account config. #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[serde(tag = "backend", rename_all = "kebab-case")] -pub struct DeserializedAccountConfig { +pub struct TomlAccountConfig { pub default: Option, pub email: String, @@ -87,7 +87,7 @@ pub struct DeserializedAccountConfig { pub pgp: Option, } -impl DeserializedAccountConfig { +impl TomlAccountConfig { pub fn add_folder_kind(&self) -> Option<&BackendKind> { self.folder .as_ref() diff --git a/src/domain/account/handlers.rs b/src/domain/account/handlers.rs index bec3fc3..29d5a5b 100644 --- a/src/domain/account/handlers.rs +++ b/src/domain/account/handlers.rs @@ -20,7 +20,7 @@ use crate::{ backend::BackendContextBuilder, config::{ wizard::{prompt_passwd, prompt_secret}, - DeserializedConfig, + TomlConfig, }, printer::{PrintTableOpts, Printer}, Accounts, @@ -120,7 +120,7 @@ pub async fn configure(config: &AccountConfig, reset: bool) -> Result<()> { pub fn list<'a, P: Printer>( max_width: Option, config: &AccountConfig, - deserialized_config: &DeserializedConfig, + deserialized_config: &TomlConfig, printer: &mut P, ) -> Result<()> { info!("entering the list accounts handler"); @@ -298,7 +298,7 @@ mod tests { use termcolor::ColorSpec; use crate::{ - account::DeserializedAccountConfig, + account::TomlAccountConfig, printer::{Print, PrintTable, WriteColor}, }; @@ -367,16 +367,16 @@ mod tests { let mut printer = PrinterServiceTest::default(); let config = AccountConfig::default(); - let deserialized_config = DeserializedConfig { + let deserialized_config = TomlConfig { accounts: HashMap::from_iter([( "account-1".into(), - DeserializedAccountConfig { + TomlAccountConfig { default: Some(true), backend: BackendConfig::Imap(ImapConfig::default()), - ..DeserializedAccountConfig::default() + ..TomlAccountConfig::default() }, )]), - ..DeserializedConfig::default() + ..TomlConfig::default() }; assert!(list(None, &config, &deserialized_config, &mut printer).is_ok()); diff --git a/src/domain/account/wizard.rs b/src/domain/account/wizard.rs index f64f394..84ff44e 100644 --- a/src/domain/account/wizard.rs +++ b/src/domain/account/wizard.rs @@ -4,10 +4,10 @@ use email_address::EmailAddress; use crate::config::wizard::THEME; -use super::DeserializedAccountConfig; +use super::TomlAccountConfig; -pub(crate) async fn configure() -> Result> { - let mut config = DeserializedAccountConfig::default(); +pub(crate) async fn configure() -> Result> { + let mut config = TomlAccountConfig::default(); let account_name = Input::with_theme(&*THEME) .with_prompt("Account name") diff --git a/src/main.rs b/src/main.rs index 43c6fcb..2cd9433 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use himalaya::{ account, backend::BackendBuilder, cache, compl, - config::{self, DeserializedConfig}, + config::{self, TomlConfig}, email, flag, folder, man, output, printer::StdoutPrinter, tpl, @@ -53,11 +53,11 @@ async fn main() -> Result<()> { let default_env_filter = env_logger::DEFAULT_FILTER_ENV; env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off")); - // checks mailto command before app initialization + // check mailto command before app initialization let raw_args: Vec = env::args().collect(); if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { let url = Url::parse(&raw_args[1])?; - let (toml_account_config, account_config) = DeserializedConfig::from_opt_path(None) + let (toml_account_config, account_config) = TomlConfig::from_maybe_path(None) .await? .into_account_configs(None, false)?; let backend_builder = @@ -88,11 +88,12 @@ async fn main() -> Result<()> { _ => (), } - let folder = folder::args::parse_source_arg(&m); - let disable_cache = cache::args::parse_disable_cache_flag(&m); + let maybe_config_path = config::args::parse_arg(&m); let maybe_account_name = account::args::parse_arg(&m); + let disable_cache = cache::args::parse_disable_cache_flag(&m); + let folder = folder::args::parse_source_arg(&m); - let toml_config = DeserializedConfig::from_opt_path(config::args::parse_arg(&m)).await?; + let toml_config = TomlConfig::from_maybe_path(maybe_config_path).await?; let mut printer = StdoutPrinter::try_from(&m)?; From a5cacb3f674aa94632720d98b7f59eadd2fd25aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 29 Nov 2023 11:04:25 +0100 Subject: [PATCH 09/29] build only used backends --- src/backend.rs | 359 +++++++++++++---------- src/domain/account/config.rs | 31 +- src/domain/email/envelope/config.rs | 43 ++- src/domain/email/envelope/flag/config.rs | 59 +++- src/domain/email/message/config.rs | 107 ++++++- src/domain/folder/config.rs | 91 +++++- src/main.rs | 43 ++- 7 files changed, 562 insertions(+), 171 deletions(-) diff --git a/src/backend.rs b/src/backend.rs index 9284a77..b907263 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -47,7 +47,7 @@ use serde::{Deserialize, Serialize}; use crate::{account::TomlAccountConfig, Envelopes, IdMapper}; -#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum BackendKind { #[default] @@ -124,145 +124,6 @@ pub struct BackendContext { pub sendmail: Option, } -pub struct Backend { - toml_account_config: TomlAccountConfig, - backend: email::backend::Backend, -} - -impl Backend { - fn build_id_mapper( - &self, - folder: &str, - backend_kind: Option<&BackendKind>, - ) -> Result { - let mut id_mapper = IdMapper::Dummy; - - match backend_kind { - Some(BackendKind::Maildir) => { - if let Some(mdir_config) = &self.toml_account_config.maildir { - id_mapper = IdMapper::new( - &self.backend.account_config, - folder, - mdir_config.root_dir.clone(), - )?; - } - } - Some(BackendKind::MaildirForSync) => { - id_mapper = IdMapper::new( - &self.backend.account_config, - folder, - self.backend.account_config.sync_dir()?, - )?; - } - #[cfg(feature = "notmuch-backend")] - Some(BackendKind::Notmuch) => { - if let Some(notmuch_config) = &self.toml_account_config.notmuch { - id_mapper = IdMapper::new( - &self.backend.account_config, - folder, - mdir_config.root_dir.clone(), - )?; - } - } - _ => (), - }; - - Ok(id_mapper) - } - - pub async fn list_envelopes( - &self, - folder: &str, - page_size: usize, - page: usize, - ) -> Result { - let backend_kind = self.toml_account_config.list_envelopes_kind(); - let id_mapper = self.build_id_mapper(folder, backend_kind)?; - let envelopes = self.backend.list_envelopes(folder, page_size, page).await?; - let envelopes = Envelopes::from_backend(&self.account_config, &id_mapper, envelopes)?; - Ok(envelopes) - } - - pub async fn add_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { - let backend_kind = self.toml_account_config.add_flags_kind(); - let id_mapper = self.build_id_mapper(folder, backend_kind)?; - let ids = Id::multiple(id_mapper.get_ids(ids)?); - self.backend.add_flags(folder, &ids, flags).await - } - - pub async fn set_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { - let backend_kind = self.toml_account_config.set_flags_kind(); - let id_mapper = self.build_id_mapper(folder, backend_kind)?; - let ids = Id::multiple(id_mapper.get_ids(ids)?); - self.backend.set_flags(folder, &ids, flags).await - } - - pub async fn remove_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { - let backend_kind = self.toml_account_config.remove_flags_kind(); - let id_mapper = self.build_id_mapper(folder, backend_kind)?; - let ids = Id::multiple(id_mapper.get_ids(ids)?); - self.backend.remove_flags(folder, &ids, flags).await - } - - pub async fn get_messages(&self, folder: &str, ids: &[&str]) -> Result { - let backend_kind = self.toml_account_config.get_messages_kind(); - let id_mapper = self.build_id_mapper(folder, backend_kind)?; - let ids = Id::multiple(id_mapper.get_ids(ids)?); - self.backend.get_messages(folder, &ids).await - } - - pub async fn copy_messages( - &self, - from_folder: &str, - to_folder: &str, - ids: &[&str], - ) -> Result<()> { - let backend_kind = self.toml_account_config.move_messages_kind(); - let id_mapper = self.build_id_mapper(from_folder, backend_kind)?; - let ids = Id::multiple(id_mapper.get_ids(ids)?); - self.backend - .copy_messages(from_folder, to_folder, &ids) - .await - } - - pub async fn move_messages( - &self, - from_folder: &str, - to_folder: &str, - ids: &[&str], - ) -> Result<()> { - let backend_kind = self.toml_account_config.move_messages_kind(); - let id_mapper = self.build_id_mapper(from_folder, backend_kind)?; - let ids = Id::multiple(id_mapper.get_ids(ids)?); - self.backend - .move_messages(from_folder, to_folder, &ids) - .await - } - - pub async fn delete_messages(&self, folder: &str, ids: &[&str]) -> Result<()> { - let backend_kind = self.toml_account_config.delete_messages_kind(); - let id_mapper = self.build_id_mapper(folder, backend_kind)?; - let ids = Id::multiple(id_mapper.get_ids(ids)?); - self.backend.delete_messages(folder, &ids).await - } - - pub async fn add_raw_message(&self, folder: &str, email: &[u8]) -> Result { - let backend_kind = self.toml_account_config.add_raw_message_kind(); - let id_mapper = self.build_id_mapper(folder, backend_kind)?; - let id = self.backend.add_raw_message(folder, email).await?; - id_mapper.create_alias(&*id)?; - Ok(id) - } -} - -impl Deref for Backend { - type Target = email::backend::Backend; - - fn deref(&self) -> &Self::Target { - &self.backend - } -} - pub struct BackendBuilder { toml_account_config: TomlAccountConfig, builder: email::backend::BackendBuilder, @@ -272,32 +133,64 @@ impl BackendBuilder { pub async fn new( toml_account_config: TomlAccountConfig, account_config: AccountConfig, + with_sending: bool, ) -> Result { + let used_backends = toml_account_config.get_used_backends(); + + let is_maildir_used = used_backends.contains(&BackendKind::Maildir); + let is_maildir_for_sync_used = used_backends.contains(&BackendKind::MaildirForSync); + #[cfg(feature = "imap-backend")] + let is_imap_used = used_backends.contains(&BackendKind::Imap); + #[cfg(feature = "notmuch-backend")] + let is_notmuch_used = used_backends.contains(&BackendKind::Notmuch); + #[cfg(feature = "smtp-sender")] + let is_smtp_used = used_backends.contains(&BackendKind::Smtp); + let is_sendmail_used = used_backends.contains(&BackendKind::Sendmail); + let backend_ctx_builder = BackendContextBuilder { - maildir: toml_account_config.maildir.as_ref().map(|mdir_config| { - MaildirSessionBuilder::new(account_config.clone(), mdir_config.clone()) - }), - maildir_for_sync: Some(MaildirSessionBuilder::new( - account_config.clone(), - MaildirConfig { - root_dir: account_config.sync_dir()?, - }, - )), + maildir: toml_account_config + .maildir + .as_ref() + .filter(|_| is_maildir_used) + .map(|mdir_config| { + MaildirSessionBuilder::new(account_config.clone(), mdir_config.clone()) + }), + maildir_for_sync: Some(MaildirConfig { + root_dir: account_config.sync_dir()?, + }) + .filter(|_| is_maildir_for_sync_used) + .map(|mdir_config| MaildirSessionBuilder::new(account_config.clone(), mdir_config)), + #[cfg(feature = "imap-backend")] - imap: toml_account_config.imap.as_ref().map(|imap_config| { - ImapSessionBuilder::new(account_config.clone(), imap_config.clone()) - }), + imap: toml_account_config + .imap + .as_ref() + .filter(|_| is_imap_used) + .map(|imap_config| { + ImapSessionBuilder::new(account_config.clone(), imap_config.clone()) + }), #[cfg(feature = "notmuch-backend")] - notmuch: toml_account_config.notmuch.as_ref().map(|notmuch_config| { - NotmuchSessionBuilder::new(account_config.clone(), notmuch_config.clone()) - }), + notmuch: toml_account_config + .notmuch + .as_ref() + .filter(|_| is_notmuch_used) + .map(|notmuch_config| { + NotmuchSessionBuilder::new(account_config.clone(), notmuch_config.clone()) + }), #[cfg(feature = "smtp-sender")] - smtp: toml_account_config.smtp.as_ref().map(|smtp_config| { - SmtpClientBuilder::new(account_config.clone(), smtp_config.clone()) - }), + smtp: toml_account_config + .smtp + .as_ref() + .filter(|_| with_sending) + .filter(|_| is_smtp_used) + .map(|smtp_config| { + SmtpClientBuilder::new(account_config.clone(), smtp_config.clone()) + }), sendmail: toml_account_config .sendmail .as_ref() + .filter(|_| with_sending) + .filter(|_| is_sendmail_used) .map(|sendmail_config| { SendmailContext::new(account_config.clone(), sendmail_config.clone()) }), @@ -734,3 +627,153 @@ impl Into> for BackendBuil self.builder } } + +pub struct Backend { + toml_account_config: TomlAccountConfig, + backend: email::backend::Backend, +} + +impl Backend { + pub async fn new( + toml_account_config: TomlAccountConfig, + account_config: AccountConfig, + with_sending: bool, + ) -> Result { + BackendBuilder::new(toml_account_config, account_config, with_sending) + .await? + .build() + .await + } + + fn build_id_mapper( + &self, + folder: &str, + backend_kind: Option<&BackendKind>, + ) -> Result { + let mut id_mapper = IdMapper::Dummy; + + match backend_kind { + Some(BackendKind::Maildir) => { + if let Some(mdir_config) = &self.toml_account_config.maildir { + id_mapper = IdMapper::new( + &self.backend.account_config, + folder, + mdir_config.root_dir.clone(), + )?; + } + } + Some(BackendKind::MaildirForSync) => { + id_mapper = IdMapper::new( + &self.backend.account_config, + folder, + self.backend.account_config.sync_dir()?, + )?; + } + #[cfg(feature = "notmuch-backend")] + Some(BackendKind::Notmuch) => { + if let Some(notmuch_config) = &self.toml_account_config.notmuch { + id_mapper = IdMapper::new( + &self.backend.account_config, + folder, + mdir_config.root_dir.clone(), + )?; + } + } + _ => (), + }; + + Ok(id_mapper) + } + + pub async fn list_envelopes( + &self, + folder: &str, + page_size: usize, + page: usize, + ) -> Result { + let backend_kind = self.toml_account_config.list_envelopes_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let envelopes = self.backend.list_envelopes(folder, page_size, page).await?; + let envelopes = Envelopes::from_backend(&self.account_config, &id_mapper, envelopes)?; + Ok(envelopes) + } + + pub async fn add_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { + let backend_kind = self.toml_account_config.add_flags_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.add_flags(folder, &ids, flags).await + } + + pub async fn set_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { + let backend_kind = self.toml_account_config.set_flags_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.set_flags(folder, &ids, flags).await + } + + pub async fn remove_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { + let backend_kind = self.toml_account_config.remove_flags_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.remove_flags(folder, &ids, flags).await + } + + pub async fn get_messages(&self, folder: &str, ids: &[&str]) -> Result { + let backend_kind = self.toml_account_config.get_messages_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.get_messages(folder, &ids).await + } + + pub async fn copy_messages( + &self, + from_folder: &str, + to_folder: &str, + ids: &[&str], + ) -> Result<()> { + let backend_kind = self.toml_account_config.move_messages_kind(); + let id_mapper = self.build_id_mapper(from_folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend + .copy_messages(from_folder, to_folder, &ids) + .await + } + + pub async fn move_messages( + &self, + from_folder: &str, + to_folder: &str, + ids: &[&str], + ) -> Result<()> { + let backend_kind = self.toml_account_config.move_messages_kind(); + let id_mapper = self.build_id_mapper(from_folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend + .move_messages(from_folder, to_folder, &ids) + .await + } + + pub async fn delete_messages(&self, folder: &str, ids: &[&str]) -> Result<()> { + let backend_kind = self.toml_account_config.delete_messages_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.delete_messages(folder, &ids).await + } + + pub async fn add_raw_message(&self, folder: &str, email: &[u8]) -> Result { + let backend_kind = self.toml_account_config.add_raw_message_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let id = self.backend.add_raw_message(folder, email).await?; + id_mapper.create_alias(&*id)?; + Ok(id) + } +} + +impl Deref for Backend { + type Target = email::backend::Backend; + + fn deref(&self) -> &Self::Target { + &self.backend + } +} diff --git a/src/domain/account/config.rs b/src/domain/account/config.rs index f64396f..d514396 100644 --- a/src/domain/account/config.rs +++ b/src/domain/account/config.rs @@ -16,7 +16,10 @@ use email::{ sendmail::SendmailConfig, }; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, path::PathBuf}; +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, +}; use crate::{ backend::BackendKind, @@ -223,4 +226,30 @@ impl TomlAccountConfig { .and_then(|send| send.backend.as_ref()) .or_else(|| self.backend.as_ref()) } + + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut used_backends = HashSet::default(); + + if let Some(ref kind) = self.backend { + used_backends.insert(kind); + } + + if let Some(ref folder) = self.folder { + used_backends.extend(folder.get_used_backends()); + } + + if let Some(ref envelope) = self.envelope { + used_backends.extend(envelope.get_used_backends()); + } + + if let Some(ref flag) = self.flag { + used_backends.extend(flag.get_used_backends()); + } + + if let Some(ref msg) = self.message { + used_backends.extend(msg.get_used_backends()); + } + + used_backends + } } diff --git a/src/domain/email/envelope/config.rs b/src/domain/email/envelope/config.rs index 35a5629..fa7f5d8 100644 --- a/src/domain/email/envelope/config.rs +++ b/src/domain/email/envelope/config.rs @@ -1,4 +1,5 @@ -use ::serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; use crate::backend::BackendKind; @@ -8,12 +9,52 @@ pub struct EnvelopeConfig { pub get: Option, } +impl EnvelopeConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(list) = &self.list { + kinds.extend(list.get_used_backends()); + } + + if let Some(get) = &self.get { + kinds.extend(get.get_used_backends()); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct EnvelopeListConfig { pub backend: Option, } +impl EnvelopeListConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct EnvelopeGetConfig { pub backend: Option, } + +impl EnvelopeGetConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} diff --git a/src/domain/email/envelope/flag/config.rs b/src/domain/email/envelope/flag/config.rs index af01500..eb75af4 100644 --- a/src/domain/email/envelope/flag/config.rs +++ b/src/domain/email/envelope/flag/config.rs @@ -1,4 +1,5 @@ -use ::serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; use crate::backend::BackendKind; @@ -9,17 +10,73 @@ pub struct FlagConfig { pub remove: Option, } +impl FlagConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(add) = &self.add { + kinds.extend(add.get_used_backends()); + } + + if let Some(set) = &self.set { + kinds.extend(set.get_used_backends()); + } + + if let Some(remove) = &self.remove { + kinds.extend(remove.get_used_backends()); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct FlagAddConfig { pub backend: Option, } +impl FlagAddConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct FlagSetConfig { pub backend: Option, } +impl FlagSetConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct FlagRemoveConfig { pub backend: Option, } + +impl FlagRemoveConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} diff --git a/src/domain/email/message/config.rs b/src/domain/email/message/config.rs index abb2072..0ab96a4 100644 --- a/src/domain/email/message/config.rs +++ b/src/domain/email/message/config.rs @@ -1,4 +1,5 @@ -use ::serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; use crate::backend::BackendKind; @@ -13,32 +14,136 @@ pub struct MessageConfig { pub move_: Option, } +impl MessageConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(add) = &self.add { + kinds.extend(add.get_used_backends()); + } + + if let Some(send) = &self.send { + kinds.extend(send.get_used_backends()); + } + + if let Some(peek) = &self.peek { + kinds.extend(peek.get_used_backends()); + } + + if let Some(get) = &self.get { + kinds.extend(get.get_used_backends()); + } + + if let Some(copy) = &self.copy { + kinds.extend(copy.get_used_backends()); + } + + if let Some(move_) = &self.move_ { + kinds.extend(move_.get_used_backends()); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct MessageAddConfig { pub backend: Option, } +impl MessageAddConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct MessageSendConfig { pub backend: Option, } +impl MessageSendConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct MessagePeekConfig { pub backend: Option, } +impl MessagePeekConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct MessageGetConfig { pub backend: Option, } +impl MessageGetConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct MessageCopyConfig { pub backend: Option, } +impl MessageCopyConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct MessageMoveConfig { pub backend: Option, } + +impl MessageMoveConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} diff --git a/src/domain/folder/config.rs b/src/domain/folder/config.rs index 105db57..16250f9 100644 --- a/src/domain/folder/config.rs +++ b/src/domain/folder/config.rs @@ -1,4 +1,5 @@ -use ::serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; use crate::backend::BackendKind; @@ -11,27 +12,115 @@ pub struct FolderConfig { pub delete: Option, } +impl FolderConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(add) = &self.add { + kinds.extend(add.get_used_backends()); + } + + if let Some(list) = &self.list { + kinds.extend(list.get_used_backends()); + } + + if let Some(expunge) = &self.expunge { + kinds.extend(expunge.get_used_backends()); + } + + if let Some(purge) = &self.purge { + kinds.extend(purge.get_used_backends()); + } + + if let Some(delete) = &self.delete { + kinds.extend(delete.get_used_backends()); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct FolderAddConfig { pub backend: Option, } +impl FolderAddConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct FolderListConfig { pub backend: Option, } +impl FolderListConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct FolderExpungeConfig { pub backend: Option, } +impl FolderExpungeConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct FolderPurgeConfig { pub backend: Option, } +impl FolderPurgeConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct FolderDeleteConfig { pub backend: Option, } + +impl FolderDeleteConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} diff --git a/src/main.rs b/src/main.rs index 2cd9433..18213e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use url::Url; use himalaya::imap; use himalaya::{ account, - backend::BackendBuilder, + backend::{Backend, BackendBuilder}, cache, compl, config::{self, TomlConfig}, email, flag, folder, man, output, @@ -61,7 +61,7 @@ async fn main() -> Result<()> { .await? .into_account_configs(None, false)?; let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone()).await?; + BackendBuilder::new(toml_account_config, account_config.clone(), true).await?; let backend = backend_builder.build().await?; let mut printer = StdoutPrinter::default(); @@ -130,7 +130,7 @@ async fn main() -> Result<()> { .clone() .into_account_configs(maybe_account_name, true)?; let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone()).await?; + BackendBuilder::new(toml_account_config, account_config.clone(), false).await?; let sync_builder = AccountSyncBuilder::new(backend_builder.into()) .await? .with_some_folders_strategy(strategy) @@ -149,28 +149,29 @@ async fn main() -> Result<()> { let (toml_account_config, account_config) = toml_config .clone() .into_account_configs(maybe_account_name, disable_cache)?; - println!("toml_account_config: {:#?}", toml_account_config); - let backend_builder = BackendBuilder::new(toml_account_config, account_config.clone()).await?; - let backend = backend_builder.build().await?; // checks folder commands match folder::args::matches(&m)? { Some(folder::args::Cmd::Create) => { + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; let folder = folder .ok_or_else(|| anyhow!("the folder argument is missing")) .context("cannot create folder")?; return folder::handlers::create(&mut printer, &backend, &folder).await; } Some(folder::args::Cmd::List(max_width)) => { + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; return folder::handlers::list(&account_config, &mut printer, &backend, max_width) .await; } Some(folder::args::Cmd::Expunge) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; return folder::handlers::expunge(&mut printer, &backend, &folder).await; } Some(folder::args::Cmd::Delete) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; return folder::handlers::delete(&mut printer, &backend, &folder).await; } _ => (), @@ -180,6 +181,7 @@ async fn main() -> Result<()> { match email::args::matches(&m)? { Some(email::args::Cmd::Attachments(ids)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; return email::handlers::attachments( &account_config, &mut printer, @@ -191,14 +193,17 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Copy(ids, to_folder)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; return email::handlers::copy(&mut printer, &backend, &folder, to_folder, ids).await; } Some(email::args::Cmd::Delete(ids)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; return email::handlers::delete(&mut printer, &backend, &folder, ids).await; } Some(email::args::Cmd::Forward(id, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; return email::handlers::forward( &account_config, &mut printer, @@ -212,6 +217,7 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::List(max_width, page_size, page)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; return email::handlers::list( &account_config, &mut printer, @@ -225,10 +231,12 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Move(ids, to_folder)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; return email::handlers::move_(&mut printer, &backend, &folder, to_folder, ids).await; } Some(email::args::Cmd::Read(ids, text_mime, raw, headers)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; return email::handlers::read( &account_config, &mut printer, @@ -243,6 +251,7 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Reply(id, all, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; return email::handlers::reply( &account_config, &mut printer, @@ -257,10 +266,12 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Save(raw_email)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; return email::handlers::save(&mut printer, &backend, &folder, raw_email).await; } Some(email::args::Cmd::Search(query, max_width, page_size, page)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; return email::handlers::search( &account_config, &mut printer, @@ -275,6 +286,7 @@ async fn main() -> Result<()> { } Some(email::args::Cmd::Sort(criteria, query, max_width, page_size, page)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; return email::handlers::sort( &account_config, &mut printer, @@ -289,19 +301,26 @@ async fn main() -> Result<()> { .await; } Some(email::args::Cmd::Send(raw_email)) => { + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; return email::handlers::send(&account_config, &mut printer, &backend, raw_email).await; } Some(email::args::Cmd::Flag(m)) => match m { Some(flag::args::Cmd::Set(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = + Backend::new(toml_account_config, account_config.clone(), false).await?; return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await; } Some(flag::args::Cmd::Add(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = + Backend::new(toml_account_config, account_config.clone(), false).await?; return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await; } Some(flag::args::Cmd::Remove(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = + Backend::new(toml_account_config, account_config.clone(), false).await?; return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await; } _ => (), @@ -309,6 +328,8 @@ async fn main() -> Result<()> { Some(email::args::Cmd::Tpl(m)) => match m { Some(tpl::args::Cmd::Forward(id, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = + Backend::new(toml_account_config, account_config.clone(), false).await?; return tpl::handlers::forward( &account_config, &mut printer, @@ -321,11 +342,12 @@ async fn main() -> Result<()> { .await; } Some(tpl::args::Cmd::Write(headers, body)) => { - tpl::handlers::write(&account_config, &mut printer, headers, body).await?; - return Ok(()); + return tpl::handlers::write(&account_config, &mut printer, headers, body).await; } Some(tpl::args::Cmd::Reply(id, all, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = + Backend::new(toml_account_config, account_config.clone(), false).await?; return tpl::handlers::reply( &account_config, &mut printer, @@ -340,15 +362,20 @@ async fn main() -> Result<()> { } Some(tpl::args::Cmd::Save(tpl)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = + Backend::new(toml_account_config, account_config.clone(), false).await?; return tpl::handlers::save(&account_config, &mut printer, &backend, &folder, tpl) .await; } Some(tpl::args::Cmd::Send(tpl)) => { + let backend = + Backend::new(toml_account_config, account_config.clone(), true).await?; return tpl::handlers::send(&account_config, &mut printer, &backend, tpl).await; } _ => (), }, Some(email::args::Cmd::Write(headers, body)) => { + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; return email::handlers::write(&account_config, &mut printer, &backend, headers, body) .await; } From f24a0475cc1c3d7fda2412375ecba8f02b7c3f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 3 Dec 2023 13:03:36 +0100 Subject: [PATCH 10/29] fix imap credentials and pgp --- Cargo.lock | 21 ++------------------- Cargo.toml | 8 +++++--- LICENSE | 2 +- src/backend.rs | 22 +++++++++++++++------- src/config/prelude.rs | 27 +++++++++++++++++++++++++-- src/domain/tpl/handlers.rs | 10 ++++++---- src/ui/editor.rs | 10 ++++++---- 7 files changed, 60 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3bfaa3..2dba8c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1001,7 +1001,7 @@ dependencies = [ "mail-send", "maildirpp", "md5", - "mml-lib 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mml-lib", "notmuch", "oauth-lib 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "once_cell", @@ -2069,7 +2069,7 @@ dependencies = [ "keyring-lib", "log", "md5", - "mml-lib 1.0.1", + "mml-lib", "oauth-lib 0.1.0", "once_cell", "process-lib", @@ -2687,23 +2687,6 @@ dependencies = [ [[package]] name = "mml-lib" version = "1.0.1" -dependencies = [ - "async-recursion", - "chumsky", - "log", - "mail-builder", - "mail-parser", - "nanohtml2text", - "shellexpand-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror", - "tree_magic_mini", -] - -[[package]] -name = "mml-lib" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915efb96feb76c123001341d1631dc06c9bbe14c1a249ef709cf84b341eb8295" dependencies = [ "async-recursion", "chumsky", diff --git a/Cargo.toml b/Cargo.toml index e8f25e9..2f7eea1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,9 +25,9 @@ imap-backend = ["email-lib/imap-backend"] notmuch-backend = ["email-lib/notmuch-backend"] smtp-sender = ["email-lib/smtp-sender"] pgp = [] -pgp-commands = ["pgp", "email-lib/pgp-commands"] -pgp-gpg = ["pgp", "email-lib/pgp-gpg"] -pgp-native = ["pgp", "email-lib/pgp-native"] +pgp-commands = ["pgp", "mml-lib/pgp-commands", "email-lib/pgp-commands"] +pgp-gpg = ["pgp", "mml-lib/pgp-gpg", "email-lib/pgp-gpg"] +pgp-native = ["pgp", "mml-lib/pgp-native", "email-lib/pgp-native"] # dev dependencies @@ -107,6 +107,8 @@ version = "=0.1.0" [dependencies.mml-lib] # version = "=1.0.1" +default-features = false +features = ["compiler", "interpreter"] path = "/home/soywod/sourcehut/pimalaya/mml" [dependencies.secret-lib] diff --git a/LICENSE b/LICENSE index 113587b..90fc14c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 soywod +Copyright (c) 2022-2023 soywod Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/backend.rs b/src/backend.rs index b907263..e99f5bb 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -162,13 +162,21 @@ impl BackendBuilder { .map(|mdir_config| MaildirSessionBuilder::new(account_config.clone(), mdir_config)), #[cfg(feature = "imap-backend")] - imap: toml_account_config - .imap - .as_ref() - .filter(|_| is_imap_used) - .map(|imap_config| { - ImapSessionBuilder::new(account_config.clone(), imap_config.clone()) - }), + imap: { + let ctx_builder = toml_account_config + .imap + .as_ref() + .filter(|_| is_imap_used) + .map(|imap_config| { + ImapSessionBuilder::new(account_config.clone(), imap_config.clone()) + .with_prebuilt_credentials() + }); + + match ctx_builder { + Some(ctx_builder) => Some(ctx_builder.await?), + None => None, + } + }, #[cfg(feature = "notmuch-backend")] notmuch: toml_account_config .notmuch diff --git a/src/config/prelude.rs b/src/config/prelude.rs index 0b24daa..f61982b 100644 --- a/src/config/prelude.rs +++ b/src/config/prelude.rs @@ -537,10 +537,33 @@ pub enum FolderSyncStrategyDef { #[cfg(feature = "pgp")] #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(remote = "Option", from = "OptionPgpConfig")] +pub struct OptionPgpConfigDef; + +#[cfg(feature = "pgp")] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct OptionPgpConfig { + #[serde(default, skip)] + is_none: bool, + #[serde(flatten, with = "PgpConfigDef")] + inner: PgpConfig, +} + +#[cfg(feature = "pgp")] +impl From for Option { + fn from(config: OptionPgpConfig) -> Option { + if config.is_none { + None + } else { + Some(config.inner) + } + } +} + +#[cfg(feature = "pgp")] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(remote = "PgpConfig", tag = "backend", rename_all = "kebab-case")] pub enum PgpConfigDef { - #[default] - None, #[cfg(feature = "pgp-commands")] #[serde(with = "CmdsPgpConfigDef", alias = "commands")] Cmds(CmdsPgpConfig), diff --git a/src/domain/tpl/handlers.rs b/src/domain/tpl/handlers.rs index f59b2f7..b036370 100644 --- a/src/domain/tpl/handlers.rs +++ b/src/domain/tpl/handlers.rs @@ -77,10 +77,11 @@ pub async fn save( .join("\n") }; - let compiler = MmlCompilerBuilder::new(); + #[allow(unused_mut)] + let mut compiler = MmlCompilerBuilder::new(); #[cfg(feature = "pgp")] - let compiler = compiler.with_pgp(config.pgp.clone()); + compiler.set_some_pgp(config.pgp.clone()); let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; @@ -108,10 +109,11 @@ pub async fn send( .join("\n") }; - let compiler = MmlCompilerBuilder::new(); + #[allow(unused_mut)] + let mut compiler = MmlCompilerBuilder::new(); #[cfg(feature = "pgp")] - let compiler = compiler.with_pgp(config.pgp.clone()); + compiler.set_some_pgp(config.pgp.clone()); let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; diff --git a/src/ui/editor.rs b/src/ui/editor.rs index 34eb59d..b65f461 100644 --- a/src/ui/editor.rs +++ b/src/ui/editor.rs @@ -77,10 +77,11 @@ pub async fn edit_tpl_with_editor( Ok(PostEditChoice::Send) => { printer.print_log("Sending email…")?; - let compiler = MmlCompilerBuilder::new(); + #[allow(unused_mut)] + let mut compiler = MmlCompilerBuilder::new(); #[cfg(feature = "pgp")] - let compiler = compiler.with_pgp(config.pgp.clone()); + compiler.set_some_pgp(config.pgp.clone()); let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; @@ -107,10 +108,11 @@ pub async fn edit_tpl_with_editor( break; } Ok(PostEditChoice::RemoteDraft) => { - let compiler = MmlCompilerBuilder::new(); + #[allow(unused_mut)] + let mut compiler = MmlCompilerBuilder::new(); #[cfg(feature = "pgp")] - let compiler = compiler.with_pgp(config.pgp.clone()); + compiler.set_some_pgp(config.pgp.clone()); let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; From c54ada730b0c01b6b8da77e7ed550827f296502e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 3 Dec 2023 22:31:43 +0100 Subject: [PATCH 11/29] fix wizard --- src/backend/config.rs | 19 +++ src/{backend.rs => backend/mod.rs} | 25 +++- src/backend/wizard.rs | 71 +++++++++ src/config/config.rs | 135 ++++++++++++------ src/config/wizard.rs | 5 +- src/domain/account/wizard.rs | 53 ++++++- src/domain/backend/imap/mod.rs | 3 - src/domain/backend/maildir/mod.rs | 1 - src/domain/backend/mod.rs | 6 - src/domain/backend/wizard.rs | 44 ------ src/domain/mod.rs | 3 - src/domain/sender/mod.rs | 4 - src/domain/sender/sendmail/mod.rs | 1 - src/domain/sender/smtp/mod.rs | 1 - src/domain/sender/wizard.rs | 35 ----- src/{domain/backend => }/imap/args.rs | 0 src/{domain/backend => }/imap/handlers.rs | 0 src/imap/mod.rs | 3 + src/{domain/backend => }/imap/wizard.rs | 3 +- src/lib.rs | 8 ++ .../backend/notmuch => maildir}/mod.rs | 0 src/{domain/backend => }/maildir/wizard.rs | 4 +- src/main.rs | 27 ++-- src/notmuch/mod.rs | 1 + src/{domain/backend => }/notmuch/wizard.rs | 0 src/sendmail/mod.rs | 1 + src/{domain/sender => }/sendmail/wizard.rs | 8 +- src/smtp/mod.rs | 1 + src/{domain/sender => }/smtp/wizard.rs | 7 +- 29 files changed, 292 insertions(+), 177 deletions(-) create mode 100644 src/backend/config.rs rename src/{backend.rs => backend/mod.rs} (97%) create mode 100644 src/backend/wizard.rs delete mode 100644 src/domain/backend/imap/mod.rs delete mode 100644 src/domain/backend/maildir/mod.rs delete mode 100644 src/domain/backend/mod.rs delete mode 100644 src/domain/backend/wizard.rs delete mode 100644 src/domain/sender/mod.rs delete mode 100644 src/domain/sender/sendmail/mod.rs delete mode 100644 src/domain/sender/smtp/mod.rs delete mode 100644 src/domain/sender/wizard.rs rename src/{domain/backend => }/imap/args.rs (100%) rename src/{domain/backend => }/imap/handlers.rs (100%) create mode 100644 src/imap/mod.rs rename src/{domain/backend => }/imap/wizard.rs (99%) rename src/{domain/backend/notmuch => maildir}/mod.rs (100%) rename src/{domain/backend => }/maildir/wizard.rs (82%) create mode 100644 src/notmuch/mod.rs rename src/{domain/backend => }/notmuch/wizard.rs (100%) create mode 100644 src/sendmail/mod.rs rename src/{domain/sender => }/sendmail/wizard.rs (60%) create mode 100644 src/smtp/mod.rs rename src/{domain/sender => }/smtp/wizard.rs (98%) diff --git a/src/backend/config.rs b/src/backend/config.rs new file mode 100644 index 0000000..fb18479 --- /dev/null +++ b/src/backend/config.rs @@ -0,0 +1,19 @@ +#[cfg(feature = "imap-backend")] +use email::imap::ImapConfig; +#[cfg(feature = "notmuch-backend")] +use email::notmuch::NotmuchConfig; +#[cfg(feature = "smtp-sender")] +use email::smtp::SmtpConfig; +use email::{maildir::MaildirConfig, sendmail::SendmailConfig}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum BackendConfig { + Maildir(MaildirConfig), + #[cfg(feature = "imap-backend")] + Imap(ImapConfig), + #[cfg(feature = "notmuch-backend")] + Notmuch(NotmuchConfig), + #[cfg(feature = "smtp-sender")] + Smtp(SmtpConfig), + Sendmail(SendmailConfig), +} diff --git a/src/backend.rs b/src/backend/mod.rs similarity index 97% rename from src/backend.rs rename to src/backend/mod.rs index e99f5bb..c92ddec 100644 --- a/src/backend.rs +++ b/src/backend/mod.rs @@ -1,3 +1,6 @@ +pub mod config; +pub(crate) mod wizard; + use anyhow::Result; use async_trait::async_trait; use std::ops::Deref; @@ -47,11 +50,9 @@ use serde::{Deserialize, Serialize}; use crate::{account::TomlAccountConfig, Envelopes, IdMapper}; -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum BackendKind { - #[default] - None, Maildir, #[serde(skip_deserializing)] MaildirForSync, @@ -64,6 +65,24 @@ pub enum BackendKind { Sendmail, } +impl ToString for BackendKind { + fn to_string(&self) -> String { + let kind = match self { + Self::Maildir => "Maildir", + Self::MaildirForSync => "Maildir", + #[cfg(feature = "imap-backend")] + Self::Imap => "IMAP", + #[cfg(feature = "notmuch-backend")] + Self::Notmuch => "Notmuch", + #[cfg(feature = "smtp-sender")] + Self::Smtp => "SMTP", + Self::Sendmail => "Sendmail", + }; + + kind.to_string() + } +} + #[derive(Clone, Default)] pub struct BackendContextBuilder { maildir: Option, diff --git a/src/backend/wizard.rs b/src/backend/wizard.rs new file mode 100644 index 0000000..6d9ddca --- /dev/null +++ b/src/backend/wizard.rs @@ -0,0 +1,71 @@ +use anyhow::Result; +use dialoguer::Select; + +#[cfg(feature = "imap-backend")] +use crate::imap; +#[cfg(feature = "notmuch-backend")] +use crate::notmuch; +#[cfg(feature = "smtp-sender")] +use crate::smtp; +use crate::{config::wizard::THEME, maildir, sendmail}; + +use super::{config::BackendConfig, BackendKind}; + +const DEFAULT_BACKEND_KINDS: &[BackendKind] = &[ + BackendKind::Maildir, + #[cfg(feature = "imap-backend")] + BackendKind::Imap, + #[cfg(feature = "notmuch-backend")] + BackendKind::Notmuch, +]; + +const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[ + BackendKind::Sendmail, + #[cfg(feature = "smtp-sender")] + BackendKind::Smtp, +]; + +pub(crate) async fn configure(account_name: &str, email: &str) -> Result> { + let kind = Select::with_theme(&*THEME) + .with_prompt("Default email backend") + .items(DEFAULT_BACKEND_KINDS) + .default(0) + .interact_opt()? + .and_then(|idx| DEFAULT_BACKEND_KINDS.get(idx).map(Clone::clone)); + + let config = match kind { + Some(kind) if kind == BackendKind::Maildir => Some(maildir::wizard::configure()?), + #[cfg(feature = "imap-backend")] + Some(kind) if kind == BackendKind::Imap => { + Some(imap::wizard::configure(account_name, email).await?) + } + #[cfg(feature = "notmuch-backend")] + Some(kind) if kind == BackendKind::Notmuch => Some(notmuch::wizard::configure()?), + _ => None, + }; + + Ok(config) +} + +pub(crate) async fn configure_sender( + account_name: &str, + email: &str, +) -> Result> { + let kind = Select::with_theme(&*THEME) + .with_prompt("Default email backend") + .items(SEND_MESSAGE_BACKEND_KINDS) + .default(0) + .interact_opt()? + .and_then(|idx| SEND_MESSAGE_BACKEND_KINDS.get(idx).map(Clone::clone)); + + let config = match kind { + Some(kind) if kind == BackendKind::Sendmail => Some(sendmail::wizard::configure()?), + #[cfg(feature = "smtp-sender")] + Some(kind) if kind == BackendKind::Smtp => { + Some(smtp::wizard::configure(account_name, email).await?) + } + _ => None, + }; + + Ok(config) +} diff --git a/src/config/config.rs b/src/config/config.rs index 9c618d7..574dad1 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -11,9 +11,13 @@ use email::{ config::Config, email::{EmailHooks, EmailTextPlainFormat}, }; -use log::{debug, trace}; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fs, path::PathBuf, process::exit}; +use std::{ + collections::HashMap, + fs, + path::{Path, PathBuf}, + process, +}; use toml; use crate::{ @@ -52,51 +56,95 @@ pub struct TomlConfig { } impl TomlConfig { - /// Tries to create a config from an optional path. - pub async fn from_maybe_path(path: Option<&str>) -> Result { - debug!("path: {:?}", path); - - let config = if let Some(path) = path.map(PathBuf::from).or_else(Self::path) { - let content = fs::read_to_string(path).context("cannot read config file")?; - toml::from_str(&content).context("cannot parse config file")? - } else { - wizard_warn!("Himalaya could not find an already existing configuration file."); - - if !Confirm::new() - .with_prompt(wizard_prompt!( - "Would you like to create one with the wizard?" - )) - .default(true) - .interact_opt()? - .unwrap_or_default() - { - exit(0); - } - - wizard::configure().await? - }; - - if config.accounts.is_empty() { - return Err(anyhow!("config file must contain at least one account")); - } - - trace!("config: {:#?}", config); - Ok(config) + /// Read and parse the TOML configuration at the given path. + /// + /// Returns an error if the configuration file cannot be read or + /// if its content cannot be parsed. + fn from_path(path: &Path) -> Result { + let content = + fs::read_to_string(path).context(format!("cannot read config file at {path:?}"))?; + toml::from_str(&content).context(format!("cannot parse config file at {path:?}")) } - /// Tries to return a config path from a few default settings. + /// Create and save a TOML configuration using the wizard. + /// + /// If the user accepts the confirmation, the wizard starts and + /// help him to create his configuration file. Otherwise the + /// program stops. + /// + /// NOTE: the wizard can only be used with interactive shells. + async fn from_wizard(path: PathBuf) -> Result { + wizard_warn!("Cannot find existing configuration at {path:?}."); + + let confirm = Confirm::new() + .with_prompt(wizard_prompt!( + "Would you like to create it with the wizard?" + )) + .default(true) + .interact_opt()? + .unwrap_or_default(); + + if !confirm { + process::exit(0); + } + + wizard::configure().await + } + + /// Read and parse the TOML configuration from default paths. + pub async fn from_default_paths() -> Result { + match Self::first_valid_default_path() { + Some(path) => Self::from_path(&path), + None => Self::from_wizard(Self::default_path()?).await, + } + } + + /// Read and parse the TOML configuration at the optional given + /// path. + /// + /// If the given path exists, then read and parse the TOML + /// configuration from it. + /// + /// If the given path does not exist, then create it using the + /// wizard. + /// + /// If no path is given, then either read and parse the TOML + /// configuration at the first valid default path, otherwise + /// create it using the wizard. wizard. + pub async fn from_some_path_or_default(path: Option>) -> Result { + match path.map(Into::into) { + Some(ref path) if path.exists() => Self::from_path(path), + Some(path) => Self::from_wizard(path).await, + None => match Self::first_valid_default_path() { + Some(path) => Self::from_path(&path), + None => Self::from_wizard(Self::default_path()?).await, + }, + } + } + + /// Get the default configuration path. + /// + /// Returns an error if the XDG configuration directory cannot be + /// found. + pub fn default_path() -> Result { + Ok(config_dir() + .ok_or(anyhow!("cannot get XDG config directory"))? + .join("himalaya") + .join("config.toml")) + } + + /// Get the first default configuration path that points to a + /// valid file. /// /// Tries paths in this order: /// - /// - `"$XDG_CONFIG_DIR/himalaya/config.toml"` (or equivalent to `$XDG_CONFIG_DIR` in other - /// OSes.) - /// - `"$HOME/.config/himalaya/config.toml"` - /// - `"$HOME/.himalayarc"` - /// - /// Returns `Some(path)` if the path exists, otherwise `None`. - pub fn path() -> Option { - config_dir() - .map(|p| p.join("himalaya").join("config.toml")) + /// - `$XDG_CONFIG_DIR/himalaya/config.toml` (or equivalent to + /// `$XDG_CONFIG_DIR` in other OSes.) + /// - `$HOME/.config/himalaya/config.toml` + /// - `$HOME/.himalayarc` + pub fn first_valid_default_path() -> Option { + Self::default_path() + .ok() .filter(|p| p.exists()) .or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml"))) .filter(|p| p.exists()) @@ -104,6 +152,7 @@ impl TomlConfig { .filter(|p| p.exists()) } + /// Build account configurations from a given account name. pub fn into_account_configs( self, account_name: Option<&str>, @@ -232,7 +281,7 @@ mod tests { async fn make_config(config: &str) -> Result { let mut file = NamedTempFile::new().unwrap(); write!(file, "{}", config).unwrap(); - TomlConfig::from_maybe_path(file.into_temp_path().to_str()).await + TomlConfig::from_some_path_or_default(file.into_temp_path().to_str()).await } #[tokio::test] diff --git a/src/config/wizard.rs b/src/config/wizard.rs index f6add5e..f6b359e 100644 --- a/src/config/wizard.rs +++ b/src/config/wizard.rs @@ -57,7 +57,10 @@ pub(crate) async fn configure() -> Result { // accounts are setup, decide which will be the default. If no // accounts are setup, exit the process. let default_account = match config.accounts.len() { - 0 => process::exit(0), + 0 => { + wizard_warn!("No account configured, exiting."); + process::exit(0); + } 1 => Some(config.accounts.values_mut().next().unwrap()), _ => { let accounts = config.accounts.clone(); diff --git a/src/domain/account/wizard.rs b/src/domain/account/wizard.rs index 84ff44e..5096585 100644 --- a/src/domain/account/wizard.rs +++ b/src/domain/account/wizard.rs @@ -1,8 +1,12 @@ -use anyhow::{anyhow, Result}; +use anyhow::{bail, Result}; use dialoguer::Input; use email_address::EmailAddress; -use crate::config::wizard::THEME; +use crate::{ + backend::{self, config::BackendConfig, BackendKind}, + config::wizard::THEME, + message::config::{MessageConfig, MessageSendConfig}, +}; use super::TomlAccountConfig; @@ -20,7 +24,7 @@ pub(crate) async fn configure() -> Result> { if EmailAddress::is_valid(email) { Ok(()) } else { - Err(anyhow!("Invalid email address: {email}")) + bail!("Invalid email address: {email}") } }) .interact()?; @@ -31,9 +35,48 @@ pub(crate) async fn configure() -> Result> { .interact()?, ); - // config.backend = backend::wizard::configure(&account_name, &config.email).await?; + match backend::wizard::configure(&account_name, &config.email).await? { + Some(BackendConfig::Maildir(mdir_config)) => { + config.maildir = Some(mdir_config); + config.backend = Some(BackendKind::Maildir); + } + #[cfg(feature = "imap-backend")] + Some(BackendConfig::Imap(imap_config)) => { + config.imap = Some(imap_config); + config.backend = Some(BackendKind::Imap); + } + #[cfg(feature = "notmuch-backend")] + Some(BackendConfig::Notmuch(notmuch_config)) => { + config.notmuch = Some(notmuch_config); + config.backend = Some(BackendKind::Notmuch); + } + _ => (), + }; - // config.sender = sender::wizard::configure(&account_name, &config.email).await?; + match backend::wizard::configure_sender(&account_name, &config.email).await? { + Some(BackendConfig::Sendmail(sendmail_config)) => { + config.sendmail = Some(sendmail_config); + config.message = Some(MessageConfig { + send: Some(MessageSendConfig { + backend: Some(BackendKind::Sendmail), + ..Default::default() + }), + ..Default::default() + }); + } + #[cfg(feature = "smtp-sender")] + Some(BackendConfig::Smtp(smtp_config)) => { + config.smtp = Some(smtp_config); + config.message = Some(MessageConfig { + send: Some(MessageSendConfig { + backend: Some(BackendKind::Smtp), + ..Default::default() + }), + ..Default::default() + }); + } + _ => (), + }; Ok(Some((account_name, config))) } diff --git a/src/domain/backend/imap/mod.rs b/src/domain/backend/imap/mod.rs deleted file mode 100644 index b6c153f..0000000 --- a/src/domain/backend/imap/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod args; -// pub mod handlers; -// pub(crate) mod wizard; diff --git a/src/domain/backend/maildir/mod.rs b/src/domain/backend/maildir/mod.rs deleted file mode 100644 index d4b1a6e..0000000 --- a/src/domain/backend/maildir/mod.rs +++ /dev/null @@ -1 +0,0 @@ -// pub(crate) mod wizard; diff --git a/src/domain/backend/mod.rs b/src/domain/backend/mod.rs deleted file mode 100644 index b33f50e..0000000 --- a/src/domain/backend/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[cfg(feature = "imap-backend")] -pub mod imap; -pub mod maildir; -#[cfg(feature = "notmuch-backend")] -pub mod notmuch; -// pub(crate) mod wizard; diff --git a/src/domain/backend/wizard.rs b/src/domain/backend/wizard.rs deleted file mode 100644 index 47e84fe..0000000 --- a/src/domain/backend/wizard.rs +++ /dev/null @@ -1,44 +0,0 @@ -use anyhow::Result; -use dialoguer::Select; -use email::backend::BackendConfig; - -use crate::config::wizard::THEME; - -#[cfg(feature = "imap-backend")] -use super::imap; -use super::maildir; -#[cfg(feature = "notmuch-backend")] -use super::notmuch; - -#[cfg(feature = "imap-backend")] -const IMAP: &str = "IMAP"; -const MAILDIR: &str = "Maildir"; -#[cfg(feature = "notmuch-backend")] -const NOTMUCH: &str = "Notmuch"; -const NONE: &str = "None"; - -const BACKENDS: &[&str] = &[ - #[cfg(feature = "imap-backend")] - IMAP, - MAILDIR, - #[cfg(feature = "notmuch-backend")] - NOTMUCH, - NONE, -]; - -pub(crate) async fn configure(account_name: &str, email: &str) -> Result { - let backend = Select::with_theme(&*THEME) - .with_prompt("Email backend") - .items(BACKENDS) - .default(0) - .interact_opt()?; - - match backend { - #[cfg(feature = "imap-backend")] - Some(idx) if BACKENDS[idx] == IMAP => imap::wizard::configure(account_name, email).await, - Some(idx) if BACKENDS[idx] == MAILDIR => maildir::wizard::configure(), - #[cfg(feature = "notmuch-backend")] - Some(idx) if BACKENDS[idx] == NOTMUCH => notmuch::wizard::configure(), - _ => Ok(BackendConfig::None), - } -} diff --git a/src/domain/mod.rs b/src/domain/mod.rs index d9b40c6..50ca65a 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -1,14 +1,11 @@ pub mod account; -pub mod backend; pub mod email; pub mod envelope; pub mod flag; pub mod folder; -pub mod sender; pub mod tpl; pub use self::account::{args, handlers, Account, Accounts}; -pub use self::backend::*; pub use self::email::*; pub use self::envelope::*; pub use self::flag::*; diff --git a/src/domain/sender/mod.rs b/src/domain/sender/mod.rs deleted file mode 100644 index eab09f5..0000000 --- a/src/domain/sender/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod sendmail; -#[cfg(feature = "smtp-sender")] -pub mod smtp; -// pub(crate) mod wizard; diff --git a/src/domain/sender/sendmail/mod.rs b/src/domain/sender/sendmail/mod.rs deleted file mode 100644 index d4b1a6e..0000000 --- a/src/domain/sender/sendmail/mod.rs +++ /dev/null @@ -1 +0,0 @@ -// pub(crate) mod wizard; diff --git a/src/domain/sender/smtp/mod.rs b/src/domain/sender/smtp/mod.rs deleted file mode 100644 index d4b1a6e..0000000 --- a/src/domain/sender/smtp/mod.rs +++ /dev/null @@ -1 +0,0 @@ -// pub(crate) mod wizard; diff --git a/src/domain/sender/wizard.rs b/src/domain/sender/wizard.rs deleted file mode 100644 index bb4b9d6..0000000 --- a/src/domain/sender/wizard.rs +++ /dev/null @@ -1,35 +0,0 @@ -use anyhow::Result; -use dialoguer::Select; - -use crate::config::wizard::THEME; - -use super::sendmail; -#[cfg(feature = "smtp-sender")] -use super::smtp; - -#[cfg(feature = "smtp-sender")] -const SMTP: &str = "SMTP"; -const SENDMAIL: &str = "Sendmail"; -const NONE: &str = "None"; - -const SENDERS: &[&str] = &[ - #[cfg(feature = "smtp-sender")] - SMTP, - SENDMAIL, - NONE, -]; - -pub(crate) async fn configure(account_name: &str, email: &str) -> Result { - let sender = Select::with_theme(&*THEME) - .with_prompt("Email sender") - .items(SENDERS) - .default(0) - .interact_opt()?; - - match sender { - #[cfg(feature = "smtp-sender")] - Some(n) if SENDERS[n] == SMTP => smtp::wizard::configure(account_name, email).await, - Some(n) if SENDERS[n] == SENDMAIL => sendmail::wizard::configure(), - _ => Ok(SenderConfig::None), - } -} diff --git a/src/domain/backend/imap/args.rs b/src/imap/args.rs similarity index 100% rename from src/domain/backend/imap/args.rs rename to src/imap/args.rs diff --git a/src/domain/backend/imap/handlers.rs b/src/imap/handlers.rs similarity index 100% rename from src/domain/backend/imap/handlers.rs rename to src/imap/handlers.rs diff --git a/src/imap/mod.rs b/src/imap/mod.rs new file mode 100644 index 0000000..7a553c7 --- /dev/null +++ b/src/imap/mod.rs @@ -0,0 +1,3 @@ +// pub mod args; +// pub mod handlers; +pub(crate) mod wizard; diff --git a/src/domain/backend/imap/wizard.rs b/src/imap/wizard.rs similarity index 99% rename from src/domain/backend/imap/wizard.rs rename to src/imap/wizard.rs index 1322517..7ea750e 100644 --- a/src/domain/backend/imap/wizard.rs +++ b/src/imap/wizard.rs @@ -2,12 +2,13 @@ use anyhow::Result; use dialoguer::{Confirm, Input, Password, Select}; use email::{ account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig}, - backend::{BackendConfig, ImapAuthConfig, ImapConfig}, + imap::{ImapAuthConfig, ImapConfig}, }; use oauth::v2_0::{AuthorizationCodeGrant, Client}; use secret::Secret; use crate::{ + backend::config::BackendConfig, config::wizard::{prompt_passwd, THEME}, wizard_log, wizard_prompt, }; diff --git a/src/lib.rs b/src/lib.rs index d1b00f6..f37795c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,9 +3,17 @@ pub mod cache; pub mod compl; pub mod config; pub mod domain; +#[cfg(feature = "imap-backend")] +pub mod imap; +pub mod maildir; pub mod man; +#[cfg(feature = "notmuch-backend")] +pub mod notmuch; pub mod output; pub mod printer; +pub mod sendmail; +#[cfg(feature = "smtp-sender")] +pub mod smtp; pub mod ui; pub use cache::IdMapper; diff --git a/src/domain/backend/notmuch/mod.rs b/src/maildir/mod.rs similarity index 100% rename from src/domain/backend/notmuch/mod.rs rename to src/maildir/mod.rs diff --git a/src/domain/backend/maildir/wizard.rs b/src/maildir/wizard.rs similarity index 82% rename from src/domain/backend/maildir/wizard.rs rename to src/maildir/wizard.rs index 9dd96bb..04a1e03 100644 --- a/src/domain/backend/maildir/wizard.rs +++ b/src/maildir/wizard.rs @@ -1,9 +1,9 @@ use anyhow::Result; use dialoguer::Input; use dirs::home_dir; -use email::backend::{BackendConfig, MaildirConfig}; +use email::maildir::MaildirConfig; -use crate::config::wizard::THEME; +use crate::{backend::config::BackendConfig, config::wizard::THEME}; pub(crate) fn configure() -> Result { let mut config = MaildirConfig::default(); diff --git a/src/main.rs b/src/main.rs index 18213e2..59df0cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,8 +5,6 @@ use log::{debug, warn}; use std::env; use url::Url; -#[cfg(feature = "imap-backend")] -use himalaya::imap; use himalaya::{ account, backend::{Backend, BackendBuilder}, @@ -18,7 +16,7 @@ use himalaya::{ }; fn create_app() -> Command { - let app = Command::new(env!("CARGO_PKG_NAME")) + Command::new(env!("CARGO_PKG_NAME")) .version(env!("CARGO_PKG_VERSION")) .about(env!("CARGO_PKG_DESCRIPTION")) .author(env!("CARGO_PKG_AUTHORS")) @@ -33,12 +31,7 @@ fn create_app() -> Command { .subcommand(man::args::subcmd()) .subcommand(account::args::subcmd()) .subcommand(folder::args::subcmd()) - .subcommands(email::args::subcmds()); - - #[cfg(feature = "imap-backend")] - let app = app.subcommands(imap::args::subcmds()); - - app + .subcommands(email::args::subcmds()) } #[allow(clippy::single_match)] @@ -57,7 +50,7 @@ async fn main() -> Result<()> { let raw_args: Vec = env::args().collect(); if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { let url = Url::parse(&raw_args[1])?; - let (toml_account_config, account_config) = TomlConfig::from_maybe_path(None) + let (toml_account_config, account_config) = TomlConfig::from_default_paths() .await? .into_account_configs(None, false)?; let backend_builder = @@ -88,12 +81,12 @@ async fn main() -> Result<()> { _ => (), } - let maybe_config_path = config::args::parse_arg(&m); - let maybe_account_name = account::args::parse_arg(&m); + let some_config_path = config::args::parse_arg(&m); + let some_account_name = account::args::parse_arg(&m); let disable_cache = cache::args::parse_disable_cache_flag(&m); let folder = folder::args::parse_source_arg(&m); - let toml_config = TomlConfig::from_maybe_path(maybe_config_path).await?; + let toml_config = TomlConfig::from_some_path_or_default(some_config_path).await?; let mut printer = StdoutPrinter::try_from(&m)?; @@ -122,13 +115,13 @@ async fn main() -> Result<()> { Some(account::args::Cmd::List(max_width)) => { let (_, account_config) = toml_config .clone() - .into_account_configs(maybe_account_name, disable_cache)?; + .into_account_configs(some_account_name, disable_cache)?; return account::handlers::list(max_width, &account_config, &toml_config, &mut printer); } Some(account::args::Cmd::Sync(strategy, dry_run)) => { let (toml_account_config, account_config) = toml_config .clone() - .into_account_configs(maybe_account_name, true)?; + .into_account_configs(some_account_name, true)?; let backend_builder = BackendBuilder::new(toml_account_config, account_config.clone(), false).await?; let sync_builder = AccountSyncBuilder::new(backend_builder.into()) @@ -140,7 +133,7 @@ async fn main() -> Result<()> { Some(account::args::Cmd::Configure(reset)) => { let (_, account_config) = toml_config .clone() - .into_account_configs(maybe_account_name, disable_cache)?; + .into_account_configs(some_account_name, disable_cache)?; return account::handlers::configure(&account_config, reset).await; } _ => (), @@ -148,7 +141,7 @@ async fn main() -> Result<()> { let (toml_account_config, account_config) = toml_config .clone() - .into_account_configs(maybe_account_name, disable_cache)?; + .into_account_configs(some_account_name, disable_cache)?; // checks folder commands match folder::args::matches(&m)? { diff --git a/src/notmuch/mod.rs b/src/notmuch/mod.rs new file mode 100644 index 0000000..73818b4 --- /dev/null +++ b/src/notmuch/mod.rs @@ -0,0 +1 @@ +pub(crate) mod wizard; diff --git a/src/domain/backend/notmuch/wizard.rs b/src/notmuch/wizard.rs similarity index 100% rename from src/domain/backend/notmuch/wizard.rs rename to src/notmuch/wizard.rs diff --git a/src/sendmail/mod.rs b/src/sendmail/mod.rs new file mode 100644 index 0000000..73818b4 --- /dev/null +++ b/src/sendmail/mod.rs @@ -0,0 +1 @@ +pub(crate) mod wizard; diff --git a/src/domain/sender/sendmail/wizard.rs b/src/sendmail/wizard.rs similarity index 60% rename from src/domain/sender/sendmail/wizard.rs rename to src/sendmail/wizard.rs index 0297ea5..e2a2896 100644 --- a/src/domain/sender/sendmail/wizard.rs +++ b/src/sendmail/wizard.rs @@ -1,10 +1,10 @@ use anyhow::Result; use dialoguer::Input; -use email::sender::{SenderConfig, SendmailConfig}; +use email::sendmail::SendmailConfig; -use crate::config::wizard::THEME; +use crate::{backend::config::BackendConfig, config::wizard::THEME}; -pub(crate) fn configure() -> Result { +pub(crate) fn configure() -> Result { let mut config = SendmailConfig::default(); config.cmd = Input::with_theme(&*THEME) @@ -13,5 +13,5 @@ pub(crate) fn configure() -> Result { .interact()? .into(); - Ok(SenderConfig::Sendmail(config)) + Ok(BackendConfig::Sendmail(config)) } diff --git a/src/smtp/mod.rs b/src/smtp/mod.rs new file mode 100644 index 0000000..73818b4 --- /dev/null +++ b/src/smtp/mod.rs @@ -0,0 +1 @@ +pub(crate) mod wizard; diff --git a/src/domain/sender/smtp/wizard.rs b/src/smtp/wizard.rs similarity index 98% rename from src/domain/sender/smtp/wizard.rs rename to src/smtp/wizard.rs index 423f155..6dab9cf 100644 --- a/src/domain/sender/smtp/wizard.rs +++ b/src/smtp/wizard.rs @@ -2,12 +2,13 @@ use anyhow::Result; use dialoguer::{Confirm, Input, Select}; use email::{ account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig}, - sender::{SenderConfig, SmtpAuthConfig, SmtpConfig}, + smtp::{SmtpAuthConfig, SmtpConfig}, }; use oauth::v2_0::{AuthorizationCodeGrant, Client}; use secret::Secret; use crate::{ + backend::config::BackendConfig, config::wizard::{prompt_passwd, THEME}, wizard_log, wizard_prompt, }; @@ -30,7 +31,7 @@ const KEYRING: &str = "Ask the password, then save it in my system's global keyr const RAW: &str = "Ask the password, then save it in the configuration file (not safe)"; const CMD: &str = "Use a shell command that exposes the password"; -pub(crate) async fn configure(account_name: &str, email: &str) -> Result { +pub(crate) async fn configure(account_name: &str, email: &str) -> Result { let mut config = SmtpConfig::default(); config.host = Input::with_theme(&*THEME) @@ -218,5 +219,5 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result SmtpAuthConfig::default(), }; - Ok(SenderConfig::Smtp(config)) + Ok(BackendConfig::Smtp(config)) } From ea9c28b9d7c13170235eb83475cbb9d19686b404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Mon, 4 Dec 2023 16:25:56 +0100 Subject: [PATCH 12/29] fix config and oauth2 --- src/backend/wizard.rs | 6 +- src/config/config.rs | 16 +- src/config/prelude.rs | 326 +++++++++++++++++++++-------- src/config/wizard.rs | 92 ++++++-- src/domain/account/config.rs | 56 ++++- src/domain/account/handlers.rs | 126 +++++------ src/domain/email/message/config.rs | 2 +- src/imap/wizard.rs | 2 +- src/main.rs | 4 +- 9 files changed, 453 insertions(+), 177 deletions(-) diff --git a/src/backend/wizard.rs b/src/backend/wizard.rs index 6d9ddca..4234889 100644 --- a/src/backend/wizard.rs +++ b/src/backend/wizard.rs @@ -12,17 +12,17 @@ use crate::{config::wizard::THEME, maildir, sendmail}; use super::{config::BackendConfig, BackendKind}; const DEFAULT_BACKEND_KINDS: &[BackendKind] = &[ - BackendKind::Maildir, #[cfg(feature = "imap-backend")] BackendKind::Imap, + BackendKind::Maildir, #[cfg(feature = "notmuch-backend")] BackendKind::Notmuch, ]; const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[ - BackendKind::Sendmail, #[cfg(feature = "smtp-sender")] BackendKind::Smtp, + BackendKind::Sendmail, ]; pub(crate) async fn configure(account_name: &str, email: &str) -> Result> { @@ -52,7 +52,7 @@ pub(crate) async fn configure_sender( email: &str, ) -> Result> { let kind = Select::with_theme(&*THEME) - .with_prompt("Default email backend") + .with_prompt("Backend for sending messages") .items(SEND_MESSAGE_BACKEND_KINDS) .default(0) .interact_opt()? diff --git a/src/config/config.rs b/src/config/config.rs index 574dad1..cdc4670 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -44,11 +44,19 @@ pub struct TomlConfig { pub email_listing_datetime_fmt: Option, pub email_listing_datetime_local_tz: Option, pub email_reading_headers: Option>, - #[serde(default, with = "OptionEmailTextPlainFormatDef")] + #[serde( + default, + with = "OptionEmailTextPlainFormatDef", + skip_serializing_if = "Option::is_none" + )] pub email_reading_format: Option, pub email_writing_headers: Option>, pub email_sending_save_copy: Option, - #[serde(default, with = "OptionEmailHooksDef")] + #[serde( + default, + with = "OptionEmailHooksDef", + skip_serializing_if = "Option::is_none" + )] pub email_hooks: Option, #[serde(flatten)] @@ -78,7 +86,7 @@ impl TomlConfig { let confirm = Confirm::new() .with_prompt(wizard_prompt!( - "Would you like to create it with the wizard?" + "Would you like to create one with the wizard?" )) .default(true) .interact_opt()? @@ -88,7 +96,7 @@ impl TomlConfig { process::exit(0); } - wizard::configure().await + wizard::configure(path).await } /// Read and parse the TOML configuration from default paths. diff --git a/src/config/prelude.rs b/src/config/prelude.rs index f61982b..df1aaa7 100644 --- a/src/config/prelude.rs +++ b/src/config/prelude.rs @@ -61,27 +61,38 @@ pub enum CmdDef { } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(remote = "Option", from = "OptionCmd")] +#[serde(remote = "Option", from = "OptionCmd", into = "OptionCmd")] pub struct OptionCmdDef; #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum OptionCmd { - #[default] - #[serde(skip_serializing)] - None, - #[serde(with = "SingleCmdDef")] - SingleCmd(SingleCmd), - #[serde(with = "PipelineDef")] - Pipeline(Pipeline), +pub struct OptionCmd { + #[serde(default, skip)] + is_some: bool, + #[serde(flatten, with = "CmdDef")] + inner: Cmd, } impl From for Option { fn from(cmd: OptionCmd) -> Option { - match cmd { - OptionCmd::None => None, - OptionCmd::SingleCmd(cmd) => Some(Cmd::SingleCmd(cmd)), - OptionCmd::Pipeline(pipeline) => Some(Cmd::Pipeline(pipeline)), + if cmd.is_some { + Some(cmd.inner) + } else { + None + } + } +} + +impl Into for Option { + fn into(self) -> OptionCmd { + match self { + Some(cmd) => OptionCmd { + is_some: true, + inner: cmd, + }, + None => OptionCmd { + is_some: false, + inner: Default::default(), + }, } } } @@ -108,7 +119,11 @@ pub enum OAuth2MethodDef { } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(remote = "Option", from = "OptionImapConfig")] +#[serde( + remote = "Option", + from = "OptionImapConfig", + into = "OptionImapConfig" +)] pub struct OptionImapConfigDef; #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] @@ -129,6 +144,21 @@ impl From for Option { } } +impl Into for Option { + fn into(self) -> OptionImapConfig { + match self { + Some(config) => OptionImapConfig { + is_none: false, + inner: config, + }, + None => OptionImapConfig { + is_none: true, + inner: Default::default(), + }, + } + } +} + #[cfg(feature = "imap-backend")] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(remote = "ImapConfig", rename_all = "kebab-case")] @@ -226,23 +256,42 @@ pub enum ImapOAuth2ScopesDef { } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(remote = "Option", from = "OptionMaildirConfig")] +#[serde( + remote = "Option", + from = "OptionMaildirConfig", + into = "OptionMaildirConfig" +)] pub struct OptionMaildirConfigDef; #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum OptionMaildirConfig { - #[default] - #[serde(skip_serializing)] - None, - Some(#[serde(with = "MaildirConfigDef")] MaildirConfig), +pub struct OptionMaildirConfig { + #[serde(default, skip)] + is_none: bool, + #[serde(flatten, with = "MaildirConfigDef")] + inner: MaildirConfig, } impl From for Option { fn from(config: OptionMaildirConfig) -> Option { - match config { - OptionMaildirConfig::None => None, - OptionMaildirConfig::Some(config) => Some(config), + if config.is_none { + None + } else { + Some(config.inner) + } + } +} + +impl Into for Option { + fn into(self) -> OptionMaildirConfig { + match self { + Some(config) => OptionMaildirConfig { + is_none: false, + inner: config, + }, + None => OptionMaildirConfig { + is_none: true, + inner: Default::default(), + }, } } } @@ -256,25 +305,45 @@ pub struct MaildirConfigDef { #[cfg(feature = "notmuch-backend")] #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(remote = "Option", from = "OptionNotmuchConfig")] +#[serde( + remote = "Option", + from = "OptionNotmuchConfig", + into = "OptionNotmuchConfig" +)] pub struct OptionNotmuchConfigDef; #[cfg(feature = "notmuch-backend")] #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum OptionNotmuchConfig { - #[default] - #[serde(skip_serializing)] - None, - Some(#[serde(with = "NotmuchConfigDef")] NotmuchConfig), +pub struct OptionNotmuchConfig { + #[serde(default, skip)] + is_none: bool, + #[serde(flatten, with = "NotmuchConfigDef")] + inner: NotmuchConfig, } #[cfg(feature = "notmuch-backend")] impl From for Option { fn from(config: OptionNotmuchConfig) -> Option { - match config { - OptionNotmuchConfig::None => None, - OptionNotmuchConfig::Some(config) => Some(config), + if config.is_none { + None + } else { + Some(config.inner) + } + } +} + +#[cfg(feature = "notmuch-backend")] +impl Into for Option { + fn into(self) -> OptionNotmuchConfig { + match self { + Some(config) => OptionNotmuchConfig { + is_none: false, + inner: config, + }, + None => OptionNotmuchConfig { + is_none: true, + inner: Default::default(), + }, } } } @@ -290,28 +359,40 @@ pub struct NotmuchConfigDef { #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde( remote = "Option", - from = "OptionEmailTextPlainFormat" + from = "OptionEmailTextPlainFormat", + into = "OptionEmailTextPlainFormat" )] pub struct OptionEmailTextPlainFormatDef; #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum OptionEmailTextPlainFormat { - #[serde(skip_serializing)] - None, - #[default] - Auto, - Flowed, - Fixed(usize), +pub struct OptionEmailTextPlainFormat { + #[serde(default, skip)] + is_none: bool, + #[serde(flatten, with = "EmailTextPlainFormatDef")] + inner: EmailTextPlainFormat, } impl From for Option { fn from(fmt: OptionEmailTextPlainFormat) -> Option { - match fmt { - OptionEmailTextPlainFormat::None => None, - OptionEmailTextPlainFormat::Auto => Some(EmailTextPlainFormat::Auto), - OptionEmailTextPlainFormat::Flowed => Some(EmailTextPlainFormat::Flowed), - OptionEmailTextPlainFormat::Fixed(size) => Some(EmailTextPlainFormat::Fixed(size)), + if fmt.is_none { + None + } else { + Some(fmt.inner) + } + } +} + +impl Into for Option { + fn into(self) -> OptionEmailTextPlainFormat { + match self { + Some(config) => OptionEmailTextPlainFormat { + is_none: false, + inner: config, + }, + None => OptionEmailTextPlainFormat { + is_none: true, + inner: Default::default(), + }, } } } @@ -331,7 +412,11 @@ pub enum EmailTextPlainFormatDef { } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(remote = "Option", from = "OptionSmtpConfig")] +#[serde( + remote = "Option", + from = "OptionSmtpConfig", + into = "OptionSmtpConfig" +)] pub struct OptionSmtpConfigDef; #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] @@ -352,6 +437,21 @@ impl From for Option { } } +impl Into for Option { + fn into(self) -> OptionSmtpConfig { + match self { + Some(config) => OptionSmtpConfig { + is_none: false, + inner: config, + }, + None => OptionSmtpConfig { + is_none: true, + inner: Default::default(), + }, + } + } +} + #[cfg(feature = "smtp-sender")] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(remote = "SmtpConfig")] @@ -434,23 +534,42 @@ pub enum SmtpOAuth2ScopesDef { } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(remote = "Option", from = "OptionSendmailConfig")] +#[serde( + remote = "Option", + from = "OptionSendmailConfig", + into = "OptionSendmailConfig" +)] pub struct OptionSendmailConfigDef; #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum OptionSendmailConfig { - #[default] - #[serde(skip_serializing)] - None, - Some(#[serde(with = "SendmailConfigDef")] SendmailConfig), +pub struct OptionSendmailConfig { + #[serde(default, skip)] + is_none: bool, + #[serde(flatten, with = "SendmailConfigDef")] + inner: SendmailConfig, } impl From for Option { fn from(config: OptionSendmailConfig) -> Option { - match config { - OptionSendmailConfig::None => None, - OptionSendmailConfig::Some(config) => Some(config), + if config.is_none { + None + } else { + Some(config.inner) + } + } +} + +impl Into for Option { + fn into(self) -> OptionSendmailConfig { + match self { + Some(config) => OptionSendmailConfig { + is_none: false, + inner: config, + }, + None => OptionSendmailConfig { + is_none: true, + inner: Default::default(), + }, } } } @@ -467,23 +586,46 @@ fn sendmail_default_cmd() -> Cmd { } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(remote = "Option", from = "OptionEmailHooks")] +#[serde( + remote = "Option", + from = "OptionEmailHooks", + into = "OptionEmailHooks" +)] pub struct OptionEmailHooksDef; #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum OptionEmailHooks { - #[default] - #[serde(skip_serializing)] - None, - Some(#[serde(with = "EmailHooksDef")] EmailHooks), +pub struct OptionEmailHooks { + #[serde(default, skip)] + is_none: bool, + #[serde( + flatten, + skip_serializing_if = "EmailHooks::is_empty", + with = "EmailHooksDef" + )] + inner: EmailHooks, } impl From for Option { - fn from(fmt: OptionEmailHooks) -> Option { - match fmt { - OptionEmailHooks::None => None, - OptionEmailHooks::Some(hooks) => Some(hooks), + fn from(hooks: OptionEmailHooks) -> Option { + if hooks.is_none { + None + } else { + Some(hooks.inner) + } + } +} + +impl Into for Option { + fn into(self) -> OptionEmailHooks { + match self { + Some(hooks) => OptionEmailHooks { + is_none: false, + inner: hooks, + }, + None => OptionEmailHooks { + is_none: true, + inner: Default::default(), + }, } } } @@ -501,24 +643,44 @@ pub struct EmailHooksDef { #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde( remote = "Option", - from = "OptionFolderSyncStrategy" + from = "OptionFolderSyncStrategy", + into = "OptionFolderSyncStrategy" )] pub struct OptionFolderSyncStrategyDef; #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum OptionFolderSyncStrategy { - #[default] - #[serde(skip_serializing)] - None, - Some(#[serde(with = "FolderSyncStrategyDef")] FolderSyncStrategy), +pub struct OptionFolderSyncStrategy { + #[serde(default, skip)] + is_some: bool, + #[serde( + flatten, + skip_serializing_if = "FolderSyncStrategy::is_default", + with = "FolderSyncStrategyDef" + )] + inner: FolderSyncStrategy, } impl From for Option { - fn from(config: OptionFolderSyncStrategy) -> Option { - match config { - OptionFolderSyncStrategy::None => None, - OptionFolderSyncStrategy::Some(config) => Some(config), + fn from(option: OptionFolderSyncStrategy) -> Option { + if option.is_some { + Some(option.inner) + } else { + None + } + } +} + +impl Into for Option { + fn into(self) -> OptionFolderSyncStrategy { + match self { + Some(strategy) => OptionFolderSyncStrategy { + is_some: true, + inner: strategy, + }, + None => OptionFolderSyncStrategy { + is_some: false, + inner: Default::default(), + }, } } } diff --git a/src/config/wizard.rs b/src/config/wizard.rs index f6b359e..ffb3a23 100644 --- a/src/config/wizard.rs +++ b/src/config/wizard.rs @@ -1,10 +1,13 @@ -use super::TomlConfig; -use crate::account; use anyhow::Result; use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select}; use once_cell::sync::Lazy; -use shellexpand_utils::{shellexpand_path, try_shellexpand_path}; -use std::{env, fs, io, process}; +use shellexpand_utils::shellexpand_path; +use std::{fs, io, path::PathBuf, process}; +use toml_edit::{Document, Item}; + +use crate::account; + +use super::TomlConfig; #[macro_export] macro_rules! wizard_warn { @@ -31,7 +34,7 @@ macro_rules! wizard_log { pub(crate) static THEME: Lazy = Lazy::new(ColorfulTheme::default); -pub(crate) async fn configure() -> Result { +pub(crate) async fn configure(path: PathBuf) -> Result { wizard_log!("Configuring your first account:"); let mut config = TomlConfig::default(); @@ -89,25 +92,86 @@ pub(crate) async fn configure() -> Result { .with_prompt(wizard_prompt!( "Where would you like to save your configuration?" )) - .default( - dirs::config_dir() - .map(|p| p.join("himalaya").join("config.toml")) - .unwrap_or_else(|| env::temp_dir().join("himalaya").join("config.toml")) - .to_string_lossy() - .to_string(), - ) - .validate_with(|path: &String| try_shellexpand_path(path).map(|_| ())) + .default(path.to_string_lossy().to_string()) .interact()?; let path = shellexpand_path(&path); println!("Writing the configuration to {path:?}…"); + let mut doc = toml::to_string(&config)?.parse::()?; + + doc.iter_mut().for_each(|(_, item)| { + set_table_dotted(item, "folder-aliases"); + set_table_dotted(item, "sync-folders-strategy"); + + set_table_dotted(item, "folder"); + get_table_mut(item, "folder").map(|item| { + set_tables_dotted(item, ["add", "list", "expunge", "purge", "delete"]); + }); + + set_table_dotted(item, "envelope"); + get_table_mut(item, "envelope").map(|item| { + set_tables_dotted(item, ["list", "get"]); + }); + + set_table_dotted(item, "flag"); + get_table_mut(item, "flag").map(|item| { + set_tables_dotted(item, ["add", "set", "remove"]); + }); + + set_table_dotted(item, "message"); + get_table_mut(item, "message").map(|item| { + set_tables_dotted( + item, + ["add", "send", "peek", "get", "copy", "move", "delete"], + ); + }); + + set_table_dotted(item, "maildir"); + #[cfg(feature = "imap-backend")] + { + set_table_dotted(item, "imap"); + get_table_mut(item, "imap").map(|item| { + set_tables_dotted(item, ["passwd", "oauth2"]); + }); + } + #[cfg(feature = "notmuch-backend")] + set_table_dotted(item, "notmuch"); + set_table_dotted(item, "sendmail"); + #[cfg(feature = "smtp-sender")] + { + set_table_dotted(item, "smtp"); + get_table_mut(item, "smtp").map(|item| { + set_tables_dotted(item, ["passwd", "oauth2"]); + }); + } + + #[cfg(feature = "pgp")] + set_table_dotted(item, "pgp"); + }); + fs::create_dir_all(path.parent().unwrap_or(&path))?; - fs::write(path, toml::to_string(&config)?)?; + fs::write(path, doc.to_string())?; Ok(config) } +fn get_table_mut<'a>(item: &'a mut Item, key: &'a str) -> Option<&'a mut Item> { + item.get_mut(key).filter(|item| item.is_table()) +} + +fn set_table_dotted(item: &mut Item, key: &str) { + get_table_mut(item, key) + .and_then(|item| item.as_table_mut()) + .map(|table| table.set_dotted(true)); +} + +fn set_tables_dotted<'a>(item: &'a mut Item, keys: impl IntoIterator) { + for key in keys { + set_table_dotted(item, key) + } +} + pub(crate) fn prompt_passwd(prompt: &str) -> io::Result { Password::with_theme(&*THEME) .with_prompt(prompt) diff --git a/src/domain/account/config.rs b/src/domain/account/config.rs index d514396..01186ae 100644 --- a/src/domain/account/config.rs +++ b/src/domain/account/config.rs @@ -31,7 +31,7 @@ use crate::{ /// Represents all existing kind of account config. #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] -#[serde(tag = "backend", rename_all = "kebab-case")] +#[serde(rename_all = "kebab-case")] pub struct TomlAccountConfig { pub default: Option, @@ -48,16 +48,28 @@ pub struct TomlAccountConfig { pub email_listing_datetime_fmt: Option, pub email_listing_datetime_local_tz: Option, pub email_reading_headers: Option>, - #[serde(default, with = "OptionEmailTextPlainFormatDef")] + #[serde( + default, + with = "OptionEmailTextPlainFormatDef", + skip_serializing_if = "Option::is_none" + )] pub email_reading_format: Option, pub email_writing_headers: Option>, pub email_sending_save_copy: Option, - #[serde(default, with = "OptionEmailHooksDef")] + #[serde( + default, + with = "OptionEmailHooksDef", + skip_serializing_if = "Option::is_none" + )] pub email_hooks: Option, pub sync: Option, pub sync_dir: Option, - #[serde(default, with = "OptionFolderSyncStrategyDef")] + #[serde( + default, + with = "OptionFolderSyncStrategyDef", + skip_serializing_if = "Option::is_none" + )] pub sync_folders_strategy: Option, pub backend: Option, @@ -68,25 +80,49 @@ pub struct TomlAccountConfig { pub message: Option, #[cfg(feature = "imap-backend")] - #[serde(default, with = "OptionImapConfigDef")] + #[serde( + default, + with = "OptionImapConfigDef", + skip_serializing_if = "Option::is_none" + )] pub imap: Option, - #[serde(default, with = "OptionMaildirConfigDef")] + #[serde( + default, + with = "OptionMaildirConfigDef", + skip_serializing_if = "Option::is_none" + )] pub maildir: Option, #[cfg(feature = "notmuch-backend")] - #[serde(default, with = "OptionNotmuchConfigDef")] + #[serde( + default, + with = "OptionNotmuchConfigDef", + skip_serializing_if = "Option::is_none" + )] pub notmuch: Option, #[cfg(feature = "smtp-sender")] - #[serde(default, with = "OptionSmtpConfigDef")] + #[serde( + default, + with = "OptionSmtpConfigDef", + skip_serializing_if = "Option::is_none" + )] pub smtp: Option, - #[serde(default, with = "OptionSendmailConfigDef")] + #[serde( + default, + with = "OptionSendmailConfigDef", + skip_serializing_if = "Option::is_none" + )] pub sendmail: Option, #[cfg(feature = "pgp")] - #[serde(default, with = "OptionPgpConfigDef")] + #[serde( + default, + with = "OptionPgpConfigDef", + skip_serializing_if = "Option::is_none" + )] pub pgp: Option, } diff --git a/src/domain/account/handlers.rs b/src/domain/account/handlers.rs index 29d5a5b..0905563 100644 --- a/src/domain/account/handlers.rs +++ b/src/domain/account/handlers.rs @@ -2,7 +2,7 @@ //! //! This module gathers all account actions triggered by the CLI. -use anyhow::{Context, Result}; +use anyhow::Result; use email::account::{ sync::{AccountSyncBuilder, AccountSyncProgressEvent}, AccountConfig, @@ -12,7 +12,7 @@ use email::imap::ImapAuthConfig; #[cfg(feature = "smtp-sender")] use email::smtp::SmtpAuthConfig; use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle}; -use log::{info, trace, warn}; +use log::{debug, info, trace, warn}; use once_cell::sync::Lazy; use std::{collections::HashMap, sync::Mutex}; @@ -26,6 +26,8 @@ use crate::{ Accounts, }; +use super::TomlAccountConfig; + const MAIN_PROGRESS_STYLE: Lazy = Lazy::new(|| { ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap() }); @@ -42,71 +44,75 @@ const SUB_PROGRESS_DONE_STYLE: Lazy = Lazy::new(|| { }); /// Configure the current selected account -pub async fn configure(config: &AccountConfig, reset: bool) -> Result<()> { +pub async fn configure(config: &TomlAccountConfig, reset: bool) -> Result<()> { info!("entering the configure account handler"); - // if reset { - // #[cfg(feature = "imap-backend")] - // if let BackendConfig::Imap(imap_config) = &config.backend { - // let reset = match &imap_config.auth { - // ImapAuthConfig::Passwd(passwd) => passwd.reset(), - // ImapAuthConfig::OAuth2(oauth2) => oauth2.reset(), - // }; - // if let Err(err) = reset { - // warn!("error while resetting imap secrets, skipping it"); - // warn!("{err}"); - // } - // } + if reset { + #[cfg(feature = "imap-backend")] + if let Some(ref config) = config.imap { + let reset = match &config.auth { + ImapAuthConfig::Passwd(config) => config.reset(), + ImapAuthConfig::OAuth2(config) => config.reset(), + }; + if let Err(err) = reset { + warn!("error while resetting imap secrets: {err}"); + debug!("error while resetting imap secrets: {err:?}"); + } + } - // #[cfg(feature = "smtp-sender")] - // if let SenderConfig::Smtp(smtp_config) = &config.sender { - // let reset = match &smtp_config.auth { - // SmtpAuthConfig::Passwd(passwd) => passwd.reset(), - // SmtpAuthConfig::OAuth2(oauth2) => oauth2.reset(), - // }; - // if let Err(err) = reset { - // warn!("error while resetting smtp secrets, skipping it"); - // warn!("{err}"); - // } - // } + #[cfg(feature = "smtp-sender")] + if let Some(ref config) = config.smtp { + let reset = match &config.auth { + SmtpAuthConfig::Passwd(config) => config.reset(), + SmtpAuthConfig::OAuth2(config) => config.reset(), + }; + if let Err(err) = reset { + warn!("error while resetting smtp secrets: {err}"); + debug!("error while resetting smtp secrets: {err:?}"); + } + } - // #[cfg(feature = "pgp")] - // config.pgp.reset().await?; - // } + #[cfg(feature = "pgp")] + if let Some(ref config) = config.pgp { + config.pgp.reset().await?; + } + } - // #[cfg(feature = "imap-backend")] - // if let BackendConfig::Imap(imap_config) = &config.backend { - // match &imap_config.auth { - // ImapAuthConfig::Passwd(passwd) => { - // passwd.configure(|| prompt_passwd("IMAP password")).await - // } - // ImapAuthConfig::OAuth2(oauth2) => { - // oauth2 - // .configure(|| prompt_secret("IMAP OAuth 2.0 client secret")) - // .await - // } - // }?; - // } + #[cfg(feature = "imap-backend")] + if let Some(ref config) = config.imap { + match &config.auth { + ImapAuthConfig::Passwd(config) => { + config.configure(|| prompt_passwd("IMAP password")).await + } + ImapAuthConfig::OAuth2(config) => { + config + .configure(|| prompt_secret("IMAP OAuth 2.0 client secret")) + .await + } + }?; + } - // #[cfg(feature = "smtp-sender")] - // if let SenderConfig::Smtp(smtp_config) = &config.sender { - // match &smtp_config.auth { - // SmtpAuthConfig::Passwd(passwd) => { - // passwd.configure(|| prompt_passwd("SMTP password")).await - // } - // SmtpAuthConfig::OAuth2(oauth2) => { - // oauth2 - // .configure(|| prompt_secret("SMTP OAuth 2.0 client secret")) - // .await - // } - // }?; - // } + #[cfg(feature = "smtp-sender")] + if let Some(ref config) = config.smtp { + match &config.auth { + SmtpAuthConfig::Passwd(config) => { + config.configure(|| prompt_passwd("SMTP password")).await + } + SmtpAuthConfig::OAuth2(config) => { + config + .configure(|| prompt_secret("SMTP OAuth 2.0 client secret")) + .await + } + }?; + } - // #[cfg(feature = "pgp")] - // config - // .pgp - // .configure(&config.email, || prompt_passwd("PGP secret key password")) - // .await?; + #[cfg(feature = "pgp")] + if let Some(ref config) = config.pgp { + config + .pgp + .configure(&config.email, || prompt_passwd("PGP secret key password")) + .await?; + } println!( "Account successfully {}configured!", diff --git a/src/domain/email/message/config.rs b/src/domain/email/message/config.rs index 0ab96a4..0e3f080 100644 --- a/src/domain/email/message/config.rs +++ b/src/domain/email/message/config.rs @@ -10,7 +10,7 @@ pub struct MessageConfig { pub peek: Option, pub get: Option, pub copy: Option, - #[serde(rename = "move")] + #[serde(default, rename = "move", skip_serializing_if = "Option::is_none")] pub move_: Option, } diff --git a/src/imap/wizard.rs b/src/imap/wizard.rs index 7ea750e..00d6017 100644 --- a/src/imap/wizard.rs +++ b/src/imap/wizard.rs @@ -105,7 +105,7 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result { - let mut config = OAuth2Config::default(); + let mut config = OAuth2Config::new()?; let method = Select::with_theme(&*THEME) .with_prompt("IMAP OAuth 2.0 mechanism") diff --git a/src/main.rs b/src/main.rs index 59df0cb..46a9128 100644 --- a/src/main.rs +++ b/src/main.rs @@ -131,10 +131,10 @@ async fn main() -> Result<()> { return account::handlers::sync(&mut printer, sync_builder, dry_run).await; } Some(account::args::Cmd::Configure(reset)) => { - let (_, account_config) = toml_config + let (toml_account_config, _) = toml_config .clone() .into_account_configs(some_account_name, disable_cache)?; - return account::handlers::configure(&account_config, reset).await; + return account::handlers::configure(&toml_account_config, reset).await; } _ => (), } From 8b1a289f4dfea3837fe2b6d62b2f04799eab4827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Mon, 4 Dec 2023 22:26:49 +0100 Subject: [PATCH 13/29] rename existing cargo features, fix imports --- Cargo.lock | 1 + Cargo.toml | 24 +++-- src/backend/config.rs | 20 ++-- src/backend/mod.rs | 156 +++++++++++++++---------------- src/backend/wizard.rs | 18 ++-- src/cache/id_mapper.rs | 2 +- src/config/config.rs | 29 +++--- src/config/prelude.rs | 39 ++++---- src/config/wizard.rs | 6 +- src/domain/account/accounts.rs | 6 +- src/domain/account/config.rs | 20 ++-- src/domain/account/handlers.rs | 26 +++--- src/domain/account/wizard.rs | 6 +- src/domain/email/handlers.rs | 5 +- src/domain/envelope/envelopes.rs | 12 +-- src/domain/flag/args.rs | 2 +- src/domain/flag/flag.rs | 6 +- src/domain/flag/flags.rs | 4 +- src/domain/flag/handlers.rs | 2 +- src/domain/folder/handlers.rs | 2 +- src/domain/tpl/handlers.rs | 5 +- src/imap/wizard.rs | 7 +- src/lib.rs | 6 +- src/maildir/wizard.rs | 2 +- src/main.rs | 4 +- src/printer/print_table.rs | 2 +- src/sendmail/wizard.rs | 2 +- src/smtp/wizard.rs | 7 +- src/ui/editor.rs | 5 +- src/ui/table/table.rs | 4 +- 30 files changed, 224 insertions(+), 206 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2dba8c7..2c12a90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2068,6 +2068,7 @@ dependencies = [ "indicatif", "keyring-lib", "log", + "mail-builder", "md5", "mml-lib", "oauth-lib 0.1.0", diff --git a/Cargo.toml b/Cargo.toml index 2f7eea1..9759e3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,20 +10,27 @@ keywords = ["cli", "mail", "email", "client", "imap"] homepage = "https://pimalaya.org/himalaya" documentation = "https://pimalaya.org/himalaya/" repository = "https://github.com/soywod/himalaya/" -metadata.docs.rs.all-features = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] [features] default = [ - "imap-backend", - # "notmuch-backend", - "smtp-sender", + "maildir", + "imap", + # "notmuch", + "smtp", + "sendmail", # "pgp-commands", # "pgp-gpg", # "pgp-native", ] -imap-backend = ["email-lib/imap-backend"] -notmuch-backend = ["email-lib/notmuch-backend"] -smtp-sender = ["email-lib/smtp-sender"] +maildir = ["email-lib/maildir"] +imap = ["email-lib/imap"] +notmuch = ["email-lib/notmuch"] +smtp = ["email-lib/smtp"] +sendmail = ["email-lib/sendmail"] pgp = [] pgp-commands = ["pgp", "mml-lib/pgp-commands", "email-lib/pgp-commands"] pgp-gpg = ["pgp", "mml-lib/pgp-gpg", "email-lib/pgp-gpg"] @@ -98,6 +105,9 @@ path = "/home/soywod/sourcehut/pimalaya/email" [dependencies.keyring-lib] version = "=0.1.0" +[dependencies.mail-builder] +version = "0.3" + [dependencies.oauth-lib] # version = "=0.1.0" path = "/home/soywod/sourcehut/pimalaya/oauth" diff --git a/src/backend/config.rs b/src/backend/config.rs index fb18479..2284de3 100644 --- a/src/backend/config.rs +++ b/src/backend/config.rs @@ -1,19 +1,19 @@ -#[cfg(feature = "imap-backend")] -use email::imap::ImapConfig; -#[cfg(feature = "notmuch-backend")] -use email::notmuch::NotmuchConfig; -#[cfg(feature = "smtp-sender")] -use email::smtp::SmtpConfig; -use email::{maildir::MaildirConfig, sendmail::SendmailConfig}; +#[cfg(feature = "imap")] +use email::imap::config::ImapConfig; +#[cfg(feature = "notmuch")] +use email::notmuch::config::NotmuchConfig; +#[cfg(feature = "smtp")] +use email::smtp::config::SmtpConfig; +use email::{maildir::config::MaildirConfig, sendmail::config::SendmailConfig}; #[derive(Clone, Debug, Eq, PartialEq)] pub enum BackendConfig { Maildir(MaildirConfig), - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Imap(ImapConfig), - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Notmuch(NotmuchConfig), - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] Smtp(SmtpConfig), Sendmail(SendmailConfig), } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index c92ddec..1c9af19 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -5,36 +5,23 @@ use anyhow::Result; use async_trait::async_trait; use std::ops::Deref; -#[cfg(feature = "imap-backend")] +#[cfg(feature = "imap")] use email::imap::{ImapSessionBuilder, ImapSessionSync}; -#[cfg(feature = "smtp-sender")] +#[cfg(feature = "smtp")] use email::smtp::{SmtpClientBuilder, SmtpClientSync}; use email::{ - account::AccountConfig, - email::{ - envelope::{ - get::{imap::GetEnvelopeImap, maildir::GetEnvelopeMaildir}, - list::{imap::ListEnvelopesImap, maildir::ListEnvelopesMaildir}, - }, - flag::{ - add::{imap::AddFlagsImap, maildir::AddFlagsMaildir}, - remove::{imap::RemoveFlagsImap, maildir::RemoveFlagsMaildir}, - set::{imap::SetFlagsImap, maildir::SetFlagsMaildir}, - }, - message::{ - add_raw::imap::AddRawMessageImap, - add_raw_with_flags::{ - imap::AddRawMessageWithFlagsImap, maildir::AddRawMessageWithFlagsMaildir, - }, - copy::{imap::CopyMessagesImap, maildir::CopyMessagesMaildir}, - get::imap::GetMessagesImap, - move_::{imap::MoveMessagesImap, maildir::MoveMessagesMaildir}, - peek::{imap::PeekMessagesImap, maildir::PeekMessagesMaildir}, - send_raw::{sendmail::SendRawMessageSendmail, smtp::SendRawMessageSmtp}, - }, + account::config::AccountConfig, + envelope::{ + get::{imap::GetEnvelopeImap, maildir::GetEnvelopeMaildir}, + list::{imap::ListEnvelopesImap, maildir::ListEnvelopesMaildir}, + Id, SingleId, + }, + flag::{ + add::{imap::AddFlagsImap, maildir::AddFlagsMaildir}, + remove::{imap::RemoveFlagsImap, maildir::RemoveFlagsMaildir}, + set::{imap::SetFlagsImap, maildir::SetFlagsMaildir}, + Flags, }, - envelope::{Id, SingleId}, - flag::Flags, folder::{ add::{imap::AddFolderImap, maildir::AddFolderMaildir}, delete::{imap::DeleteFolderImap, maildir::DeleteFolderMaildir}, @@ -42,8 +29,19 @@ use email::{ list::{imap::ListFoldersImap, maildir::ListFoldersMaildir}, purge::imap::PurgeFolderImap, }, - maildir::{MaildirConfig, MaildirSessionBuilder, MaildirSessionSync}, - message::Messages, + maildir::{config::MaildirConfig, MaildirSessionBuilder, MaildirSessionSync}, + message::{ + add_raw::imap::AddRawMessageImap, + add_raw_with_flags::{ + imap::AddRawMessageWithFlagsImap, maildir::AddRawMessageWithFlagsMaildir, + }, + copy::{imap::CopyMessagesImap, maildir::CopyMessagesMaildir}, + get::imap::GetMessagesImap, + move_::{imap::MoveMessagesImap, maildir::MoveMessagesMaildir}, + peek::{imap::PeekMessagesImap, maildir::PeekMessagesMaildir}, + send_raw::{sendmail::SendRawMessageSendmail, smtp::SendRawMessageSmtp}, + Messages, + }, sendmail::SendmailContext, }; use serde::{Deserialize, Serialize}; @@ -56,11 +54,11 @@ pub enum BackendKind { Maildir, #[serde(skip_deserializing)] MaildirForSync, - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Imap, - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Notmuch, - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] Smtp, Sendmail, } @@ -70,11 +68,11 @@ impl ToString for BackendKind { let kind = match self { Self::Maildir => "Maildir", Self::MaildirForSync => "Maildir", - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Self::Imap => "IMAP", - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Self::Notmuch => "Notmuch", - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] Self::Smtp => "SMTP", Self::Sendmail => "Sendmail", }; @@ -87,9 +85,9 @@ impl ToString for BackendKind { pub struct BackendContextBuilder { maildir: Option, maildir_for_sync: Option, - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] imap: Option, - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] smtp: Option, sendmail: Option, } @@ -109,17 +107,17 @@ impl email::backend::BackendContextBuilder for BackendContextBuilder { ctx.maildir_for_sync = Some(maildir.build().await?); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] if let Some(imap) = self.imap { ctx.imap = Some(imap.build().await?); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] if let Some(notmuch) = self.notmuch { ctx.notmuch = Some(notmuch.build().await?); } - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] if let Some(smtp) = self.smtp { ctx.smtp = Some(smtp.build().await?); } @@ -136,9 +134,9 @@ impl email::backend::BackendContextBuilder for BackendContextBuilder { pub struct BackendContext { pub maildir: Option, pub maildir_for_sync: Option, - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] pub imap: Option, - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] pub smtp: Option, pub sendmail: Option, } @@ -158,11 +156,11 @@ impl BackendBuilder { let is_maildir_used = used_backends.contains(&BackendKind::Maildir); let is_maildir_for_sync_used = used_backends.contains(&BackendKind::MaildirForSync); - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] let is_imap_used = used_backends.contains(&BackendKind::Imap); - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] let is_notmuch_used = used_backends.contains(&BackendKind::Notmuch); - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] let is_smtp_used = used_backends.contains(&BackendKind::Smtp); let is_sendmail_used = used_backends.contains(&BackendKind::Sendmail); @@ -180,7 +178,7 @@ impl BackendBuilder { .filter(|_| is_maildir_for_sync_used) .map(|mdir_config| MaildirSessionBuilder::new(account_config.clone(), mdir_config)), - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] imap: { let ctx_builder = toml_account_config .imap @@ -196,7 +194,7 @@ impl BackendBuilder { None => None, } }, - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] notmuch: toml_account_config .notmuch .as_ref() @@ -204,7 +202,7 @@ impl BackendBuilder { .map(|notmuch_config| { NotmuchSessionBuilder::new(account_config.clone(), notmuch_config.clone()) }), - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] smtp: toml_account_config .smtp .as_ref() @@ -238,12 +236,12 @@ impl BackendBuilder { .and_then(AddFolderMaildir::new) }); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_add_folder(|ctx| ctx.imap.as_ref().and_then(AddFolderImap::new)); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder .with_add_folder(|ctx| ctx.notmuch.as_ref().and_then(AddFolderNotmuch::new)); @@ -264,12 +262,12 @@ impl BackendBuilder { .and_then(ListFoldersMaildir::new) }); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_list_folders(|ctx| ctx.imap.as_ref().and_then(ListFoldersImap::new)); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder.with_list_folders(|ctx| { ctx.notmuch.as_ref().and_then(ListFoldersNotmuch::new) @@ -291,12 +289,12 @@ impl BackendBuilder { .and_then(ExpungeFolderMaildir::new) }); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_expunge_folder(|ctx| ctx.imap.as_ref().and_then(ExpungeFolderImap::new)); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder.with_expunge_folder(|ctx| { ctx.notmuch.as_ref().and_then(ExpungeFolderNotmuch::new) @@ -316,12 +314,12 @@ impl BackendBuilder { // backend_builder = backend_builder // .with_purge_folder(|ctx| ctx.maildir_for_sync.as_ref().and_then(PurgeFolderMaildir::new)); // } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_purge_folder(|ctx| ctx.imap.as_ref().and_then(PurgeFolderImap::new)); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder.with_purge_folder(|ctx| { ctx.notmuch.as_ref().and_then(PurgeFolderNotmuch::new) @@ -343,12 +341,12 @@ impl BackendBuilder { .and_then(DeleteFolderMaildir::new) }); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_delete_folder(|ctx| ctx.imap.as_ref().and_then(DeleteFolderImap::new)); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder.with_delete_folder(|ctx| { ctx.notmuch.as_ref().and_then(DeleteFolderNotmuch::new) @@ -370,12 +368,12 @@ impl BackendBuilder { .and_then(GetEnvelopeMaildir::new) }); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_get_envelope(|ctx| ctx.imap.as_ref().and_then(GetEnvelopeImap::new)); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder.with_get_envelope(|ctx| { ctx.notmuch.as_ref().and_then(GetEnvelopeNotmuch::new) @@ -397,12 +395,12 @@ impl BackendBuilder { .and_then(ListEnvelopesMaildir::new) }); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_list_envelopes(|ctx| ctx.imap.as_ref().and_then(ListEnvelopesImap::new)); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder.with_list_envelopes(|ctx| { ctx.notmuch.as_ref().and_then(ListEnvelopesNotmuch::new) @@ -421,12 +419,12 @@ impl BackendBuilder { ctx.maildir_for_sync.as_ref().and_then(AddFlagsMaildir::new) }); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_add_flags(|ctx| ctx.imap.as_ref().and_then(AddFlagsImap::new)); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder .with_add_flags(|ctx| ctx.notmuch.as_ref().and_then(AddFlagsNotmuch::new)); @@ -444,12 +442,12 @@ impl BackendBuilder { ctx.maildir_for_sync.as_ref().and_then(SetFlagsMaildir::new) }); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_set_flags(|ctx| ctx.imap.as_ref().and_then(SetFlagsImap::new)); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder .with_set_flags(|ctx| ctx.notmuch.as_ref().and_then(SetFlagsNotmuch::new)); @@ -470,12 +468,12 @@ impl BackendBuilder { .and_then(RemoveFlagsMaildir::new) }); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_remove_flags(|ctx| ctx.imap.as_ref().and_then(RemoveFlagsImap::new)); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder.with_remove_flags(|ctx| { ctx.notmuch.as_ref().and_then(RemoveFlagsNotmuch::new) @@ -485,7 +483,7 @@ impl BackendBuilder { } match toml_account_config.send_raw_message_kind() { - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] Some(BackendKind::Smtp) => { backend_builder = backend_builder.with_send_raw_message(|ctx| { ctx.smtp.as_ref().and_then(SendRawMessageSmtp::new) @@ -514,7 +512,7 @@ impl BackendBuilder { .and_then(AddRawMessageWithFlagsMaildir::new) }); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_add_raw_message(|ctx| ctx.imap.as_ref().and_then(AddRawMessageImap::new)) @@ -522,7 +520,7 @@ impl BackendBuilder { ctx.imap.as_ref().and_then(AddRawMessageWithFlagsImap::new) }); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder.with_add_raw_message(|ctx| { ctx.notmuch.as_ref().and_then(AddRawMessageNotmuch::new) @@ -544,12 +542,12 @@ impl BackendBuilder { .and_then(PeekMessagesMaildir::new) }); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_peek_messages(|ctx| ctx.imap.as_ref().and_then(PeekMessagesImap::new)); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder.with_peek_messages(|ctx| { ctx.notmuch.as_ref().and_then(PeekMessagesNotmuch::new) @@ -559,12 +557,12 @@ impl BackendBuilder { } match toml_account_config.get_messages_kind() { - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_get_messages(|ctx| ctx.imap.as_ref().and_then(GetMessagesImap::new)); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder.with_get_messages(|ctx| { ctx.notmuch.as_ref().and_then(GetMessagesNotmuch::new) @@ -586,12 +584,12 @@ impl BackendBuilder { .and_then(CopyMessagesMaildir::new) }); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_copy_messages(|ctx| ctx.imap.as_ref().and_then(CopyMessagesImap::new)); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder.with_copy_messages(|ctx| { ctx.notmuch.as_ref().and_then(CopyMessagesNotmuch::new) @@ -613,12 +611,12 @@ impl BackendBuilder { .and_then(MoveMessagesMaildir::new) }); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendKind::Imap) => { backend_builder = backend_builder .with_move_messages(|ctx| ctx.imap.as_ref().and_then(MoveMessagesImap::new)); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { backend_builder = backend_builder.with_move_messages(|ctx| { ctx.notmuch.as_ref().and_then(MoveMessagesNotmuch::new) @@ -696,7 +694,7 @@ impl Backend { self.backend.account_config.sync_dir()?, )?; } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendKind::Notmuch) => { if let Some(notmuch_config) = &self.toml_account_config.notmuch { id_mapper = IdMapper::new( diff --git a/src/backend/wizard.rs b/src/backend/wizard.rs index 4234889..475dcbf 100644 --- a/src/backend/wizard.rs +++ b/src/backend/wizard.rs @@ -1,26 +1,26 @@ use anyhow::Result; use dialoguer::Select; -#[cfg(feature = "imap-backend")] +#[cfg(feature = "imap")] use crate::imap; -#[cfg(feature = "notmuch-backend")] +#[cfg(feature = "notmuch")] use crate::notmuch; -#[cfg(feature = "smtp-sender")] +#[cfg(feature = "smtp")] use crate::smtp; use crate::{config::wizard::THEME, maildir, sendmail}; use super::{config::BackendConfig, BackendKind}; const DEFAULT_BACKEND_KINDS: &[BackendKind] = &[ - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] BackendKind::Imap, BackendKind::Maildir, - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] BackendKind::Notmuch, ]; const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[ - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] BackendKind::Smtp, BackendKind::Sendmail, ]; @@ -35,11 +35,11 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result Some(maildir::wizard::configure()?), - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(kind) if kind == BackendKind::Imap => { Some(imap::wizard::configure(account_name, email).await?) } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(kind) if kind == BackendKind::Notmuch => Some(notmuch::wizard::configure()?), _ => None, }; @@ -60,7 +60,7 @@ pub(crate) async fn configure_sender( let config = match kind { Some(kind) if kind == BackendKind::Sendmail => Some(sendmail::wizard::configure()?), - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] Some(kind) if kind == BackendKind::Smtp => { Some(smtp::wizard::configure(account_name, email).await?) } diff --git a/src/cache/id_mapper.rs b/src/cache/id_mapper.rs index fb2ff86..7668aa9 100644 --- a/src/cache/id_mapper.rs +++ b/src/cache/id_mapper.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Context, Result}; -use email::account::AccountConfig; +use email::account::config::AccountConfig; use log::{debug, trace}; use std::path::{Path, PathBuf}; diff --git a/src/config/config.rs b/src/config/config.rs index cdc4670..6a166dd 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -7,9 +7,9 @@ use anyhow::{anyhow, Context, Result}; use dialoguer::Confirm; use dirs::{config_dir, home_dir}; use email::{ - account::AccountConfig, + account::config::AccountConfig, config::Config, - email::{EmailHooks, EmailTextPlainFormat}, + email::config::{EmailHooks, EmailTextPlainFormat}, }; use serde::{Deserialize, Serialize}; use std::{ @@ -184,14 +184,14 @@ impl TomlConfig { .ok_or_else(|| anyhow!("cannot find account {name}")), }?; - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] if let Some(imap_config) = toml_account_config.imap.as_mut() { imap_config .auth .replace_undefined_keyring_entries(&account_name); } - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] if let Some(smtp_config) = toml_account_config.smtp.as_mut() { smtp_config .auth @@ -268,18 +268,17 @@ impl TomlConfig { #[cfg(test)] mod tests { use email::{ - account::PasswdConfig, - backend::{BackendConfig, MaildirConfig}, - sender::{SenderConfig, SendmailConfig}, + account::config::passwd::PasswdConfig, maildir::config::MaildirConfig, + sendmail::config::SendmailConfig, }; use secret::Secret; - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] use email::backend::NotmuchConfig; - #[cfg(feature = "imap-backend")] - use email::backend::{ImapAuthConfig, ImapConfig}; - #[cfg(feature = "smtp-sender")] - use email::sender::{SmtpAuthConfig, SmtpConfig}; + #[cfg(feature = "imap")] + use email::imap::config::{ImapAuthConfig, ImapConfig}; + #[cfg(feature = "smtp")] + use email::smtp::config::{SmtpAuthConfig, SmtpConfig}; use std::io::Write; use tempfile::NamedTempFile; @@ -435,7 +434,7 @@ mod tests { .contains("missing field `maildir-root-dir`")); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] #[tokio::test] async fn account_backend_notmuch_missing_db_path_field() { let config = make_config( @@ -588,7 +587,7 @@ mod tests { ) } - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] #[tokio::test] async fn account_smtp_sender_minimum_config() { use email::sender::SenderConfig; @@ -727,7 +726,7 @@ mod tests { ) } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] #[tokio::test] async fn account_backend_notmuch_minimum_config() { let config = make_config( diff --git a/src/config/prelude.rs b/src/config/prelude.rs index df1aaa7..4728a0f 100644 --- a/src/config/prelude.rs +++ b/src/config/prelude.rs @@ -6,18 +6,21 @@ use email::account::GpgConfig; use email::account::PgpConfig; #[cfg(feature = "pgp-native")] use email::account::{NativePgpConfig, NativePgpSecretKey, SignedSecretKey}; -#[cfg(feature = "notmuch-backend")] +#[cfg(feature = "notmuch")] use email::backend::NotmuchConfig; -#[cfg(feature = "imap-backend")] -use email::imap::{ImapAuthConfig, ImapConfig}; -#[cfg(feature = "smtp-sender")] -use email::smtp::{SmtpAuthConfig, SmtpConfig}; +#[cfg(feature = "imap")] +use email::imap::config::{ImapAuthConfig, ImapConfig}; +#[cfg(feature = "smtp")] +use email::smtp::config::{SmtpAuthConfig, SmtpConfig}; use email::{ - account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig}, - email::{EmailHooks, EmailTextPlainFormat}, + account::config::{ + oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes}, + passwd::PasswdConfig, + }, + email::config::{EmailHooks, EmailTextPlainFormat}, folder::sync::FolderSyncStrategy, - maildir::MaildirConfig, - sendmail::SendmailConfig, + maildir::config::MaildirConfig, + sendmail::config::SendmailConfig, }; use keyring::Entry; use process::{Cmd, Pipeline, SingleCmd}; @@ -159,7 +162,7 @@ impl Into for Option { } } -#[cfg(feature = "imap-backend")] +#[cfg(feature = "imap")] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(remote = "ImapConfig", rename_all = "kebab-case")] pub struct ImapConfigDef { @@ -176,7 +179,7 @@ pub struct ImapConfigDef { pub watch_cmds: Option>, } -#[cfg(feature = "imap-backend")] +#[cfg(feature = "imap")] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(remote = "ImapAuthConfig", tag = "auth")] pub enum ImapAuthConfigDef { @@ -303,7 +306,7 @@ pub struct MaildirConfigDef { pub root_dir: PathBuf, } -#[cfg(feature = "notmuch-backend")] +#[cfg(feature = "notmuch")] #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde( remote = "Option", @@ -312,7 +315,7 @@ pub struct MaildirConfigDef { )] pub struct OptionNotmuchConfigDef; -#[cfg(feature = "notmuch-backend")] +#[cfg(feature = "notmuch")] #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] pub struct OptionNotmuchConfig { #[serde(default, skip)] @@ -321,7 +324,7 @@ pub struct OptionNotmuchConfig { inner: NotmuchConfig, } -#[cfg(feature = "notmuch-backend")] +#[cfg(feature = "notmuch")] impl From for Option { fn from(config: OptionNotmuchConfig) -> Option { if config.is_none { @@ -332,7 +335,7 @@ impl From for Option { } } -#[cfg(feature = "notmuch-backend")] +#[cfg(feature = "notmuch")] impl Into for Option { fn into(self) -> OptionNotmuchConfig { match self { @@ -348,7 +351,7 @@ impl Into for Option { } } -#[cfg(feature = "notmuch-backend")] +#[cfg(feature = "notmuch")] #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde(remote = "NotmuchConfig", rename_all = "kebab-case")] pub struct NotmuchConfigDef { @@ -452,7 +455,7 @@ impl Into for Option { } } -#[cfg(feature = "smtp-sender")] +#[cfg(feature = "smtp")] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(remote = "SmtpConfig")] struct SmtpConfigDef { @@ -466,7 +469,7 @@ struct SmtpConfigDef { pub auth: SmtpAuthConfig, } -#[cfg(feature = "smtp-sender")] +#[cfg(feature = "smtp")] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(remote = "SmtpAuthConfig", tag = "auth")] pub enum SmtpAuthConfigDef { diff --git a/src/config/wizard.rs b/src/config/wizard.rs index ffb3a23..8d43942 100644 --- a/src/config/wizard.rs +++ b/src/config/wizard.rs @@ -128,17 +128,17 @@ pub(crate) async fn configure(path: PathBuf) -> Result { }); set_table_dotted(item, "maildir"); - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] { set_table_dotted(item, "imap"); get_table_mut(item, "imap").map(|item| { set_tables_dotted(item, ["passwd", "oauth2"]); }); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] set_table_dotted(item, "notmuch"); set_table_dotted(item, "sendmail"); - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] { set_table_dotted(item, "smtp"); get_table_mut(item, "smtp").map(|item| { diff --git a/src/domain/account/accounts.rs b/src/domain/account/accounts.rs index 59a4579..c567044 100644 --- a/src/domain/account/accounts.rs +++ b/src/domain/account/accounts.rs @@ -42,7 +42,7 @@ impl From> for Accounts { .map(|(name, account)| { let mut backends = String::new(); - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] if account.imap.is_some() { backends.push_str("imap"); } @@ -54,7 +54,7 @@ impl From> for Accounts { backends.push_str("maildir"); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] if account.imap.is_some() { if !backends.is_empty() { backends.push_str(", ") @@ -62,7 +62,7 @@ impl From> for Accounts { backends.push_str("notmuch"); } - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] if account.smtp.is_some() { if !backends.is_empty() { backends.push_str(", ") diff --git a/src/domain/account/config.rs b/src/domain/account/config.rs index 01186ae..5f289fe 100644 --- a/src/domain/account/config.rs +++ b/src/domain/account/config.rs @@ -5,15 +5,15 @@ #[cfg(feature = "pgp")] use email::account::PgpConfig; -#[cfg(feature = "imap-backend")] -use email::imap::ImapConfig; -#[cfg(feature = "smtp-sender")] -use email::smtp::SmtpConfig; +#[cfg(feature = "imap")] +use email::imap::config::ImapConfig; +#[cfg(feature = "smtp")] +use email::smtp::config::SmtpConfig; use email::{ - email::{EmailHooks, EmailTextPlainFormat}, + email::config::{EmailHooks, EmailTextPlainFormat}, folder::sync::FolderSyncStrategy, - maildir::MaildirConfig, - sendmail::SendmailConfig, + maildir::config::MaildirConfig, + sendmail::config::SendmailConfig, }; use serde::{Deserialize, Serialize}; use std::{ @@ -79,7 +79,7 @@ pub struct TomlAccountConfig { pub flag: Option, pub message: Option, - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] #[serde( default, with = "OptionImapConfigDef", @@ -94,7 +94,7 @@ pub struct TomlAccountConfig { )] pub maildir: Option, - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] #[serde( default, with = "OptionNotmuchConfigDef", @@ -102,7 +102,7 @@ pub struct TomlAccountConfig { )] pub notmuch: Option, - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] #[serde( default, with = "OptionSmtpConfigDef", diff --git a/src/domain/account/handlers.rs b/src/domain/account/handlers.rs index 0905563..a37c9dd 100644 --- a/src/domain/account/handlers.rs +++ b/src/domain/account/handlers.rs @@ -4,13 +4,13 @@ use anyhow::Result; use email::account::{ + config::AccountConfig, sync::{AccountSyncBuilder, AccountSyncProgressEvent}, - AccountConfig, }; -#[cfg(feature = "imap-backend")] -use email::imap::ImapAuthConfig; -#[cfg(feature = "smtp-sender")] -use email::smtp::SmtpAuthConfig; +#[cfg(feature = "imap")] +use email::imap::config::ImapAuthConfig; +#[cfg(feature = "smtp")] +use email::smtp::config::SmtpAuthConfig; use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle}; use log::{debug, info, trace, warn}; use once_cell::sync::Lazy; @@ -48,7 +48,7 @@ pub async fn configure(config: &TomlAccountConfig, reset: bool) -> Result<()> { info!("entering the configure account handler"); if reset { - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] if let Some(ref config) = config.imap { let reset = match &config.auth { ImapAuthConfig::Passwd(config) => config.reset(), @@ -60,7 +60,7 @@ pub async fn configure(config: &TomlAccountConfig, reset: bool) -> Result<()> { } } - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] if let Some(ref config) = config.smtp { let reset = match &config.auth { SmtpAuthConfig::Passwd(config) => config.reset(), @@ -78,7 +78,7 @@ pub async fn configure(config: &TomlAccountConfig, reset: bool) -> Result<()> { } } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] if let Some(ref config) = config.imap { match &config.auth { ImapAuthConfig::Passwd(config) => { @@ -92,7 +92,7 @@ pub async fn configure(config: &TomlAccountConfig, reset: bool) -> Result<()> { }?; } - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] if let Some(ref config) = config.smtp { match &config.auth { SmtpAuthConfig::Passwd(config) => { @@ -299,12 +299,13 @@ pub async fn sync( #[cfg(test)] mod tests { - use email::{account::AccountConfig, backend::ImapConfig}; + use email::{account::config::AccountConfig, imap::config::ImapConfig}; use std::{collections::HashMap, fmt::Debug, io}; use termcolor::ColorSpec; use crate::{ account::TomlAccountConfig, + backend::BackendKind, printer::{Print, PrintTable, WriteColor}, }; @@ -378,8 +379,9 @@ mod tests { "account-1".into(), TomlAccountConfig { default: Some(true), - backend: BackendConfig::Imap(ImapConfig::default()), - ..TomlAccountConfig::default() + backend: Some(BackendKind::Imap), + imap: Some(ImapConfig::default()), + ..Default::default() }, )]), ..TomlConfig::default() diff --git a/src/domain/account/wizard.rs b/src/domain/account/wizard.rs index 5096585..c470011 100644 --- a/src/domain/account/wizard.rs +++ b/src/domain/account/wizard.rs @@ -40,12 +40,12 @@ pub(crate) async fn configure() -> Result> { config.maildir = Some(mdir_config); config.backend = Some(BackendKind::Maildir); } - #[cfg(feature = "imap-backend")] + #[cfg(feature = "imap")] Some(BackendConfig::Imap(imap_config)) => { config.imap = Some(imap_config); config.backend = Some(BackendKind::Imap); } - #[cfg(feature = "notmuch-backend")] + #[cfg(feature = "notmuch")] Some(BackendConfig::Notmuch(notmuch_config)) => { config.notmuch = Some(notmuch_config); config.backend = Some(BackendKind::Notmuch); @@ -64,7 +64,7 @@ pub(crate) async fn configure() -> Result> { ..Default::default() }); } - #[cfg(feature = "smtp-sender")] + #[cfg(feature = "smtp")] Some(BackendConfig::Smtp(smtp_config)) => { config.smtp = Some(smtp_config); config.message = Some(MessageConfig { diff --git a/src/domain/email/handlers.rs b/src/domain/email/handlers.rs index fb15a2b..b835ce8 100644 --- a/src/domain/email/handlers.rs +++ b/src/domain/email/handlers.rs @@ -1,10 +1,11 @@ use anyhow::{anyhow, Context, Result}; use atty::Stream; use email::{ - account::AccountConfig, - email::{envelope::Id, template::FilterParts, Flag, Message, MessageBuilder}, + account::config::AccountConfig, envelope::Id, flag::Flag, message::Message, + template::FilterParts, }; use log::{debug, trace}; +use mail_builder::MessageBuilder; use std::{ fs, io::{self, BufRead}, diff --git a/src/domain/envelope/envelopes.rs b/src/domain/envelope/envelopes.rs index 9da59c8..ee1fdd3 100644 --- a/src/domain/envelope/envelopes.rs +++ b/src/domain/envelope/envelopes.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use email::account::AccountConfig; +use email::account::config::AccountConfig; use serde::Serialize; use std::ops; @@ -17,7 +17,7 @@ impl Envelopes { pub fn from_backend( config: &AccountConfig, id_mapper: &IdMapper, - envelopes: email::email::Envelopes, + envelopes: email::envelope::Envelopes, ) -> Result { let envelopes = envelopes .iter() @@ -59,7 +59,7 @@ impl PrintTable for Envelopes { #[cfg(test)] mod tests { use chrono::DateTime; - use email::account::AccountConfig; + use email::account::config::AccountConfig; use std::env; use crate::{Envelopes, IdMapper}; @@ -69,7 +69,7 @@ mod tests { let config = AccountConfig::default(); let id_mapper = IdMapper::Dummy; - let envelopes = email::email::Envelopes::from_iter([email::email::Envelope { + let envelopes = email::envelope::Envelopes::from_iter([email::envelope::Envelope { date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(), ..Default::default() }]); @@ -89,7 +89,7 @@ mod tests { ..AccountConfig::default() }; - let envelopes = email::email::Envelopes::from_iter([email::email::Envelope { + let envelopes = email::envelope::Envelopes::from_iter([email::envelope::Envelope { date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(), ..Default::default() }]); @@ -112,7 +112,7 @@ mod tests { ..AccountConfig::default() }; - let envelopes = email::email::Envelopes::from_iter([email::email::Envelope { + let envelopes = email::envelope::Envelopes::from_iter([email::envelope::Envelope { date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(), ..Default::default() }]); diff --git a/src/domain/flag/args.rs b/src/domain/flag/args.rs index b365f13..bf3d438 100644 --- a/src/domain/flag/args.rs +++ b/src/domain/flag/args.rs @@ -3,7 +3,7 @@ //! This module contains the command matcher, the subcommands and the //! arguments related to the email flag domain. -use ::email::email::{Flag, Flags}; +use ::email::flag::{Flag, Flags}; use anyhow::Result; use clap::{Arg, ArgMatches, Command}; use log::{debug, info}; diff --git a/src/domain/flag/flag.rs b/src/domain/flag/flag.rs index e48f0bb..bf3712a 100644 --- a/src/domain/flag/flag.rs +++ b/src/domain/flag/flag.rs @@ -11,9 +11,9 @@ pub enum Flag { Custom(String), } -impl From<&email::email::Flag> for Flag { - fn from(flag: &email::email::Flag) -> Self { - use email::email::Flag::*; +impl From<&email::flag::Flag> for Flag { + fn from(flag: &email::flag::Flag) -> Self { + use email::flag::Flag::*; match flag { Seen => Flag::Seen, Answered => Flag::Answered, diff --git a/src/domain/flag/flags.rs b/src/domain/flag/flags.rs index bcf9f1f..d0c30d2 100644 --- a/src/domain/flag/flags.rs +++ b/src/domain/flag/flags.rs @@ -14,8 +14,8 @@ impl ops::Deref for Flags { } } -impl From for Flags { - fn from(flags: email::email::Flags) -> Self { +impl From for Flags { + fn from(flags: email::flag::Flags) -> Self { Flags(flags.iter().map(Flag::from).collect()) } } diff --git a/src/domain/flag/handlers.rs b/src/domain/flag/handlers.rs index 5316316..dc3812c 100644 --- a/src/domain/flag/handlers.rs +++ b/src/domain/flag/handlers.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use email::email::Flags; +use email::flag::Flags; use crate::{backend::Backend, printer::Printer}; diff --git a/src/domain/folder/handlers.rs b/src/domain/folder/handlers.rs index 9c6937e..44be274 100644 --- a/src/domain/folder/handlers.rs +++ b/src/domain/folder/handlers.rs @@ -4,7 +4,7 @@ use anyhow::Result; use dialoguer::Confirm; -use email::account::AccountConfig; +use email::account::config::AccountConfig; use std::process; use crate::{ diff --git a/src/domain/tpl/handlers.rs b/src/domain/tpl/handlers.rs index b036370..188bb74 100644 --- a/src/domain/tpl/handlers.rs +++ b/src/domain/tpl/handlers.rs @@ -1,9 +1,6 @@ use anyhow::{anyhow, Result}; use atty::Stream; -use email::{ - account::AccountConfig, - email::{Flag, Message}, -}; +use email::{account::config::AccountConfig, flag::Flag, message::Message}; use mml::MmlCompilerBuilder; use std::io::{stdin, BufRead}; diff --git a/src/imap/wizard.rs b/src/imap/wizard.rs index 00d6017..5efccfa 100644 --- a/src/imap/wizard.rs +++ b/src/imap/wizard.rs @@ -1,8 +1,11 @@ use anyhow::Result; use dialoguer::{Confirm, Input, Password, Select}; use email::{ - account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig}, - imap::{ImapAuthConfig, ImapConfig}, + account::config::{ + oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes}, + passwd::PasswdConfig, + }, + imap::config::{ImapAuthConfig, ImapConfig}, }; use oauth::v2_0::{AuthorizationCodeGrant, Client}; use secret::Secret; diff --git a/src/lib.rs b/src/lib.rs index f37795c..52e83b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,16 +3,16 @@ pub mod cache; pub mod compl; pub mod config; pub mod domain; -#[cfg(feature = "imap-backend")] +#[cfg(feature = "imap")] pub mod imap; pub mod maildir; pub mod man; -#[cfg(feature = "notmuch-backend")] +#[cfg(feature = "notmuch")] pub mod notmuch; pub mod output; pub mod printer; pub mod sendmail; -#[cfg(feature = "smtp-sender")] +#[cfg(feature = "smtp")] pub mod smtp; pub mod ui; diff --git a/src/maildir/wizard.rs b/src/maildir/wizard.rs index 04a1e03..ee7b260 100644 --- a/src/maildir/wizard.rs +++ b/src/maildir/wizard.rs @@ -1,7 +1,7 @@ use anyhow::Result; use dialoguer::Input; use dirs::home_dir; -use email::maildir::MaildirConfig; +use email::maildir::config::MaildirConfig; use crate::{backend::config::BackendConfig, config::wizard::THEME}; diff --git a/src/main.rs b/src/main.rs index 46a9128..ad2e1df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use ::email::account::{sync::AccountSyncBuilder, DEFAULT_INBOX_FOLDER}; +use ::email::account::{config::DEFAULT_INBOX_FOLDER, sync::AccountSyncBuilder}; use anyhow::{anyhow, Context, Result}; use clap::Command; use log::{debug, warn}; @@ -91,7 +91,7 @@ async fn main() -> Result<()> { let mut printer = StdoutPrinter::try_from(&m)?; // FIXME - // #[cfg(feature = "imap-backend")] + // #[cfg(feature = "imap")] // if let BackendConfig::Imap(imap_config) = &account_config.backend { // let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); // match imap::args::matches(&m)? { diff --git a/src/printer/print_table.rs b/src/printer/print_table.rs index 7066221..e371d9b 100644 --- a/src/printer/print_table.rs +++ b/src/printer/print_table.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use email::email::EmailTextPlainFormat; +use email::email::config::EmailTextPlainFormat; use std::io; use termcolor::{self, StandardStream}; diff --git a/src/sendmail/wizard.rs b/src/sendmail/wizard.rs index e2a2896..1906b71 100644 --- a/src/sendmail/wizard.rs +++ b/src/sendmail/wizard.rs @@ -1,6 +1,6 @@ use anyhow::Result; use dialoguer::Input; -use email::sendmail::SendmailConfig; +use email::sendmail::config::SendmailConfig; use crate::{backend::config::BackendConfig, config::wizard::THEME}; diff --git a/src/smtp/wizard.rs b/src/smtp/wizard.rs index 6dab9cf..83d1aa1 100644 --- a/src/smtp/wizard.rs +++ b/src/smtp/wizard.rs @@ -1,8 +1,11 @@ use anyhow::Result; use dialoguer::{Confirm, Input, Select}; use email::{ - account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig}, - smtp::{SmtpAuthConfig, SmtpConfig}, + account::config::{ + oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes}, + passwd::PasswdConfig, + }, + smtp::config::{SmtpAuthConfig, SmtpConfig}, }; use oauth::v2_0::{AuthorizationCodeGrant, Client}; use secret::Secret; diff --git a/src/ui/editor.rs b/src/ui/editor.rs index b65f461..34eef3c 100644 --- a/src/ui/editor.rs +++ b/src/ui/editor.rs @@ -1,7 +1,8 @@ use anyhow::{Context, Result}; use email::{ - account::AccountConfig, - email::{local_draft_path, remove_local_draft, Flag, Flags}, + account::config::AccountConfig, + email::utils::{local_draft_path, remove_local_draft}, + flag::{Flag, Flags}, }; use log::debug; use mml::MmlCompilerBuilder; diff --git a/src/ui/table/table.rs b/src/ui/table/table.rs index adbe2ca..685eb0a 100644 --- a/src/ui/table/table.rs +++ b/src/ui/table/table.rs @@ -5,7 +5,7 @@ //! [builder design pattern]: https://refactoring.guru/design-patterns/builder use anyhow::{Context, Result}; -use email::email::EmailTextPlainFormat; +use email::email::config::EmailTextPlainFormat; use log::trace; use termcolor::{Color, ColorSpec}; use terminal_size::terminal_size; @@ -267,7 +267,7 @@ where #[cfg(test)] mod tests { - use email::email::EmailTextPlainFormat; + use email::email::config::EmailTextPlainFormat; use std::io; use super::*; From 7a10a7fc2510f0192b19e63a07d7e929bf5312ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 5 Dec 2023 15:06:26 +0100 Subject: [PATCH 14/29] reorganize folder and cli structure --- src/{domain => }/account/args.rs | 8 +- src/{domain => }/account/config.rs | 7 +- src/{domain => }/account/handlers.rs | 2 +- .../account/accounts.rs => account/mod.rs} | 59 +- src/{domain => }/account/wizard.rs | 0 src/backend/mod.rs | 2 +- src/cache/id_mapper.rs | 166 ---- src/cache/mod.rs | 168 +++- src/{compl => completion}/args.rs | 2 +- src/{compl => completion}/handlers.rs | 0 src/{compl => completion}/mod.rs | 0 src/config/config.rs | 758 ------------------ src/config/mod.rs | 758 +++++++++++++++++- src/domain/account/account.rs | 54 -- src/domain/account/mod.rs | 10 - src/domain/email/envelope/flag/mod.rs | 1 - src/domain/email/envelope/mod.rs | 2 - src/domain/email/message/mod.rs | 1 - src/domain/email/mod.rs | 4 - src/domain/envelope/envelope.rs | 66 -- src/domain/envelope/mod.rs | 5 - src/domain/flag/flags.rs | 21 - src/domain/flag/mod.rs | 8 - src/domain/folder/folder.rs | 32 - src/domain/folder/folders.rs | 35 - src/domain/folder/mod.rs | 8 - src/domain/mod.rs | 13 - src/email/envelope/args.rs | 90 +++ src/{domain => }/email/envelope/config.rs | 0 src/{domain => email/envelope}/flag/args.rs | 60 +- .../email/envelope/flag/config.rs | 0 .../envelope}/flag/handlers.rs | 0 .../flag.rs => email/envelope/flag/mod.rs} | 22 + src/email/envelope/handlers.rs | 32 + .../envelopes.rs => email/envelope/mod.rs} | 74 +- src/{domain/email => email/message}/args.rs | 231 ++---- src/{domain => }/email/message/config.rs | 0 .../email => email/message}/handlers.rs | 87 +- src/email/message/mod.rs | 4 + .../tpl => email/message/template}/args.rs | 45 +- .../message/template}/handlers.rs | 0 .../tpl => email/message/template}/mod.rs | 0 src/email/mod.rs | 5 + src/{domain => }/folder/args.rs | 6 +- src/{domain => }/folder/config.rs | 0 src/{domain => }/folder/handlers.rs | 9 +- src/folder/mod.rs | 67 ++ src/lib.rs | 12 +- src/main.rs | 279 +++---- 49 files changed, 1555 insertions(+), 1658 deletions(-) rename src/{domain => }/account/args.rs (96%) rename src/{domain => }/account/config.rs (97%) rename src/{domain => }/account/handlers.rs (99%) rename src/{domain/account/accounts.rs => account/mod.rs} (61%) rename src/{domain => }/account/wizard.rs (100%) delete mode 100644 src/cache/id_mapper.rs rename src/{compl => completion}/args.rs (93%) rename src/{compl => completion}/handlers.rs (100%) rename src/{compl => completion}/mod.rs (100%) delete mode 100644 src/config/config.rs delete mode 100644 src/domain/account/account.rs delete mode 100644 src/domain/account/mod.rs delete mode 100644 src/domain/email/envelope/flag/mod.rs delete mode 100644 src/domain/email/envelope/mod.rs delete mode 100644 src/domain/email/message/mod.rs delete mode 100644 src/domain/email/mod.rs delete mode 100644 src/domain/envelope/envelope.rs delete mode 100644 src/domain/envelope/mod.rs delete mode 100644 src/domain/flag/flags.rs delete mode 100644 src/domain/flag/mod.rs delete mode 100644 src/domain/folder/folder.rs delete mode 100644 src/domain/folder/folders.rs delete mode 100644 src/domain/folder/mod.rs delete mode 100644 src/domain/mod.rs create mode 100644 src/email/envelope/args.rs rename src/{domain => }/email/envelope/config.rs (100%) rename src/{domain => email/envelope}/flag/args.rs (58%) rename src/{domain => }/email/envelope/flag/config.rs (100%) rename src/{domain => email/envelope}/flag/handlers.rs (100%) rename src/{domain/flag/flag.rs => email/envelope/flag/mod.rs} (56%) create mode 100644 src/email/envelope/handlers.rs rename src/{domain/envelope/envelopes.rs => email/envelope/mod.rs} (64%) rename src/{domain/email => email/message}/args.rs (57%) rename src/{domain => }/email/message/config.rs (100%) rename src/{domain/email => email/message}/handlers.rs (78%) create mode 100644 src/email/message/mod.rs rename src/{domain/tpl => email/message/template}/args.rs (78%) rename src/{domain/tpl => email/message/template}/handlers.rs (100%) rename src/{domain/tpl => email/message/template}/mod.rs (100%) create mode 100644 src/email/mod.rs rename src/{domain => }/folder/args.rs (98%) rename src/{domain => }/folder/config.rs (100%) rename src/{domain => }/folder/handlers.rs (98%) create mode 100644 src/folder/mod.rs diff --git a/src/domain/account/args.rs b/src/account/args.rs similarity index 96% rename from src/domain/account/args.rs rename to src/account/args.rs index ee3c43b..0917fb1 100644 --- a/src/domain/account/args.rs +++ b/src/account/args.rs @@ -11,7 +11,7 @@ use crate::{folder, ui::table}; const ARG_ACCOUNT: &str = "account"; const ARG_DRY_RUN: &str = "dry-run"; const ARG_RESET: &str = "reset"; -const CMD_ACCOUNTS: &str = "accounts"; +const CMD_ACCOUNT: &str = "account"; const CMD_CONFIGURE: &str = "configure"; const CMD_LIST: &str = "list"; const CMD_SYNC: &str = "sync"; @@ -32,7 +32,7 @@ pub enum Cmd { /// Represents the account command matcher. pub fn matches(m: &ArgMatches) -> Result> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNTS) { + let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNT) { if let Some(m) = m.subcommand_matches(CMD_SYNC) { info!("sync account subcommand matched"); let dry_run = parse_dry_run_arg(m); @@ -73,8 +73,8 @@ pub fn matches(m: &ArgMatches) -> Result> { /// Represents the account subcommand. pub fn subcmd() -> Command { - Command::new(CMD_ACCOUNTS) - .about("Manage accounts") + Command::new(CMD_ACCOUNT) + .about("Subcommand to manage accounts") .subcommands([ Command::new(CMD_LIST) .about("List all accounts from the config file") diff --git a/src/domain/account/config.rs b/src/account/config.rs similarity index 97% rename from src/domain/account/config.rs rename to src/account/config.rs index 5f289fe..a916dbb 100644 --- a/src/domain/account/config.rs +++ b/src/account/config.rs @@ -22,11 +22,8 @@ use std::{ }; use crate::{ - backend::BackendKind, - config::prelude::*, - domain::config::FolderConfig, - email::envelope::{config::EnvelopeConfig, flag::config::FlagConfig}, - message::config::MessageConfig, + backend::BackendKind, config::prelude::*, envelope::config::EnvelopeConfig, + flag::config::FlagConfig, folder::config::FolderConfig, message::config::MessageConfig, }; /// Represents all existing kind of account config. diff --git a/src/domain/account/handlers.rs b/src/account/handlers.rs similarity index 99% rename from src/domain/account/handlers.rs rename to src/account/handlers.rs index a37c9dd..f1f15be 100644 --- a/src/domain/account/handlers.rs +++ b/src/account/handlers.rs @@ -17,13 +17,13 @@ use once_cell::sync::Lazy; use std::{collections::HashMap, sync::Mutex}; use crate::{ + account::Accounts, backend::BackendContextBuilder, config::{ wizard::{prompt_passwd, prompt_secret}, TomlConfig, }, printer::{PrintTableOpts, Printer}, - Accounts, }; use super::TomlAccountConfig; diff --git a/src/domain/account/accounts.rs b/src/account/mod.rs similarity index 61% rename from src/domain/account/accounts.rs rename to src/account/mod.rs index c567044..1c5708d 100644 --- a/src/domain/account/accounts.rs +++ b/src/account/mod.rs @@ -1,19 +1,62 @@ -//! Account module. -//! -//! This module contains the definition of the printable account, -//! which is only used by the "accounts" command to list all available -//! accounts from the config file. +pub mod args; +pub mod config; +pub mod handlers; +pub(crate) mod wizard; use anyhow::Result; use serde::Serialize; -use std::{collections::hash_map::Iter, ops::Deref}; +use std::{collections::hash_map::Iter, fmt, ops::Deref}; use crate::{ printer::{PrintTable, PrintTableOpts, WriteColor}, - ui::Table, + ui::table::{Cell, Row, Table}, }; -use super::{Account, TomlAccountConfig}; +use self::config::TomlAccountConfig; + +/// Represents the printable account. +#[derive(Debug, Default, PartialEq, Eq, Serialize)] +pub struct Account { + /// Represents the account name. + pub name: String, + /// Represents the backend name of the account. + pub backend: String, + /// Represents the default state of the account. + pub default: bool, +} + +impl Account { + pub fn new(name: &str, backend: &str, default: bool) -> Self { + Self { + name: name.into(), + backend: backend.into(), + default, + } + } +} + +impl fmt::Display for Account { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +impl Table for Account { + fn head() -> Row { + Row::new() + .cell(Cell::new("NAME").shrinkable().bold().underline().white()) + .cell(Cell::new("BACKEND").bold().underline().white()) + .cell(Cell::new("DEFAULT").bold().underline().white()) + } + + fn row(&self) -> Row { + let default = if self.default { "yes" } else { "" }; + Row::new() + .cell(Cell::new(&self.name).shrinkable().green()) + .cell(Cell::new(&self.backend).blue()) + .cell(Cell::new(default).white()) + } +} /// Represents the list of printable accounts. #[derive(Debug, Default, Serialize)] diff --git a/src/domain/account/wizard.rs b/src/account/wizard.rs similarity index 100% rename from src/domain/account/wizard.rs rename to src/account/wizard.rs diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 1c9af19..4084e0b 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -46,7 +46,7 @@ use email::{ }; use serde::{Deserialize, Serialize}; -use crate::{account::TomlAccountConfig, Envelopes, IdMapper}; +use crate::{account::config::TomlAccountConfig, cache::IdMapper, envelope::Envelopes}; #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] diff --git a/src/cache/id_mapper.rs b/src/cache/id_mapper.rs deleted file mode 100644 index 7668aa9..0000000 --- a/src/cache/id_mapper.rs +++ /dev/null @@ -1,166 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use email::account::config::AccountConfig; -use log::{debug, trace}; -use std::path::{Path, PathBuf}; - -const ID_MAPPER_DB_FILE_NAME: &str = ".id-mapper.sqlite"; - -#[derive(Debug)] -pub enum IdMapper { - Dummy, - Mapper(String, rusqlite::Connection), -} - -impl IdMapper { - pub fn find_closest_db_path(dir: impl AsRef) -> PathBuf { - let mut db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME); - let mut db_parent_dir = dir.as_ref().parent(); - - while !db_path.is_file() { - match db_parent_dir { - Some(dir) => { - db_path = dir.join(ID_MAPPER_DB_FILE_NAME); - db_parent_dir = dir.parent(); - } - None => { - db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME); - break; - } - } - } - - db_path - } - - pub fn new(account_config: &AccountConfig, folder: &str, db_path: PathBuf) -> Result { - let folder = account_config.get_folder_alias(folder)?; - let digest = md5::compute(account_config.name.clone() + &folder); - let table = format!("id_mapper_{digest:x}"); - debug!("creating id mapper table {table} at {db_path:?}…"); - - let db_path = Self::find_closest_db_path(db_path); - let conn = rusqlite::Connection::open(&db_path) - .with_context(|| format!("cannot open id mapper database at {db_path:?}"))?; - - let query = format!( - "CREATE TABLE IF NOT EXISTS {table} ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - internal_id TEXT UNIQUE - )", - ); - trace!("create table query: {query:#?}"); - - conn.execute(&query, []) - .context("cannot create id mapper table")?; - - Ok(Self::Mapper(table, conn)) - } - - pub fn create_alias(&self, id: I) -> Result - where - I: AsRef, - { - let id = id.as_ref(); - match self { - Self::Dummy => Ok(id.to_owned()), - Self::Mapper(table, conn) => { - debug!("creating alias for id {id}…"); - - let query = format!("INSERT OR IGNORE INTO {} (internal_id) VALUES (?)", table); - trace!("insert query: {query:#?}"); - - conn.execute(&query, [id]) - .with_context(|| format!("cannot create id alias for id {id}"))?; - - let alias = conn.last_insert_rowid().to_string(); - debug!("created alias {alias} for id {id}"); - - Ok(alias) - } - } - } - - pub fn get_or_create_alias(&self, id: I) -> Result - where - I: AsRef, - { - let id = id.as_ref(); - match self { - Self::Dummy => Ok(id.to_owned()), - Self::Mapper(table, conn) => { - debug!("getting alias for id {id}…"); - - let query = format!("SELECT id FROM {} WHERE internal_id = ?", table); - trace!("select query: {query:#?}"); - - let mut stmt = conn - .prepare(&query) - .with_context(|| format!("cannot get alias for id {id}"))?; - let aliases: Vec = stmt - .query_map([id], |row| row.get(0)) - .with_context(|| format!("cannot get alias for id {id}"))? - .collect::>() - .with_context(|| format!("cannot get alias for id {id}"))?; - let alias = match aliases.first() { - Some(alias) => { - debug!("found alias {alias} for id {id}"); - alias.to_string() - } - None => { - debug!("alias not found, creating it…"); - self.create_alias(id)? - } - }; - - Ok(alias) - } - } - } - - pub fn get_id(&self, alias: A) -> Result - where - A: AsRef, - { - let alias = alias.as_ref(); - let alias = alias - .parse::() - .context(format!("cannot parse id mapper alias {alias}"))?; - - match self { - Self::Dummy => Ok(alias.to_string()), - Self::Mapper(table, conn) => { - debug!("getting id from alias {alias}…"); - - let query = format!("SELECT internal_id FROM {} WHERE id = ?", table); - trace!("select query: {query:#?}"); - - let mut stmt = conn - .prepare(&query) - .with_context(|| format!("cannot get id from alias {alias}"))?; - let ids: Vec = stmt - .query_map([alias], |row| row.get(0)) - .with_context(|| format!("cannot get id from alias {alias}"))? - .collect::>() - .with_context(|| format!("cannot get id from alias {alias}"))?; - let id = ids - .first() - .ok_or_else(|| anyhow!("cannot get id from alias {alias}"))? - .to_owned(); - debug!("found id {id} from alias {alias}"); - - Ok(id) - } - } - } - - pub fn get_ids(&self, aliases: I) -> Result> - where - A: AsRef, - I: IntoIterator, - { - aliases - .into_iter() - .map(|alias| self.get_id(alias)) - .collect() - } -} diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 2dae5fe..04ac835 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1,4 +1,168 @@ pub mod args; -mod id_mapper; -pub use id_mapper::IdMapper; +use anyhow::{anyhow, Context, Result}; +use email::account::config::AccountConfig; +use log::{debug, trace}; +use std::path::{Path, PathBuf}; + +const ID_MAPPER_DB_FILE_NAME: &str = ".id-mapper.sqlite"; + +#[derive(Debug)] +pub enum IdMapper { + Dummy, + Mapper(String, rusqlite::Connection), +} + +impl IdMapper { + pub fn find_closest_db_path(dir: impl AsRef) -> PathBuf { + let mut db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME); + let mut db_parent_dir = dir.as_ref().parent(); + + while !db_path.is_file() { + match db_parent_dir { + Some(dir) => { + db_path = dir.join(ID_MAPPER_DB_FILE_NAME); + db_parent_dir = dir.parent(); + } + None => { + db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME); + break; + } + } + } + + db_path + } + + pub fn new(account_config: &AccountConfig, folder: &str, db_path: PathBuf) -> Result { + let folder = account_config.get_folder_alias(folder)?; + let digest = md5::compute(account_config.name.clone() + &folder); + let table = format!("id_mapper_{digest:x}"); + debug!("creating id mapper table {table} at {db_path:?}…"); + + let db_path = Self::find_closest_db_path(db_path); + let conn = rusqlite::Connection::open(&db_path) + .with_context(|| format!("cannot open id mapper database at {db_path:?}"))?; + + let query = format!( + "CREATE TABLE IF NOT EXISTS {table} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + internal_id TEXT UNIQUE + )", + ); + trace!("create table query: {query:#?}"); + + conn.execute(&query, []) + .context("cannot create id mapper table")?; + + Ok(Self::Mapper(table, conn)) + } + + pub fn create_alias(&self, id: I) -> Result + where + I: AsRef, + { + let id = id.as_ref(); + match self { + Self::Dummy => Ok(id.to_owned()), + Self::Mapper(table, conn) => { + debug!("creating alias for id {id}…"); + + let query = format!("INSERT OR IGNORE INTO {} (internal_id) VALUES (?)", table); + trace!("insert query: {query:#?}"); + + conn.execute(&query, [id]) + .with_context(|| format!("cannot create id alias for id {id}"))?; + + let alias = conn.last_insert_rowid().to_string(); + debug!("created alias {alias} for id {id}"); + + Ok(alias) + } + } + } + + pub fn get_or_create_alias(&self, id: I) -> Result + where + I: AsRef, + { + let id = id.as_ref(); + match self { + Self::Dummy => Ok(id.to_owned()), + Self::Mapper(table, conn) => { + debug!("getting alias for id {id}…"); + + let query = format!("SELECT id FROM {} WHERE internal_id = ?", table); + trace!("select query: {query:#?}"); + + let mut stmt = conn + .prepare(&query) + .with_context(|| format!("cannot get alias for id {id}"))?; + let aliases: Vec = stmt + .query_map([id], |row| row.get(0)) + .with_context(|| format!("cannot get alias for id {id}"))? + .collect::>() + .with_context(|| format!("cannot get alias for id {id}"))?; + let alias = match aliases.first() { + Some(alias) => { + debug!("found alias {alias} for id {id}"); + alias.to_string() + } + None => { + debug!("alias not found, creating it…"); + self.create_alias(id)? + } + }; + + Ok(alias) + } + } + } + + pub fn get_id(&self, alias: A) -> Result + where + A: AsRef, + { + let alias = alias.as_ref(); + let alias = alias + .parse::() + .context(format!("cannot parse id mapper alias {alias}"))?; + + match self { + Self::Dummy => Ok(alias.to_string()), + Self::Mapper(table, conn) => { + debug!("getting id from alias {alias}…"); + + let query = format!("SELECT internal_id FROM {} WHERE id = ?", table); + trace!("select query: {query:#?}"); + + let mut stmt = conn + .prepare(&query) + .with_context(|| format!("cannot get id from alias {alias}"))?; + let ids: Vec = stmt + .query_map([alias], |row| row.get(0)) + .with_context(|| format!("cannot get id from alias {alias}"))? + .collect::>() + .with_context(|| format!("cannot get id from alias {alias}"))?; + let id = ids + .first() + .ok_or_else(|| anyhow!("cannot get id from alias {alias}"))? + .to_owned(); + debug!("found id {id} from alias {alias}"); + + Ok(id) + } + } + } + + pub fn get_ids(&self, aliases: I) -> Result> + where + A: AsRef, + I: IntoIterator, + { + aliases + .into_iter() + .map(|alias| self.get_id(alias)) + .collect() + } +} diff --git a/src/compl/args.rs b/src/completion/args.rs similarity index 93% rename from src/compl/args.rs rename to src/completion/args.rs index 137d31f..00c79e9 100644 --- a/src/compl/args.rs +++ b/src/completion/args.rs @@ -32,7 +32,7 @@ pub fn matches(m: &ArgMatches) -> Result> { /// Completion subcommands. pub fn subcmd() -> Command { Command::new(CMD_COMPLETION) - .about("Generates the completion script for the given shell") + .about("Generate the completion script for the given shell") .args(&[Arg::new(ARG_SHELL) .value_parser(value_parser!(Shell)) .required(true)]) diff --git a/src/compl/handlers.rs b/src/completion/handlers.rs similarity index 100% rename from src/compl/handlers.rs rename to src/completion/handlers.rs diff --git a/src/compl/mod.rs b/src/completion/mod.rs similarity index 100% rename from src/compl/mod.rs rename to src/completion/mod.rs diff --git a/src/config/config.rs b/src/config/config.rs deleted file mode 100644 index 6a166dd..0000000 --- a/src/config/config.rs +++ /dev/null @@ -1,758 +0,0 @@ -//! Deserialized config module. -//! -//! This module contains the raw deserialized representation of the -//! user configuration file. - -use anyhow::{anyhow, Context, Result}; -use dialoguer::Confirm; -use dirs::{config_dir, home_dir}; -use email::{ - account::config::AccountConfig, - config::Config, - email::config::{EmailHooks, EmailTextPlainFormat}, -}; -use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - fs, - path::{Path, PathBuf}, - process, -}; -use toml; - -use crate::{ - account::TomlAccountConfig, - backend::BackendKind, - config::{prelude::*, wizard}, - wizard_prompt, wizard_warn, -}; - -/// Represents the user config file. -#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct TomlConfig { - #[serde(alias = "name")] - pub display_name: Option, - pub signature_delim: Option, - pub signature: Option, - pub downloads_dir: Option, - - pub folder_listing_page_size: Option, - pub folder_aliases: Option>, - - pub email_listing_page_size: Option, - pub email_listing_datetime_fmt: Option, - pub email_listing_datetime_local_tz: Option, - pub email_reading_headers: Option>, - #[serde( - default, - with = "OptionEmailTextPlainFormatDef", - skip_serializing_if = "Option::is_none" - )] - pub email_reading_format: Option, - pub email_writing_headers: Option>, - pub email_sending_save_copy: Option, - #[serde( - default, - with = "OptionEmailHooksDef", - skip_serializing_if = "Option::is_none" - )] - pub email_hooks: Option, - - #[serde(flatten)] - pub accounts: HashMap, -} - -impl TomlConfig { - /// Read and parse the TOML configuration at the given path. - /// - /// Returns an error if the configuration file cannot be read or - /// if its content cannot be parsed. - fn from_path(path: &Path) -> Result { - let content = - fs::read_to_string(path).context(format!("cannot read config file at {path:?}"))?; - toml::from_str(&content).context(format!("cannot parse config file at {path:?}")) - } - - /// Create and save a TOML configuration using the wizard. - /// - /// If the user accepts the confirmation, the wizard starts and - /// help him to create his configuration file. Otherwise the - /// program stops. - /// - /// NOTE: the wizard can only be used with interactive shells. - async fn from_wizard(path: PathBuf) -> Result { - wizard_warn!("Cannot find existing configuration at {path:?}."); - - let confirm = Confirm::new() - .with_prompt(wizard_prompt!( - "Would you like to create one with the wizard?" - )) - .default(true) - .interact_opt()? - .unwrap_or_default(); - - if !confirm { - process::exit(0); - } - - wizard::configure(path).await - } - - /// Read and parse the TOML configuration from default paths. - pub async fn from_default_paths() -> Result { - match Self::first_valid_default_path() { - Some(path) => Self::from_path(&path), - None => Self::from_wizard(Self::default_path()?).await, - } - } - - /// Read and parse the TOML configuration at the optional given - /// path. - /// - /// If the given path exists, then read and parse the TOML - /// configuration from it. - /// - /// If the given path does not exist, then create it using the - /// wizard. - /// - /// If no path is given, then either read and parse the TOML - /// configuration at the first valid default path, otherwise - /// create it using the wizard. wizard. - pub async fn from_some_path_or_default(path: Option>) -> Result { - match path.map(Into::into) { - Some(ref path) if path.exists() => Self::from_path(path), - Some(path) => Self::from_wizard(path).await, - None => match Self::first_valid_default_path() { - Some(path) => Self::from_path(&path), - None => Self::from_wizard(Self::default_path()?).await, - }, - } - } - - /// Get the default configuration path. - /// - /// Returns an error if the XDG configuration directory cannot be - /// found. - pub fn default_path() -> Result { - Ok(config_dir() - .ok_or(anyhow!("cannot get XDG config directory"))? - .join("himalaya") - .join("config.toml")) - } - - /// Get the first default configuration path that points to a - /// valid file. - /// - /// Tries paths in this order: - /// - /// - `$XDG_CONFIG_DIR/himalaya/config.toml` (or equivalent to - /// `$XDG_CONFIG_DIR` in other OSes.) - /// - `$HOME/.config/himalaya/config.toml` - /// - `$HOME/.himalayarc` - pub fn first_valid_default_path() -> Option { - Self::default_path() - .ok() - .filter(|p| p.exists()) - .or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml"))) - .filter(|p| p.exists()) - .or_else(|| home_dir().map(|p| p.join(".himalayarc"))) - .filter(|p| p.exists()) - } - - /// Build account configurations from a given account name. - pub fn into_account_configs( - self, - account_name: Option<&str>, - disable_cache: bool, - ) -> Result<(TomlAccountConfig, AccountConfig)> { - let (account_name, mut toml_account_config) = match account_name { - Some("default") | Some("") | None => self - .accounts - .iter() - .find_map(|(name, account)| { - account - .default - .filter(|default| *default == true) - .map(|_| (name.to_owned(), account.clone())) - }) - .ok_or_else(|| anyhow!("cannot find default account")), - Some(name) => self - .accounts - .get(name) - .map(|account| (name.to_owned(), account.clone())) - .ok_or_else(|| anyhow!("cannot find account {name}")), - }?; - - #[cfg(feature = "imap")] - if let Some(imap_config) = toml_account_config.imap.as_mut() { - imap_config - .auth - .replace_undefined_keyring_entries(&account_name); - } - - #[cfg(feature = "smtp")] - if let Some(smtp_config) = toml_account_config.smtp.as_mut() { - smtp_config - .auth - .replace_undefined_keyring_entries(&account_name); - } - - if let Some(true) = toml_account_config.sync { - if !disable_cache { - toml_account_config.backend = Some(BackendKind::MaildirForSync); - } - } - - let config = Config { - display_name: self.display_name, - signature_delim: self.signature_delim, - signature: self.signature, - downloads_dir: self.downloads_dir, - - folder_listing_page_size: self.folder_listing_page_size, - folder_aliases: self.folder_aliases, - - email_listing_page_size: self.email_listing_page_size, - email_listing_datetime_fmt: self.email_listing_datetime_fmt, - email_listing_datetime_local_tz: self.email_listing_datetime_local_tz, - email_reading_headers: self.email_reading_headers, - email_reading_format: self.email_reading_format, - email_writing_headers: self.email_writing_headers, - email_sending_save_copy: self.email_sending_save_copy, - email_hooks: self.email_hooks, - - accounts: HashMap::from_iter(self.accounts.clone().into_iter().map( - |(name, config)| { - ( - name.clone(), - AccountConfig { - name, - email: config.email, - display_name: config.display_name, - signature_delim: config.signature_delim, - signature: config.signature, - downloads_dir: config.downloads_dir, - - folder_listing_page_size: config.folder_listing_page_size, - folder_aliases: config.folder_aliases.unwrap_or_default(), - - email_listing_page_size: config.email_listing_page_size, - email_listing_datetime_fmt: config.email_listing_datetime_fmt, - email_listing_datetime_local_tz: config.email_listing_datetime_local_tz, - - email_reading_headers: config.email_reading_headers, - email_reading_format: config.email_reading_format.unwrap_or_default(), - email_writing_headers: config.email_writing_headers, - email_sending_save_copy: config.email_sending_save_copy, - email_hooks: config.email_hooks.unwrap_or_default(), - - sync: config.sync.unwrap_or_default(), - sync_dir: config.sync_dir, - sync_folders_strategy: config.sync_folders_strategy.unwrap_or_default(), - - #[cfg(feature = "pgp")] - pgp: config.pgp, - }, - ) - }, - )), - }; - - let account_config = config.account(&account_name)?; - - Ok((toml_account_config, account_config)) - } -} - -#[cfg(test)] -mod tests { - use email::{ - account::config::passwd::PasswdConfig, maildir::config::MaildirConfig, - sendmail::config::SendmailConfig, - }; - use secret::Secret; - - #[cfg(feature = "notmuch")] - use email::backend::NotmuchConfig; - #[cfg(feature = "imap")] - use email::imap::config::{ImapAuthConfig, ImapConfig}; - #[cfg(feature = "smtp")] - use email::smtp::config::{SmtpAuthConfig, SmtpConfig}; - - use std::io::Write; - use tempfile::NamedTempFile; - - use super::*; - - async fn make_config(config: &str) -> Result { - let mut file = NamedTempFile::new().unwrap(); - write!(file, "{}", config).unwrap(); - TomlConfig::from_some_path_or_default(file.into_temp_path().to_str()).await - } - - #[tokio::test] - async fn empty_config() { - let config = make_config("").await; - - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "config file must contain at least one account" - ); - } - - #[tokio::test] - async fn account_missing_email_field() { - let config = make_config("[account]").await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `email`")); - } - - #[tokio::test] - async fn account_missing_backend_field() { - let config = make_config( - "[account] - email = \"test@localhost\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `backend`")); - } - - #[tokio::test] - async fn account_invalid_backend_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"bad\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("unknown variant `bad`")); - } - - #[tokio::test] - async fn imap_account_missing_host_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"imap\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `imap-host`")); - } - - #[tokio::test] - async fn account_backend_imap_missing_port_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"imap\" - imap-host = \"localhost\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `imap-port`")); - } - - #[tokio::test] - async fn account_backend_imap_missing_login_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"imap\" - imap-host = \"localhost\" - imap-port = 993", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `imap-login`")); - } - - #[tokio::test] - async fn account_backend_imap_missing_passwd_cmd_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"imap\" - imap-host = \"localhost\" - imap-port = 993 - imap-login = \"login\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `imap-auth`")); - } - - #[tokio::test] - async fn account_backend_maildir_missing_root_dir_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"maildir\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `maildir-root-dir`")); - } - - #[cfg(feature = "notmuch")] - #[tokio::test] - async fn account_backend_notmuch_missing_db_path_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"notmuch\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `notmuch-db-path`")); - } - - #[tokio::test] - async fn account_missing_sender_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `sender`")); - } - - #[tokio::test] - async fn account_invalid_sender_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"bad\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("unknown variant `bad`, expected one of `none`, `smtp`, `sendmail`"),); - } - - #[tokio::test] - async fn account_smtp_sender_missing_host_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"smtp\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `smtp-host`")); - } - - #[tokio::test] - async fn account_smtp_sender_missing_port_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"smtp\" - smtp-host = \"localhost\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `smtp-port`")); - } - - #[tokio::test] - async fn account_smtp_sender_missing_login_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"smtp\" - smtp-host = \"localhost\" - smtp-port = 25", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `smtp-login`")); - } - - #[tokio::test] - async fn account_smtp_sender_missing_auth_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"smtp\" - smtp-host = \"localhost\" - smtp-port = 25 - smtp-login = \"login\"", - ) - .await; - - assert!(config - .unwrap_err() - .root_cause() - .to_string() - .contains("missing field `smtp-auth`")); - } - - #[tokio::test] - async fn account_sendmail_sender_missing_cmd_field() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"sendmail\"", - ) - .await; - - assert_eq!( - config.unwrap(), - TomlConfig { - accounts: HashMap::from_iter([( - "account".into(), - TomlAccountConfig { - email: "test@localhost".into(), - sender: SenderConfig::Sendmail(SendmailConfig { - cmd: "/usr/sbin/sendmail".into() - }), - ..TomlAccountConfig::default() - } - )]), - ..TomlConfig::default() - } - ) - } - - #[cfg(feature = "smtp")] - #[tokio::test] - async fn account_smtp_sender_minimum_config() { - use email::sender::SenderConfig; - - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"smtp\" - smtp-host = \"localhost\" - smtp-port = 25 - smtp-login = \"login\" - smtp-auth = \"passwd\" - smtp-passwd = { cmd = \"echo password\" }", - ) - .await; - - assert_eq!( - config.unwrap(), - TomlConfig { - accounts: HashMap::from_iter([( - "account".into(), - TomlAccountConfig { - email: "test@localhost".into(), - sender: SenderConfig::Smtp(SmtpConfig { - host: "localhost".into(), - port: 25, - login: "login".into(), - auth: SmtpAuthConfig::Passwd(PasswdConfig { - passwd: Secret::new_cmd(String::from("echo password")) - }), - ..SmtpConfig::default() - }), - ..TomlAccountConfig::default() - } - )]), - ..TomlConfig::default() - } - ) - } - - #[tokio::test] - async fn account_sendmail_sender_minimum_config() { - let config = make_config( - "[account] - email = \"test@localhost\" - backend = \"none\" - sender = \"sendmail\" - sendmail-cmd = \"echo send\"", - ) - .await; - - assert_eq!( - config.unwrap(), - TomlConfig { - accounts: HashMap::from_iter([( - "account".into(), - TomlAccountConfig { - email: "test@localhost".into(), - sender: SenderConfig::Sendmail(SendmailConfig { - cmd: Cmd::from("echo send") - }), - ..TomlAccountConfig::default() - } - )]), - ..TomlConfig::default() - } - ) - } - - #[tokio::test] - async fn account_backend_imap_minimum_config() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"imap\" - imap-host = \"localhost\" - imap-port = 993 - imap-login = \"login\" - imap-auth = \"passwd\" - imap-passwd = { cmd = \"echo password\" }", - ) - .await; - - assert_eq!( - config.unwrap(), - TomlConfig { - accounts: HashMap::from_iter([( - "account".into(), - TomlAccountConfig { - email: "test@localhost".into(), - backend: BackendConfig::Imap(ImapConfig { - host: "localhost".into(), - port: 993, - login: "login".into(), - auth: ImapAuthConfig::Passwd(PasswdConfig { - passwd: Secret::new_cmd(String::from("echo password")) - }), - ..ImapConfig::default() - }), - ..TomlAccountConfig::default() - } - )]), - ..TomlConfig::default() - } - ) - } - - #[tokio::test] - async fn account_backend_maildir_minimum_config() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"maildir\" - maildir-root-dir = \"/tmp/maildir\"", - ) - .await; - - assert_eq!( - config.unwrap(), - TomlConfig { - accounts: HashMap::from_iter([( - "account".into(), - TomlAccountConfig { - email: "test@localhost".into(), - backend: BackendConfig::Maildir(MaildirConfig { - root_dir: "/tmp/maildir".into(), - }), - ..TomlAccountConfig::default() - } - )]), - ..TomlConfig::default() - } - ) - } - - #[cfg(feature = "notmuch")] - #[tokio::test] - async fn account_backend_notmuch_minimum_config() { - let config = make_config( - "[account] - email = \"test@localhost\" - sender = \"none\" - backend = \"notmuch\" - notmuch-db-path = \"/tmp/notmuch.db\"", - ) - .await; - - assert_eq!( - config.unwrap(), - TomlConfig { - accounts: HashMap::from_iter([( - "account".into(), - TomlAccountConfig { - email: "test@localhost".into(), - backend: BackendConfig::Notmuch(NotmuchConfig { - db_path: "/tmp/notmuch.db".into(), - }), - ..TomlAccountConfig::default() - } - )]), - ..TomlConfig::default() - } - ); - } -} diff --git a/src/config/mod.rs b/src/config/mod.rs index feafee6..e54f0df 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,6 +1,760 @@ +//! Deserialized config module. +//! +//! This module contains the raw deserialized representation of the +//! user configuration file. + pub mod args; -pub mod config; pub mod prelude; pub mod wizard; -pub use config::*; +use anyhow::{anyhow, Context, Result}; +use dialoguer::Confirm; +use dirs::{config_dir, home_dir}; +use email::{ + account::config::AccountConfig, + config::Config, + email::config::{EmailHooks, EmailTextPlainFormat}, +}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + fs, + path::{Path, PathBuf}, + process, +}; +use toml; + +use crate::{ + account::config::TomlAccountConfig, backend::BackendKind, config::prelude::*, wizard_prompt, + wizard_warn, +}; + +/// Represents the user config file. +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct TomlConfig { + #[serde(alias = "name")] + pub display_name: Option, + pub signature_delim: Option, + pub signature: Option, + pub downloads_dir: Option, + + pub folder_listing_page_size: Option, + pub folder_aliases: Option>, + + pub email_listing_page_size: Option, + pub email_listing_datetime_fmt: Option, + pub email_listing_datetime_local_tz: Option, + pub email_reading_headers: Option>, + #[serde( + default, + with = "OptionEmailTextPlainFormatDef", + skip_serializing_if = "Option::is_none" + )] + pub email_reading_format: Option, + pub email_writing_headers: Option>, + pub email_sending_save_copy: Option, + #[serde( + default, + with = "OptionEmailHooksDef", + skip_serializing_if = "Option::is_none" + )] + pub email_hooks: Option, + + #[serde(flatten)] + pub accounts: HashMap, +} + +impl TomlConfig { + /// Read and parse the TOML configuration at the given path. + /// + /// Returns an error if the configuration file cannot be read or + /// if its content cannot be parsed. + fn from_path(path: &Path) -> Result { + let content = + fs::read_to_string(path).context(format!("cannot read config file at {path:?}"))?; + toml::from_str(&content).context(format!("cannot parse config file at {path:?}")) + } + + /// Create and save a TOML configuration using the wizard. + /// + /// If the user accepts the confirmation, the wizard starts and + /// help him to create his configuration file. Otherwise the + /// program stops. + /// + /// NOTE: the wizard can only be used with interactive shells. + async fn from_wizard(path: PathBuf) -> Result { + wizard_warn!("Cannot find existing configuration at {path:?}."); + + let confirm = Confirm::new() + .with_prompt(wizard_prompt!( + "Would you like to create one with the wizard?" + )) + .default(true) + .interact_opt()? + .unwrap_or_default(); + + if !confirm { + process::exit(0); + } + + wizard::configure(path).await + } + + /// Read and parse the TOML configuration from default paths. + pub async fn from_default_paths() -> Result { + match Self::first_valid_default_path() { + Some(path) => Self::from_path(&path), + None => Self::from_wizard(Self::default_path()?).await, + } + } + + /// Read and parse the TOML configuration at the optional given + /// path. + /// + /// If the given path exists, then read and parse the TOML + /// configuration from it. + /// + /// If the given path does not exist, then create it using the + /// wizard. + /// + /// If no path is given, then either read and parse the TOML + /// configuration at the first valid default path, otherwise + /// create it using the wizard. wizard. + pub async fn from_some_path_or_default(path: Option>) -> Result { + match path.map(Into::into) { + Some(ref path) if path.exists() => Self::from_path(path), + Some(path) => Self::from_wizard(path).await, + None => match Self::first_valid_default_path() { + Some(path) => Self::from_path(&path), + None => Self::from_wizard(Self::default_path()?).await, + }, + } + } + + /// Get the default configuration path. + /// + /// Returns an error if the XDG configuration directory cannot be + /// found. + pub fn default_path() -> Result { + Ok(config_dir() + .ok_or(anyhow!("cannot get XDG config directory"))? + .join("himalaya") + .join("config.toml")) + } + + /// Get the first default configuration path that points to a + /// valid file. + /// + /// Tries paths in this order: + /// + /// - `$XDG_CONFIG_DIR/himalaya/config.toml` (or equivalent to + /// `$XDG_CONFIG_DIR` in other OSes.) + /// - `$HOME/.config/himalaya/config.toml` + /// - `$HOME/.himalayarc` + pub fn first_valid_default_path() -> Option { + Self::default_path() + .ok() + .filter(|p| p.exists()) + .or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml"))) + .filter(|p| p.exists()) + .or_else(|| home_dir().map(|p| p.join(".himalayarc"))) + .filter(|p| p.exists()) + } + + /// Build account configurations from a given account name. + pub fn into_account_configs( + self, + account_name: Option<&str>, + disable_cache: bool, + ) -> Result<(TomlAccountConfig, AccountConfig)> { + let (account_name, mut toml_account_config) = match account_name { + Some("default") | Some("") | None => self + .accounts + .iter() + .find_map(|(name, account)| { + account + .default + .filter(|default| *default == true) + .map(|_| (name.to_owned(), account.clone())) + }) + .ok_or_else(|| anyhow!("cannot find default account")), + Some(name) => self + .accounts + .get(name) + .map(|account| (name.to_owned(), account.clone())) + .ok_or_else(|| anyhow!("cannot find account {name}")), + }?; + + #[cfg(feature = "imap")] + if let Some(imap_config) = toml_account_config.imap.as_mut() { + imap_config + .auth + .replace_undefined_keyring_entries(&account_name); + } + + #[cfg(feature = "smtp")] + if let Some(smtp_config) = toml_account_config.smtp.as_mut() { + smtp_config + .auth + .replace_undefined_keyring_entries(&account_name); + } + + if let Some(true) = toml_account_config.sync { + if !disable_cache { + toml_account_config.backend = Some(BackendKind::MaildirForSync); + } + } + + let config = Config { + display_name: self.display_name, + signature_delim: self.signature_delim, + signature: self.signature, + downloads_dir: self.downloads_dir, + + folder_listing_page_size: self.folder_listing_page_size, + folder_aliases: self.folder_aliases, + + email_listing_page_size: self.email_listing_page_size, + email_listing_datetime_fmt: self.email_listing_datetime_fmt, + email_listing_datetime_local_tz: self.email_listing_datetime_local_tz, + email_reading_headers: self.email_reading_headers, + email_reading_format: self.email_reading_format, + email_writing_headers: self.email_writing_headers, + email_sending_save_copy: self.email_sending_save_copy, + email_hooks: self.email_hooks, + + accounts: HashMap::from_iter(self.accounts.clone().into_iter().map( + |(name, config)| { + ( + name.clone(), + AccountConfig { + name, + email: config.email, + display_name: config.display_name, + signature_delim: config.signature_delim, + signature: config.signature, + downloads_dir: config.downloads_dir, + + folder_listing_page_size: config.folder_listing_page_size, + folder_aliases: config.folder_aliases.unwrap_or_default(), + + email_listing_page_size: config.email_listing_page_size, + email_listing_datetime_fmt: config.email_listing_datetime_fmt, + email_listing_datetime_local_tz: config.email_listing_datetime_local_tz, + + email_reading_headers: config.email_reading_headers, + email_reading_format: config.email_reading_format.unwrap_or_default(), + email_writing_headers: config.email_writing_headers, + email_sending_save_copy: config.email_sending_save_copy, + email_hooks: config.email_hooks.unwrap_or_default(), + + sync: config.sync.unwrap_or_default(), + sync_dir: config.sync_dir, + sync_folders_strategy: config.sync_folders_strategy.unwrap_or_default(), + + #[cfg(feature = "pgp")] + pgp: config.pgp, + }, + ) + }, + )), + }; + + let account_config = config.account(&account_name)?; + + Ok((toml_account_config, account_config)) + } +} + +#[cfg(test)] +mod tests { + use email::{ + account::config::passwd::PasswdConfig, maildir::config::MaildirConfig, + sendmail::config::SendmailConfig, + }; + use secret::Secret; + + #[cfg(feature = "notmuch")] + use email::backend::NotmuchConfig; + #[cfg(feature = "imap")] + use email::imap::config::{ImapAuthConfig, ImapConfig}; + #[cfg(feature = "smtp")] + use email::smtp::config::{SmtpAuthConfig, SmtpConfig}; + + use std::io::Write; + use tempfile::NamedTempFile; + + use super::*; + + async fn make_config(config: &str) -> Result { + let mut file = NamedTempFile::new().unwrap(); + write!(file, "{}", config).unwrap(); + TomlConfig::from_some_path_or_default(file.into_temp_path().to_str()).await + } + + #[tokio::test] + async fn empty_config() { + let config = make_config("").await; + + assert_eq!( + config.unwrap_err().root_cause().to_string(), + "config file must contain at least one account" + ); + } + + #[tokio::test] + async fn account_missing_email_field() { + let config = make_config("[account]").await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `email`")); + } + + #[tokio::test] + async fn account_missing_backend_field() { + let config = make_config( + "[account] + email = \"test@localhost\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `backend`")); + } + + #[tokio::test] + async fn account_invalid_backend_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"bad\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("unknown variant `bad`")); + } + + #[tokio::test] + async fn imap_account_missing_host_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"imap\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `imap-host`")); + } + + #[tokio::test] + async fn account_backend_imap_missing_port_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"imap\" + imap-host = \"localhost\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `imap-port`")); + } + + #[tokio::test] + async fn account_backend_imap_missing_login_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"imap\" + imap-host = \"localhost\" + imap-port = 993", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `imap-login`")); + } + + #[tokio::test] + async fn account_backend_imap_missing_passwd_cmd_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"imap\" + imap-host = \"localhost\" + imap-port = 993 + imap-login = \"login\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `imap-auth`")); + } + + #[tokio::test] + async fn account_backend_maildir_missing_root_dir_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"maildir\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `maildir-root-dir`")); + } + + #[cfg(feature = "notmuch")] + #[tokio::test] + async fn account_backend_notmuch_missing_db_path_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"notmuch\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `notmuch-db-path`")); + } + + #[tokio::test] + async fn account_missing_sender_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `sender`")); + } + + #[tokio::test] + async fn account_invalid_sender_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"bad\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("unknown variant `bad`, expected one of `none`, `smtp`, `sendmail`"),); + } + + #[tokio::test] + async fn account_smtp_sender_missing_host_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"smtp\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `smtp-host`")); + } + + #[tokio::test] + async fn account_smtp_sender_missing_port_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"smtp\" + smtp-host = \"localhost\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `smtp-port`")); + } + + #[tokio::test] + async fn account_smtp_sender_missing_login_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"smtp\" + smtp-host = \"localhost\" + smtp-port = 25", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `smtp-login`")); + } + + #[tokio::test] + async fn account_smtp_sender_missing_auth_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"smtp\" + smtp-host = \"localhost\" + smtp-port = 25 + smtp-login = \"login\"", + ) + .await; + + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `smtp-auth`")); + } + + #[tokio::test] + async fn account_sendmail_sender_missing_cmd_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"sendmail\"", + ) + .await; + + assert_eq!( + config.unwrap(), + TomlConfig { + accounts: HashMap::from_iter([( + "account".into(), + TomlAccountConfig { + email: "test@localhost".into(), + sender: SenderConfig::Sendmail(SendmailConfig { + cmd: "/usr/sbin/sendmail".into() + }), + ..TomlAccountConfig::default() + } + )]), + ..TomlConfig::default() + } + ) + } + + #[cfg(feature = "smtp")] + #[tokio::test] + async fn account_smtp_sender_minimum_config() { + use email::sender::SenderConfig; + + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"smtp\" + smtp-host = \"localhost\" + smtp-port = 25 + smtp-login = \"login\" + smtp-auth = \"passwd\" + smtp-passwd = { cmd = \"echo password\" }", + ) + .await; + + assert_eq!( + config.unwrap(), + TomlConfig { + accounts: HashMap::from_iter([( + "account".into(), + TomlAccountConfig { + email: "test@localhost".into(), + sender: SenderConfig::Smtp(SmtpConfig { + host: "localhost".into(), + port: 25, + login: "login".into(), + auth: SmtpAuthConfig::Passwd(PasswdConfig { + passwd: Secret::new_cmd(String::from("echo password")) + }), + ..SmtpConfig::default() + }), + ..TomlAccountConfig::default() + } + )]), + ..TomlConfig::default() + } + ) + } + + #[tokio::test] + async fn account_sendmail_sender_minimum_config() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"sendmail\" + sendmail-cmd = \"echo send\"", + ) + .await; + + assert_eq!( + config.unwrap(), + TomlConfig { + accounts: HashMap::from_iter([( + "account".into(), + TomlAccountConfig { + email: "test@localhost".into(), + sender: SenderConfig::Sendmail(SendmailConfig { + cmd: Cmd::from("echo send") + }), + ..TomlAccountConfig::default() + } + )]), + ..TomlConfig::default() + } + ) + } + + #[tokio::test] + async fn account_backend_imap_minimum_config() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"imap\" + imap-host = \"localhost\" + imap-port = 993 + imap-login = \"login\" + imap-auth = \"passwd\" + imap-passwd = { cmd = \"echo password\" }", + ) + .await; + + assert_eq!( + config.unwrap(), + TomlConfig { + accounts: HashMap::from_iter([( + "account".into(), + TomlAccountConfig { + email: "test@localhost".into(), + backend: BackendConfig::Imap(ImapConfig { + host: "localhost".into(), + port: 993, + login: "login".into(), + auth: ImapAuthConfig::Passwd(PasswdConfig { + passwd: Secret::new_cmd(String::from("echo password")) + }), + ..ImapConfig::default() + }), + ..TomlAccountConfig::default() + } + )]), + ..TomlConfig::default() + } + ) + } + + #[tokio::test] + async fn account_backend_maildir_minimum_config() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"maildir\" + maildir-root-dir = \"/tmp/maildir\"", + ) + .await; + + assert_eq!( + config.unwrap(), + TomlConfig { + accounts: HashMap::from_iter([( + "account".into(), + TomlAccountConfig { + email: "test@localhost".into(), + backend: BackendConfig::Maildir(MaildirConfig { + root_dir: "/tmp/maildir".into(), + }), + ..TomlAccountConfig::default() + } + )]), + ..TomlConfig::default() + } + ) + } + + #[cfg(feature = "notmuch")] + #[tokio::test] + async fn account_backend_notmuch_minimum_config() { + let config = make_config( + "[account] + email = \"test@localhost\" + sender = \"none\" + backend = \"notmuch\" + notmuch-db-path = \"/tmp/notmuch.db\"", + ) + .await; + + assert_eq!( + config.unwrap(), + TomlConfig { + accounts: HashMap::from_iter([( + "account".into(), + TomlAccountConfig { + email: "test@localhost".into(), + backend: BackendConfig::Notmuch(NotmuchConfig { + db_path: "/tmp/notmuch.db".into(), + }), + ..TomlAccountConfig::default() + } + )]), + ..TomlConfig::default() + } + ); + } +} diff --git a/src/domain/account/account.rs b/src/domain/account/account.rs deleted file mode 100644 index 8ddae67..0000000 --- a/src/domain/account/account.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Account module. -//! -//! This module contains the definition of the printable account, -//! which is only used by the "accounts" command to list all available -//! accounts from the config file. - -use serde::Serialize; -use std::fmt; - -use crate::ui::table::{Cell, Row, Table}; - -/// Represents the printable account. -#[derive(Debug, Default, PartialEq, Eq, Serialize)] -pub struct Account { - /// Represents the account name. - pub name: String, - /// Represents the backend name of the account. - pub backend: String, - /// Represents the default state of the account. - pub default: bool, -} - -impl Account { - pub fn new(name: &str, backend: &str, default: bool) -> Self { - Self { - name: name.into(), - backend: backend.into(), - default, - } - } -} - -impl fmt::Display for Account { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name) - } -} - -impl Table for Account { - fn head() -> Row { - Row::new() - .cell(Cell::new("NAME").shrinkable().bold().underline().white()) - .cell(Cell::new("BACKEND").bold().underline().white()) - .cell(Cell::new("DEFAULT").bold().underline().white()) - } - - fn row(&self) -> Row { - let default = if self.default { "yes" } else { "" }; - Row::new() - .cell(Cell::new(&self.name).shrinkable().green()) - .cell(Cell::new(&self.backend).blue()) - .cell(Cell::new(default).white()) - } -} diff --git a/src/domain/account/mod.rs b/src/domain/account/mod.rs deleted file mode 100644 index 0f10b25..0000000 --- a/src/domain/account/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod account; -pub mod accounts; -pub mod args; -pub mod config; -pub mod handlers; -pub(crate) mod wizard; - -pub use account::*; -pub use accounts::*; -pub use config::*; diff --git a/src/domain/email/envelope/flag/mod.rs b/src/domain/email/envelope/flag/mod.rs deleted file mode 100644 index ef68c36..0000000 --- a/src/domain/email/envelope/flag/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod config; diff --git a/src/domain/email/envelope/mod.rs b/src/domain/email/envelope/mod.rs deleted file mode 100644 index abaed0b..0000000 --- a/src/domain/email/envelope/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod config; -pub mod flag; diff --git a/src/domain/email/message/mod.rs b/src/domain/email/message/mod.rs deleted file mode 100644 index ef68c36..0000000 --- a/src/domain/email/message/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod config; diff --git a/src/domain/email/mod.rs b/src/domain/email/mod.rs deleted file mode 100644 index 5de2dfe..0000000 --- a/src/domain/email/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod args; -pub mod envelope; -pub mod handlers; -pub mod message; diff --git a/src/domain/envelope/envelope.rs b/src/domain/envelope/envelope.rs deleted file mode 100644 index c683de2..0000000 --- a/src/domain/envelope/envelope.rs +++ /dev/null @@ -1,66 +0,0 @@ -use serde::Serialize; - -use crate::{ - ui::{Cell, Row, Table}, - Flag, Flags, -}; - -#[derive(Clone, Debug, Default, Serialize)] -pub struct Mailbox { - pub name: Option, - pub addr: String, -} - -#[derive(Clone, Debug, Default, Serialize)] -pub struct Envelope { - pub id: String, - pub flags: Flags, - pub subject: String, - pub from: Mailbox, - pub date: String, -} - -impl Table for Envelope { - fn head() -> Row { - Row::new() - .cell(Cell::new("ID").bold().underline().white()) - .cell(Cell::new("FLAGS").bold().underline().white()) - .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) - .cell(Cell::new("FROM").bold().underline().white()) - .cell(Cell::new("DATE").bold().underline().white()) - } - - fn row(&self) -> Row { - let id = self.id.to_string(); - let unseen = !self.flags.contains(&Flag::Seen); - let flags = { - let mut flags = String::new(); - flags.push_str(if !unseen { " " } else { "✷" }); - flags.push_str(if self.flags.contains(&Flag::Answered) { - "↵" - } else { - " " - }); - flags.push_str(if self.flags.contains(&Flag::Flagged) { - "⚑" - } else { - " " - }); - flags - }; - let subject = &self.subject; - let sender = if let Some(name) = &self.from.name { - name - } else { - &self.from.addr - }; - let date = &self.date; - - Row::new() - .cell(Cell::new(id).bold_if(unseen).red()) - .cell(Cell::new(flags).bold_if(unseen).white()) - .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) - .cell(Cell::new(sender).bold_if(unseen).blue()) - .cell(Cell::new(date).bold_if(unseen).yellow()) - } -} diff --git a/src/domain/envelope/mod.rs b/src/domain/envelope/mod.rs deleted file mode 100644 index a893d38..0000000 --- a/src/domain/envelope/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod envelope; -pub mod envelopes; - -pub use envelope::*; -pub use envelopes::*; diff --git a/src/domain/flag/flags.rs b/src/domain/flag/flags.rs deleted file mode 100644 index d0c30d2..0000000 --- a/src/domain/flag/flags.rs +++ /dev/null @@ -1,21 +0,0 @@ -use serde::Serialize; -use std::{collections::HashSet, ops}; - -use crate::Flag; - -#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] -pub struct Flags(pub HashSet); - -impl ops::Deref for Flags { - type Target = HashSet; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for Flags { - fn from(flags: email::flag::Flags) -> Self { - Flags(flags.iter().map(Flag::from).collect()) - } -} diff --git a/src/domain/flag/mod.rs b/src/domain/flag/mod.rs deleted file mode 100644 index 5d92934..0000000 --- a/src/domain/flag/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod args; -pub mod handlers; - -pub mod flag; -pub use flag::*; - -pub mod flags; -pub use flags::*; diff --git a/src/domain/folder/folder.rs b/src/domain/folder/folder.rs deleted file mode 100644 index 7d1e410..0000000 --- a/src/domain/folder/folder.rs +++ /dev/null @@ -1,32 +0,0 @@ -use serde::Serialize; - -use crate::ui::{Cell, Row, Table}; - -#[derive(Clone, Debug, Default, Serialize)] -pub struct Folder { - pub name: String, - pub desc: String, -} - -impl From<&email::folder::Folder> for Folder { - fn from(folder: &email::folder::Folder) -> Self { - Folder { - name: folder.name.clone(), - desc: folder.desc.clone(), - } - } -} - -impl Table for Folder { - fn head() -> Row { - Row::new() - .cell(Cell::new("NAME").bold().underline().white()) - .cell(Cell::new("DESC").bold().underline().white()) - } - - fn row(&self) -> Row { - Row::new() - .cell(Cell::new(&self.name).blue()) - .cell(Cell::new(&self.desc).green()) - } -} diff --git a/src/domain/folder/folders.rs b/src/domain/folder/folders.rs deleted file mode 100644 index dd5cc1d..0000000 --- a/src/domain/folder/folders.rs +++ /dev/null @@ -1,35 +0,0 @@ -use anyhow::Result; -use serde::Serialize; -use std::ops; - -use crate::{ - printer::{PrintTable, PrintTableOpts, WriteColor}, - ui::Table, - Folder, -}; - -#[derive(Clone, Debug, Default, Serialize)] -pub struct Folders(Vec); - -impl ops::Deref for Folders { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From for Folders { - fn from(folders: email::folder::Folders) -> Self { - Folders(folders.iter().map(Folder::from).collect()) - } -} - -impl PrintTable for Folders { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} diff --git a/src/domain/folder/mod.rs b/src/domain/folder/mod.rs deleted file mode 100644 index 321a172..0000000 --- a/src/domain/folder/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod args; -pub mod config; -pub mod folder; -pub mod folders; -pub mod handlers; - -pub use folder::*; -pub use folders::*; diff --git a/src/domain/mod.rs b/src/domain/mod.rs deleted file mode 100644 index 50ca65a..0000000 --- a/src/domain/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod account; -pub mod email; -pub mod envelope; -pub mod flag; -pub mod folder; -pub mod tpl; - -pub use self::account::{args, handlers, Account, Accounts}; -pub use self::email::*; -pub use self::envelope::*; -pub use self::flag::*; -pub use self::folder::*; -pub use self::tpl::*; diff --git a/src/email/envelope/args.rs b/src/email/envelope/args.rs new file mode 100644 index 0000000..717e079 --- /dev/null +++ b/src/email/envelope/args.rs @@ -0,0 +1,90 @@ +//! Email CLI module. +//! +//! This module contains the command matcher, the subcommands and the +//! arguments related to the email domain. + +use anyhow::Result; +use clap::{Arg, ArgMatches, Command}; + +use crate::ui::table; + +const ARG_PAGE: &str = "page"; +const ARG_PAGE_SIZE: &str = "page-size"; +const CMD_LIST: &str = "list"; +const CMD_ENVELOPE: &str = "envelope"; + +pub type Page = usize; +pub type PageSize = usize; + +/// Represents the email commands. +#[derive(Debug, PartialEq, Eq)] +pub enum Cmd { + List(table::args::MaxTableWidth, Option, Page), +} + +/// Email command matcher. +pub fn matches(m: &ArgMatches) -> Result> { + let cmd = if let Some(m) = m.subcommand_matches(CMD_ENVELOPE) { + if let Some(m) = m.subcommand_matches(CMD_LIST) { + let max_table_width = table::args::parse_max_width(m); + let page_size = parse_page_size_arg(m); + let page = parse_page_arg(m); + Some(Cmd::List(max_table_width, page_size, page)) + } else { + Some(Cmd::List(None, None, 0)) + } + } else { + None + }; + + Ok(cmd) +} + +/// Represents the envelope subcommand. +pub fn subcmd() -> Command { + Command::new(CMD_ENVELOPE) + .about("Manage envelopes") + .subcommands([Command::new(CMD_LIST) + .alias("lst") + .about("List envelopes") + .arg(page_size_arg()) + .arg(page_arg()) + .arg(table::args::max_width())]) +} + +/// Represents the page size argument. +fn page_size_arg() -> Arg { + Arg::new(ARG_PAGE_SIZE) + .help("Page size") + .long("page-size") + .short('s') + .value_name("INT") +} + +/// Represents the page size argument parser. +fn parse_page_size_arg(matches: &ArgMatches) -> Option { + matches + .get_one::(ARG_PAGE_SIZE) + .and_then(|s| s.parse().ok()) +} + +/// Represents the page argument. +fn page_arg() -> Arg { + Arg::new(ARG_PAGE) + .help("Page number") + .short('p') + .long("page") + .value_name("INT") + .default_value("1") +} + +/// Represents the page argument parser. +fn parse_page_arg(matches: &ArgMatches) -> usize { + matches + .get_one::(ARG_PAGE) + .unwrap() + .parse() + .ok() + .map(|page| 1.max(page) - 1) + .unwrap_or_default() +} diff --git a/src/domain/email/envelope/config.rs b/src/email/envelope/config.rs similarity index 100% rename from src/domain/email/envelope/config.rs rename to src/email/envelope/config.rs diff --git a/src/domain/flag/args.rs b/src/email/envelope/flag/args.rs similarity index 58% rename from src/domain/flag/args.rs rename to src/email/envelope/flag/args.rs index bf3d438..fd80935 100644 --- a/src/domain/flag/args.rs +++ b/src/email/envelope/flag/args.rs @@ -8,7 +8,7 @@ use anyhow::Result; use clap::{Arg, ArgMatches, Command}; use log::{debug, info}; -use crate::email; +use crate::message; const ARG_FLAGS: &str = "flag"; @@ -21,28 +21,32 @@ pub(crate) const CMD_FLAG: &str = "flags"; /// Represents the flag commands. #[derive(Debug, PartialEq, Eq)] pub enum Cmd<'a> { - Add(email::args::Ids<'a>, Flags), - Remove(email::args::Ids<'a>, Flags), - Set(email::args::Ids<'a>, Flags), + Add(message::args::Ids<'a>, Flags), + Remove(message::args::Ids<'a>, Flags), + Set(message::args::Ids<'a>, Flags), } /// Represents the flag command matcher. -pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_ADD) { - debug!("add flags command matched"); - let ids = email::args::parse_ids_arg(m); - let flags = parse_flags_arg(m); - Some(Cmd::Add(ids, flags)) - } else if let Some(m) = m.subcommand_matches(CMD_REMOVE) { - info!("remove flags command matched"); - let ids = email::args::parse_ids_arg(m); - let flags = parse_flags_arg(m); - Some(Cmd::Remove(ids, flags)) - } else if let Some(m) = m.subcommand_matches(CMD_SET) { - debug!("set flags command matched"); - let ids = email::args::parse_ids_arg(m); - let flags = parse_flags_arg(m); - Some(Cmd::Set(ids, flags)) +pub fn matches(m: &ArgMatches) -> Result> { + let cmd = if let Some(m) = m.subcommand_matches(CMD_FLAG) { + if let Some(m) = m.subcommand_matches(CMD_ADD) { + debug!("add flags command matched"); + let ids = message::args::parse_ids_arg(m); + let flags = parse_flags_arg(m); + Some(Cmd::Add(ids, flags)) + } else if let Some(m) = m.subcommand_matches(CMD_REMOVE) { + info!("remove flags command matched"); + let ids = message::args::parse_ids_arg(m); + let flags = parse_flags_arg(m); + Some(Cmd::Remove(ids, flags)) + } else if let Some(m) = m.subcommand_matches(CMD_SET) { + debug!("set flags command matched"); + let ids = message::args::parse_ids_arg(m); + let flags = parse_flags_arg(m); + Some(Cmd::Set(ids, flags)) + } else { + None + } } else { None }; @@ -50,32 +54,32 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { Ok(cmd) } -/// Represents the flag subcommands. -pub fn subcmds<'a>() -> Vec { - vec![Command::new(CMD_FLAG) - .about("Handles email flags") +/// Represents the flag subcommand. +pub fn subcmd() -> Command { + Command::new(CMD_FLAG) + .about("Manage flags") .subcommand_required(true) .arg_required_else_help(true) .subcommand( Command::new(CMD_ADD) .about("Adds flags to an email") - .arg(email::args::ids_arg()) + .arg(message::args::ids_arg()) .arg(flags_arg()), ) .subcommand( Command::new(CMD_REMOVE) .aliases(["delete", "del", "d"]) .about("Removes flags from an email") - .arg(email::args::ids_arg()) + .arg(message::args::ids_arg()) .arg(flags_arg()), ) .subcommand( Command::new(CMD_SET) .aliases(["change", "c"]) .about("Sets flags of an email") - .arg(email::args::ids_arg()) + .arg(message::args::ids_arg()) .arg(flags_arg()), - )] + ) } /// Represents the flags argument. diff --git a/src/domain/email/envelope/flag/config.rs b/src/email/envelope/flag/config.rs similarity index 100% rename from src/domain/email/envelope/flag/config.rs rename to src/email/envelope/flag/config.rs diff --git a/src/domain/flag/handlers.rs b/src/email/envelope/flag/handlers.rs similarity index 100% rename from src/domain/flag/handlers.rs rename to src/email/envelope/flag/handlers.rs diff --git a/src/domain/flag/flag.rs b/src/email/envelope/flag/mod.rs similarity index 56% rename from src/domain/flag/flag.rs rename to src/email/envelope/flag/mod.rs index bf3712a..2416abc 100644 --- a/src/domain/flag/flag.rs +++ b/src/email/envelope/flag/mod.rs @@ -1,4 +1,9 @@ +pub mod args; +pub mod config; +pub mod handlers; + use serde::Serialize; +use std::{collections::HashSet, ops}; /// Represents the flag variants. #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)] @@ -24,3 +29,20 @@ impl From<&email::flag::Flag> for Flag { } } } + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] +pub struct Flags(pub HashSet); + +impl ops::Deref for Flags { + type Target = HashSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Flags { + fn from(flags: email::flag::Flags) -> Self { + Flags(flags.iter().map(Flag::from).collect()) + } +} diff --git a/src/email/envelope/handlers.rs b/src/email/envelope/handlers.rs new file mode 100644 index 0000000..0477a66 --- /dev/null +++ b/src/email/envelope/handlers.rs @@ -0,0 +1,32 @@ +use anyhow::Result; +use email::account::config::AccountConfig; +use log::{debug, trace}; + +use crate::{ + backend::Backend, + printer::{PrintTableOpts, Printer}, +}; + +pub async fn list( + config: &AccountConfig, + printer: &mut P, + backend: &Backend, + folder: &str, + max_width: Option, + page_size: Option, + page: usize, +) -> Result<()> { + let page_size = page_size.unwrap_or(config.email_listing_page_size()); + debug!("page size: {}", page_size); + + let envelopes = backend.list_envelopes(&folder, page_size, page).await?; + trace!("envelopes: {:?}", envelopes); + + printer.print_table( + Box::new(envelopes), + PrintTableOpts { + format: &config.email_reading_format, + max_width, + }, + ) +} diff --git a/src/domain/envelope/envelopes.rs b/src/email/envelope/mod.rs similarity index 64% rename from src/domain/envelope/envelopes.rs rename to src/email/envelope/mod.rs index ee1fdd3..b43c520 100644 --- a/src/domain/envelope/envelopes.rs +++ b/src/email/envelope/mod.rs @@ -1,14 +1,80 @@ +pub mod args; +pub mod config; +pub mod flag; +pub mod handlers; + use anyhow::Result; use email::account::config::AccountConfig; use serde::Serialize; use std::ops; use crate::{ + cache::IdMapper, + flag::{Flag, Flags}, printer::{PrintTable, PrintTableOpts, WriteColor}, - ui::Table, - Envelope, IdMapper, Mailbox, + ui::{Cell, Row, Table}, }; +#[derive(Clone, Debug, Default, Serialize)] +pub struct Mailbox { + pub name: Option, + pub addr: String, +} + +#[derive(Clone, Debug, Default, Serialize)] +pub struct Envelope { + pub id: String, + pub flags: Flags, + pub subject: String, + pub from: Mailbox, + pub date: String, +} + +impl Table for Envelope { + fn head() -> Row { + Row::new() + .cell(Cell::new("ID").bold().underline().white()) + .cell(Cell::new("FLAGS").bold().underline().white()) + .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) + .cell(Cell::new("FROM").bold().underline().white()) + .cell(Cell::new("DATE").bold().underline().white()) + } + + fn row(&self) -> Row { + let id = self.id.to_string(); + let unseen = !self.flags.contains(&Flag::Seen); + let flags = { + let mut flags = String::new(); + flags.push_str(if !unseen { " " } else { "✷" }); + flags.push_str(if self.flags.contains(&Flag::Answered) { + "↵" + } else { + " " + }); + flags.push_str(if self.flags.contains(&Flag::Flagged) { + "⚑" + } else { + " " + }); + flags + }; + let subject = &self.subject; + let sender = if let Some(name) = &self.from.name { + name + } else { + &self.from.addr + }; + let date = &self.date; + + Row::new() + .cell(Cell::new(id).bold_if(unseen).red()) + .cell(Cell::new(flags).bold_if(unseen).white()) + .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) + .cell(Cell::new(sender).bold_if(unseen).blue()) + .cell(Cell::new(date).bold_if(unseen).yellow()) + } +} + /// Represents the list of envelopes. #[derive(Clone, Debug, Default, Serialize)] pub struct Envelopes(Vec); @@ -62,7 +128,9 @@ mod tests { use email::account::config::AccountConfig; use std::env; - use crate::{Envelopes, IdMapper}; + use crate::cache::IdMapper; + + use super::Envelopes; #[test] fn default_datetime_fmt() { diff --git a/src/domain/email/args.rs b/src/email/message/args.rs similarity index 57% rename from src/domain/email/args.rs rename to src/email/message/args.rs index 512f58b..92eb099 100644 --- a/src/domain/email/args.rs +++ b/src/email/message/args.rs @@ -6,15 +6,13 @@ use anyhow::Result; use clap::{Arg, ArgAction, ArgMatches, Command}; -use crate::{flag, folder, tpl, ui::table}; +use crate::{folder, template}; const ARG_CRITERIA: &str = "criterion"; const ARG_HEADERS: &str = "headers"; const ARG_ID: &str = "id"; const ARG_IDS: &str = "ids"; const ARG_MIME_TYPE: &str = "mime-type"; -const ARG_PAGE: &str = "page"; -const ARG_PAGE_SIZE: &str = "page-size"; const ARG_QUERY: &str = "query"; const ARG_RAW: &str = "raw"; const ARG_REPLY_ALL: &str = "reply-all"; @@ -22,14 +20,12 @@ const CMD_ATTACHMENTS: &str = "attachments"; const CMD_COPY: &str = "copy"; const CMD_DELETE: &str = "delete"; const CMD_FORWARD: &str = "forward"; -const CMD_LIST: &str = "list"; +const CMD_MESSAGE: &str = "message"; const CMD_MOVE: &str = "move"; const CMD_READ: &str = "read"; const CMD_REPLY: &str = "reply"; const CMD_SAVE: &str = "save"; -const CMD_SEARCH: &str = "search"; const CMD_SEND: &str = "send"; -const CMD_SORT: &str = "sort"; const CMD_WRITE: &str = "write"; pub type All = bool; @@ -38,8 +34,6 @@ pub type Folder<'a> = &'a str; pub type Headers<'a> = Vec<&'a str>; pub type Id<'a> = &'a str; pub type Ids<'a> = Vec<&'a str>; -pub type Page = usize; -pub type PageSize = usize; pub type Query = String; pub type Raw = bool; pub type RawEmail = String; @@ -51,131 +45,93 @@ pub enum Cmd<'a> { Attachments(Ids<'a>), Copy(Ids<'a>, Folder<'a>), Delete(Ids<'a>), - Flag(Option>), - Forward(Id<'a>, tpl::args::Headers<'a>, tpl::args::Body<'a>), - List(table::args::MaxTableWidth, Option, Page), + Forward( + Id<'a>, + template::args::Headers<'a>, + template::args::Body<'a>, + ), Move(Ids<'a>, Folder<'a>), Read(Ids<'a>, TextMime<'a>, Raw, Headers<'a>), - Reply(Id<'a>, All, tpl::args::Headers<'a>, tpl::args::Body<'a>), - Save(RawEmail), - Search(Query, table::args::MaxTableWidth, Option, Page), - Send(RawEmail), - Sort( - Criteria, - Query, - table::args::MaxTableWidth, - Option, - Page, + Reply( + Id<'a>, + All, + template::args::Headers<'a>, + template::args::Body<'a>, ), - Tpl(Option>), - Write(tpl::args::Headers<'a>, tpl::args::Body<'a>), + Save(RawEmail), + Send(RawEmail), + Write(template::args::Headers<'a>, template::args::Body<'a>), } /// Email command matcher. -pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) { - let ids = parse_ids_arg(m); - Cmd::Attachments(ids) - } else if let Some(m) = m.subcommand_matches(CMD_COPY) { - let ids = parse_ids_arg(m); - let folder = folder::args::parse_target_arg(m); - Cmd::Copy(ids, folder) - } else if let Some(m) = m.subcommand_matches(CMD_DELETE) { - let ids = parse_ids_arg(m); - Cmd::Delete(ids) - } else if let Some(m) = m.subcommand_matches(flag::args::CMD_FLAG) { - Cmd::Flag(flag::args::matches(m)?) - } else if let Some(m) = m.subcommand_matches(CMD_FORWARD) { - let id = parse_id_arg(m); - let headers = tpl::args::parse_headers_arg(m); - let body = tpl::args::parse_body_arg(m); - Cmd::Forward(id, headers, body) - } else if let Some(m) = m.subcommand_matches(CMD_LIST) { - let max_table_width = table::args::parse_max_width(m); - let page_size = parse_page_size_arg(m); - let page = parse_page_arg(m); - Cmd::List(max_table_width, page_size, page) - } else if let Some(m) = m.subcommand_matches(CMD_MOVE) { - let ids = parse_ids_arg(m); - let folder = folder::args::parse_target_arg(m); - Cmd::Move(ids, folder) - } else if let Some(m) = m.subcommand_matches(CMD_READ) { - let ids = parse_ids_arg(m); - let mime = parse_mime_type_arg(m); - let raw = parse_raw_flag(m); - let headers = parse_headers_arg(m); - Cmd::Read(ids, mime, raw, headers) - } else if let Some(m) = m.subcommand_matches(CMD_REPLY) { - let id = parse_id_arg(m); - let all = parse_reply_all_flag(m); - let headers = tpl::args::parse_headers_arg(m); - let body = tpl::args::parse_body_arg(m); - Cmd::Reply(id, all, headers, body) - } else if let Some(m) = m.subcommand_matches(CMD_SAVE) { - let email = parse_raw_arg(m); - Cmd::Save(email) - } else if let Some(m) = m.subcommand_matches(CMD_SEARCH) { - let max_table_width = table::args::parse_max_width(m); - let page_size = parse_page_size_arg(m); - let page = parse_page_arg(m); - let query = parse_query_arg(m); - Cmd::Search(query, max_table_width, page_size, page) - } else if let Some(m) = m.subcommand_matches(CMD_SORT) { - let max_table_width = table::args::parse_max_width(m); - let page_size = parse_page_size_arg(m); - let page = parse_page_arg(m); - let criteria = parse_criteria_arg(m); - let query = parse_query_arg(m); - Cmd::Sort(criteria, query, max_table_width, page_size, page) - } else if let Some(m) = m.subcommand_matches(CMD_SEND) { - let email = parse_raw_arg(m); - Cmd::Send(email) - } else if let Some(m) = m.subcommand_matches(tpl::args::CMD_TPL) { - Cmd::Tpl(tpl::args::matches(m)?) - } else if let Some(m) = m.subcommand_matches(CMD_WRITE) { - let headers = tpl::args::parse_headers_arg(m); - let body = tpl::args::parse_body_arg(m); - Cmd::Write(headers, body) +pub fn matches(m: &ArgMatches) -> Result> { + let cmd = if let Some(m) = m.subcommand_matches(CMD_MESSAGE) { + if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) { + let ids = parse_ids_arg(m); + Some(Cmd::Attachments(ids)) + } else if let Some(m) = m.subcommand_matches(CMD_COPY) { + let ids = parse_ids_arg(m); + let folder = folder::args::parse_target_arg(m); + Some(Cmd::Copy(ids, folder)) + } else if let Some(m) = m.subcommand_matches(CMD_DELETE) { + let ids = parse_ids_arg(m); + Some(Cmd::Delete(ids)) + } else if let Some(m) = m.subcommand_matches(CMD_FORWARD) { + let id = parse_id_arg(m); + let headers = template::args::parse_headers_arg(m); + let body = template::args::parse_body_arg(m); + Some(Cmd::Forward(id, headers, body)) + } else if let Some(m) = m.subcommand_matches(CMD_MOVE) { + let ids = parse_ids_arg(m); + let folder = folder::args::parse_target_arg(m); + Some(Cmd::Move(ids, folder)) + } else if let Some(m) = m.subcommand_matches(CMD_READ) { + let ids = parse_ids_arg(m); + let mime = parse_mime_type_arg(m); + let raw = parse_raw_flag(m); + let headers = parse_headers_arg(m); + Some(Cmd::Read(ids, mime, raw, headers)) + } else if let Some(m) = m.subcommand_matches(CMD_REPLY) { + let id = parse_id_arg(m); + let all = parse_reply_all_flag(m); + let headers = template::args::parse_headers_arg(m); + let body = template::args::parse_body_arg(m); + Some(Cmd::Reply(id, all, headers, body)) + } else if let Some(m) = m.subcommand_matches(CMD_SAVE) { + let email = parse_raw_arg(m); + Some(Cmd::Save(email)) + } else if let Some(m) = m.subcommand_matches(CMD_SEND) { + let email = parse_raw_arg(m); + Some(Cmd::Send(email)) + } else if let Some(m) = m.subcommand_matches(CMD_WRITE) { + let headers = template::args::parse_headers_arg(m); + let body = template::args::parse_body_arg(m); + Some(Cmd::Write(headers, body)) + } else { + None + } } else { - Cmd::List(None, None, 0) + None }; - Ok(Some(cmd)) + Ok(cmd) } /// Represents the email subcommands. -pub fn subcmds() -> Vec { - vec![ - flag::args::subcmds(), - tpl::args::subcmds(), - vec![ +pub fn subcmd() -> Command { + Command::new(CMD_MESSAGE) + .about("Manage messages") + .aliases(["msg"]) + .subcommand_required(true) + .arg_required_else_help(true) + .subcommands([ Command::new(CMD_ATTACHMENTS) .about("Downloads all emails attachments") .arg(ids_arg()), - Command::new(CMD_LIST) - .alias("lst") - .about("List envelopes") - .arg(page_size_arg()) - .arg(page_arg()) - .arg(table::args::max_width()), - Command::new(CMD_SEARCH) - .aliases(["query", "q"]) - .about("Filter envelopes matching the given query") - .arg(page_size_arg()) - .arg(page_arg()) - .arg(table::args::max_width()) - .arg(query_arg()), - Command::new(CMD_SORT) - .about("Sort envelopes by the given criteria and matching the given query") - .arg(page_size_arg()) - .arg(page_arg()) - .arg(table::args::max_width()) - .arg(criteria_arg()) - .arg(query_arg()), Command::new(CMD_WRITE) .about("Write a new email") .aliases(["new", "n"]) - .args(tpl::args::args()), + .args(template::args::args()), Command::new(CMD_SEND) .about("Send a raw email") .arg(raw_arg()), @@ -191,12 +147,12 @@ pub fn subcmds() -> Vec { Command::new(CMD_REPLY) .about("Answer to an email") .arg(reply_all_flag()) - .args(tpl::args::args()) + .args(template::args::args()) .arg(id_arg()), Command::new(CMD_FORWARD) .aliases(["fwd", "f"]) .about("Forward an email") - .args(tpl::args::args()) + .args(template::args::args()) .arg(id_arg()), Command::new(CMD_COPY) .alias("cp") @@ -212,9 +168,7 @@ pub fn subcmds() -> Vec { .aliases(["remove", "rm"]) .about("Delete emails") .arg(ids_arg()), - ], - ] - .concat() + ]) } /// Represents the email id argument. @@ -305,43 +259,6 @@ pub fn parse_reply_all_flag(matches: &ArgMatches) -> bool { matches.get_flag(ARG_REPLY_ALL) } -/// Represents the page size argument. -fn page_size_arg() -> Arg { - Arg::new(ARG_PAGE_SIZE) - .help("Page size") - .long("page-size") - .short('s') - .value_name("INT") -} - -/// Represents the page size argument parser. -fn parse_page_size_arg(matches: &ArgMatches) -> Option { - matches - .get_one::(ARG_PAGE_SIZE) - .and_then(|s| s.parse().ok()) -} - -/// Represents the page argument. -fn page_arg() -> Arg { - Arg::new(ARG_PAGE) - .help("Page number") - .short('p') - .long("page") - .value_name("INT") - .default_value("1") -} - -/// Represents the page argument parser. -fn parse_page_arg(matches: &ArgMatches) -> usize { - matches - .get_one::(ARG_PAGE) - .unwrap() - .parse() - .ok() - .map(|page| 1.max(page) - 1) - .unwrap_or_default() -} - /// Represents the email headers argument. pub fn headers_arg() -> Arg { Arg::new(ARG_HEADERS) diff --git a/src/domain/email/message/config.rs b/src/email/message/config.rs similarity index 100% rename from src/domain/email/message/config.rs rename to src/email/message/config.rs diff --git a/src/domain/email/handlers.rs b/src/email/message/handlers.rs similarity index 78% rename from src/domain/email/handlers.rs rename to src/email/message/handlers.rs index b835ce8..01fa6f2 100644 --- a/src/domain/email/handlers.rs +++ b/src/email/message/handlers.rs @@ -4,7 +4,7 @@ use email::{ account::config::AccountConfig, envelope::Id, flag::Flag, message::Message, template::FilterParts, }; -use log::{debug, trace}; +use log::trace; use mail_builder::MessageBuilder; use std::{ fs, @@ -13,11 +13,7 @@ use std::{ use url::Url; use uuid::Uuid; -use crate::{ - backend::Backend, - printer::{PrintTableOpts, Printer}, - ui::editor, -}; +use crate::{backend::Backend, printer::Printer, ui::editor}; pub async fn attachments( config: &AccountConfig, @@ -120,30 +116,6 @@ pub async fn forward( Ok(()) } -pub async fn list( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - folder: &str, - max_width: Option, - page_size: Option, - page: usize, -) -> Result<()> { - let page_size = page_size.unwrap_or(config.email_listing_page_size()); - debug!("page size: {}", page_size); - - let envelopes = backend.list_envelopes(&folder, page_size, page).await?; - trace!("envelopes: {:?}", envelopes); - - printer.print_table( - Box::new(envelopes), - PrintTableOpts { - format: &config.email_reading_format, - max_width, - }, - ) -} - /// Parses and edits a message from a [mailto] URL string. /// /// [mailto]: https://en.wikipedia.org/wiki/Mailto @@ -284,61 +256,6 @@ pub async fn save( Ok(()) } -pub async fn search( - _config: &AccountConfig, - _printer: &mut P, - _backend: &Backend, - _folder: &str, - _query: String, - _max_width: Option, - _page_size: Option, - _page: usize, -) -> Result<()> { - todo!() - // let page_size = page_size.unwrap_or(config.email_listing_page_size()); - // let envelopes = Envelopes::from_backend( - // config, - // id_mapper, - // backend - // .search_envelopes(&folder, &query, "", page_size, page) - // .await?, - // )?; - // let opts = PrintTableOpts { - // format: &config.email_reading_format, - // max_width, - // }; - - // printer.print_table(Box::new(envelopes), opts) -} - -pub async fn sort( - _config: &AccountConfig, - _printer: &mut P, - _backend: &Backend, - _folder: &str, - _sort: String, - _query: String, - _max_width: Option, - _page_size: Option, - _page: usize, -) -> Result<()> { - todo!() - // let page_size = page_size.unwrap_or(config.email_listing_page_size()); - // let envelopes = Envelopes::from_backend( - // config, - // id_mapper, - // backend - // .search_envelopes(&folder, &query, &sort, page_size, page) - // .await?, - // )?; - // let opts = PrintTableOpts { - // format: &config.email_reading_format, - // max_width, - // }; - - // printer.print_table(Box::new(envelopes), opts) -} - pub async fn send( config: &AccountConfig, printer: &mut P, diff --git a/src/email/message/mod.rs b/src/email/message/mod.rs new file mode 100644 index 0000000..c101d77 --- /dev/null +++ b/src/email/message/mod.rs @@ -0,0 +1,4 @@ +pub mod args; +pub mod config; +pub mod handlers; +pub mod template; diff --git a/src/domain/tpl/args.rs b/src/email/message/template/args.rs similarity index 78% rename from src/domain/tpl/args.rs rename to src/email/message/template/args.rs index 71ae87b..7664aae 100644 --- a/src/domain/tpl/args.rs +++ b/src/email/message/template/args.rs @@ -7,7 +7,7 @@ use anyhow::Result; use clap::{Arg, ArgAction, ArgMatches, Command}; use log::warn; -use crate::email; +use crate::message; const ARG_BODY: &str = "body"; const ARG_HEADERS: &str = "headers"; @@ -27,9 +27,14 @@ pub type Body<'a> = Option<&'a str>; /// Represents the template commands. #[derive(Debug, PartialEq, Eq)] pub enum Cmd<'a> { - Forward(email::args::Id<'a>, Headers<'a>, Body<'a>), + Forward(message::args::Id<'a>, Headers<'a>, Body<'a>), Write(Headers<'a>, Body<'a>), - Reply(email::args::Id<'a>, email::args::All, Headers<'a>, Body<'a>), + Reply( + message::args::Id<'a>, + message::args::All, + Headers<'a>, + Body<'a>, + ), Save(RawTpl), Send(RawTpl), } @@ -37,13 +42,13 @@ pub enum Cmd<'a> { /// Represents the template command matcher. pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { let cmd = if let Some(m) = m.subcommand_matches(CMD_FORWARD) { - let id = email::args::parse_id_arg(m); + let id = message::args::parse_id_arg(m); let headers = parse_headers_arg(m); let body = parse_body_arg(m); Some(Cmd::Forward(id, headers, body)) } else if let Some(m) = m.subcommand_matches(CMD_REPLY) { - let id = email::args::parse_id_arg(m); - let all = email::args::parse_reply_all_flag(m); + let id = message::args::parse_id_arg(m); + let all = message::args::parse_reply_all_flag(m); let headers = parse_headers_arg(m); let body = parse_body_arg(m); Some(Cmd::Reply(id, all, headers, body)) @@ -65,55 +70,55 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { } /// Represents the template subcommands. -pub fn subcmds<'a>() -> Vec { - vec![Command::new(CMD_TPL) +pub fn subcmd() -> Command { + Command::new(CMD_TPL) .alias("tpl") - .about("Handles email templates") + .about("Manage templates") .subcommand_required(true) .arg_required_else_help(true) .subcommand( Command::new(CMD_FORWARD) .alias("fwd") - .about("Generates a template for forwarding an email") - .arg(email::args::id_arg()) + .about("Generate a template for forwarding an email") + .arg(message::args::id_arg()) .args(&args()), ) .subcommand( Command::new(CMD_REPLY) - .about("Generates a template for replying to an email") - .arg(email::args::id_arg()) - .arg(email::args::reply_all_flag()) + .about("Generate a template for replying to an email") + .arg(message::args::id_arg()) + .arg(message::args::reply_all_flag()) .args(&args()), ) .subcommand( Command::new(CMD_SAVE) - .about("Compiles the template into a valid email then saves it") + .about("Compile the template into a valid email then saves it") .arg(Arg::new(ARG_TPL).raw(true)), ) .subcommand( Command::new(CMD_SEND) - .about("Compiles the template into a valid email then sends it") + .about("Compile the template into a valid email then sends it") .arg(Arg::new(ARG_TPL).raw(true)), ) .subcommand( Command::new(CMD_WRITE) .aliases(["new", "n"]) - .about("Generates a template for writing a new email") + .about("Generate a template for writing a new email") .args(&args()), - )] + ) } /// Represents the template arguments. pub fn args() -> Vec { vec![ Arg::new(ARG_HEADERS) - .help("Overrides a specific header") + .help("Override a specific header") .short('H') .long("header") .value_name("KEY:VAL") .action(ArgAction::Append), Arg::new(ARG_BODY) - .help("Overrides the body") + .help("Override the body") .short('B') .long("body") .value_name("STRING"), diff --git a/src/domain/tpl/handlers.rs b/src/email/message/template/handlers.rs similarity index 100% rename from src/domain/tpl/handlers.rs rename to src/email/message/template/handlers.rs diff --git a/src/domain/tpl/mod.rs b/src/email/message/template/mod.rs similarity index 100% rename from src/domain/tpl/mod.rs rename to src/email/message/template/mod.rs diff --git a/src/email/mod.rs b/src/email/mod.rs new file mode 100644 index 0000000..7eae3d8 --- /dev/null +++ b/src/email/mod.rs @@ -0,0 +1,5 @@ +pub mod envelope; +pub mod message; + +#[doc(inline)] +pub use self::{envelope::flag, message::template}; diff --git a/src/domain/folder/args.rs b/src/folder/args.rs similarity index 98% rename from src/domain/folder/args.rs rename to src/folder/args.rs index f83b28f..98f57aa 100644 --- a/src/domain/folder/args.rs +++ b/src/folder/args.rs @@ -19,7 +19,7 @@ const ARG_TARGET: &str = "target"; const CMD_CREATE: &str = "create"; const CMD_DELETE: &str = "delete"; const CMD_EXPUNGE: &str = "expunge"; -const CMD_FOLDERS: &str = "folders"; +const CMD_FOLDER: &str = "folder"; const CMD_LIST: &str = "list"; /// Represents the folder commands. @@ -33,7 +33,7 @@ pub enum Cmd { /// Represents the folder command matcher. pub fn matches(m: &ArgMatches) -> Result> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDERS) { + let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDER) { if let Some(_) = m.subcommand_matches(CMD_EXPUNGE) { info!("expunge folder subcommand matched"); Some(Cmd::Expunge) @@ -60,7 +60,7 @@ pub fn matches(m: &ArgMatches) -> Result> { /// Represents the folder subcommand. pub fn subcmd() -> Command { - Command::new(CMD_FOLDERS) + Command::new(CMD_FOLDER) .about("Manage folders") .subcommands([ Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"), diff --git a/src/domain/folder/config.rs b/src/folder/config.rs similarity index 100% rename from src/domain/folder/config.rs rename to src/folder/config.rs diff --git a/src/domain/folder/handlers.rs b/src/folder/handlers.rs similarity index 98% rename from src/domain/folder/handlers.rs rename to src/folder/handlers.rs index 44be274..e43d34a 100644 --- a/src/domain/folder/handlers.rs +++ b/src/folder/handlers.rs @@ -10,9 +10,10 @@ use std::process; use crate::{ backend::Backend, printer::{PrintTableOpts, Printer}, - Folders, }; +use super::Folders; + pub async fn expunge(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> { backend.expunge_folder(folder).await?; printer.print(format!("Folder {folder} successfully expunged!")) @@ -58,10 +59,12 @@ pub async fn delete(printer: &mut P, backend: &Backend, folder: &str mod tests { use async_trait::async_trait; use email::{ - account::AccountConfig, + account::config::AccountConfig, backend::Backend, - email::{Envelope, Envelopes, Flags, Messages}, + envelope::{Envelope, Envelopes}, + flag::Flags, folder::{Folder, Folders}, + message::Messages, }; use std::{any::Any, fmt::Debug, io}; use termcolor::ColorSpec; diff --git a/src/folder/mod.rs b/src/folder/mod.rs new file mode 100644 index 0000000..e448920 --- /dev/null +++ b/src/folder/mod.rs @@ -0,0 +1,67 @@ +pub mod args; +pub mod config; +pub mod handlers; + +use anyhow::Result; +use serde::Serialize; +use std::ops; + +use crate::{ + printer::{PrintTable, PrintTableOpts, WriteColor}, + ui::{Cell, Row, Table}, +}; + +#[derive(Clone, Debug, Default, Serialize)] +pub struct Folder { + pub name: String, + pub desc: String, +} + +impl From<&email::folder::Folder> for Folder { + fn from(folder: &email::folder::Folder) -> Self { + Folder { + name: folder.name.clone(), + desc: folder.desc.clone(), + } + } +} + +impl Table for Folder { + fn head() -> Row { + Row::new() + .cell(Cell::new("NAME").bold().underline().white()) + .cell(Cell::new("DESC").bold().underline().white()) + } + + fn row(&self) -> Row { + Row::new() + .cell(Cell::new(&self.name).blue()) + .cell(Cell::new(&self.desc).green()) + } +} + +#[derive(Clone, Debug, Default, Serialize)] +pub struct Folders(Vec); + +impl ops::Deref for Folders { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Folders { + fn from(folders: email::folder::Folders) -> Self { + Folders(folders.iter().map(Folder::from).collect()) + } +} + +impl PrintTable for Folders { + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writer)?; + Table::print(writer, self, opts)?; + writeln!(writer)?; + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 52e83b6..79c29ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,20 +1,24 @@ +pub mod account; pub mod backend; pub mod cache; -pub mod compl; +pub mod completion; pub mod config; -pub mod domain; +pub mod email; +pub mod folder; #[cfg(feature = "imap")] pub mod imap; +#[cfg(feature = "maildir")] pub mod maildir; pub mod man; #[cfg(feature = "notmuch")] pub mod notmuch; pub mod output; pub mod printer; +#[cfg(feature = "sendmail")] pub mod sendmail; #[cfg(feature = "smtp")] pub mod smtp; pub mod ui; -pub use cache::IdMapper; -pub use domain::*; +#[doc(inline)] +pub use email::{envelope, flag, message, template}; diff --git a/src/main.rs b/src/main.rs index ad2e1df..b399239 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,11 +8,11 @@ use url::Url; use himalaya::{ account, backend::{Backend, BackendBuilder}, - cache, compl, + cache, completion, config::{self, TomlConfig}, - email, flag, folder, man, output, + envelope, flag, folder, man, message, output, printer::StdoutPrinter, - tpl, + template, }; fn create_app() -> Command { @@ -27,14 +27,16 @@ fn create_app() -> Command { .arg(cache::args::arg()) .args(output::args::args()) .arg(folder::args::source_arg()) - .subcommand(compl::args::subcmd()) + .subcommand(completion::args::subcmd()) .subcommand(man::args::subcmd()) .subcommand(account::args::subcmd()) .subcommand(folder::args::subcmd()) - .subcommands(email::args::subcmds()) + .subcommand(envelope::args::subcmd()) + .subcommand(flag::args::subcmd()) + .subcommand(message::args::subcmd()) + .subcommand(template::args::subcmd()) } -#[allow(clippy::single_match)] #[tokio::main] async fn main() -> Result<()> { #[cfg(not(target_os = "windows"))] @@ -58,22 +60,24 @@ async fn main() -> Result<()> { let backend = backend_builder.build().await?; let mut printer = StdoutPrinter::default(); - return email::handlers::mailto(&account_config, &backend, &mut printer, &url).await; + return message::handlers::mailto(&account_config, &backend, &mut printer, &url).await; } let app = create_app(); let m = app.get_matches(); - // check completion command before configs + // check completionetion command before configs // https://github.com/soywod/himalaya/issues/115 - match compl::args::matches(&m)? { - Some(compl::args::Cmd::Generate(shell)) => { - return compl::handlers::generate(create_app(), shell); + #[allow(clippy::single_match)] + match completion::args::matches(&m)? { + Some(completion::args::Cmd::Generate(shell)) => { + return completion::handlers::generate(create_app(), shell); } _ => (), } // check also man command before configs + #[allow(clippy::single_match)] match man::args::matches(&m)? { Some(man::args::Cmd::GenerateAll(dir)) => { return man::handlers::generate(dir, create_app()); @@ -170,48 +174,11 @@ async fn main() -> Result<()> { _ => (), } - // checks email commands - match email::args::matches(&m)? { - Some(email::args::Cmd::Attachments(ids)) => { + match envelope::args::matches(&m)? { + Some(envelope::args::Cmd::List(max_width, page_size, page)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::attachments( - &account_config, - &mut printer, - &backend, - &folder, - ids, - ) - .await; - } - Some(email::args::Cmd::Copy(ids, to_folder)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::copy(&mut printer, &backend, &folder, to_folder, ids).await; - } - Some(email::args::Cmd::Delete(ids)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::delete(&mut printer, &backend, &folder, ids).await; - } - Some(email::args::Cmd::Forward(id, headers, body)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - return email::handlers::forward( - &account_config, - &mut printer, - &backend, - &folder, - id, - headers, - body, - ) - .await; - } - Some(email::args::Cmd::List(max_width, page_size, page)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::list( + return envelope::handlers::list( &account_config, &mut printer, &backend, @@ -222,15 +189,74 @@ async fn main() -> Result<()> { ) .await; } - Some(email::args::Cmd::Move(ids, to_folder)) => { + _ => (), + } + + match flag::args::matches(&m)? { + Some(flag::args::Cmd::Set(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::move_(&mut printer, &backend, &folder, to_folder, ids).await; + return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await; } - Some(email::args::Cmd::Read(ids, text_mime, raw, headers)) => { + Some(flag::args::Cmd::Add(ids, ref flags)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::read( + return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await; + } + Some(flag::args::Cmd::Remove(ids, ref flags)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await; + } + _ => (), + } + + match message::args::matches(&m)? { + Some(message::args::Cmd::Attachments(ids)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + return message::handlers::attachments( + &account_config, + &mut printer, + &backend, + &folder, + ids, + ) + .await; + } + Some(message::args::Cmd::Copy(ids, to_folder)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + return message::handlers::copy(&mut printer, &backend, &folder, to_folder, ids).await; + } + Some(message::args::Cmd::Delete(ids)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + return message::handlers::delete(&mut printer, &backend, &folder, ids).await; + } + Some(message::args::Cmd::Forward(id, headers, body)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + return message::handlers::forward( + &account_config, + &mut printer, + &backend, + &folder, + id, + headers, + body, + ) + .await; + } + Some(message::args::Cmd::Move(ids, to_folder)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + return message::handlers::move_(&mut printer, &backend, &folder, to_folder, ids).await; + } + Some(message::args::Cmd::Read(ids, text_mime, raw, headers)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + return message::handlers::read( &account_config, &mut printer, &backend, @@ -242,10 +268,10 @@ async fn main() -> Result<()> { ) .await; } - Some(email::args::Cmd::Reply(id, all, headers, body)) => { + Some(message::args::Cmd::Reply(id, all, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - return email::handlers::reply( + return message::handlers::reply( &account_config, &mut printer, &backend, @@ -257,119 +283,78 @@ async fn main() -> Result<()> { ) .await; } - Some(email::args::Cmd::Save(raw_email)) => { + Some(message::args::Cmd::Save(raw_email)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::save(&mut printer, &backend, &folder, raw_email).await; + return message::handlers::save(&mut printer, &backend, &folder, raw_email).await; } - Some(email::args::Cmd::Search(query, max_width, page_size, page)) => { + Some(message::args::Cmd::Send(raw_email)) => { + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + return message::handlers::send(&account_config, &mut printer, &backend, raw_email) + .await; + } + Some(message::args::Cmd::Write(headers, body)) => { + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + return message::handlers::write( + &account_config, + &mut printer, + &backend, + headers, + body, + ) + .await; + } + _ => (), + } + + match template::args::matches(&m)? { + Some(template::args::Cmd::Forward(id, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::search( + return template::handlers::forward( &account_config, &mut printer, &backend, &folder, - query, - max_width, - page_size, - page, + id, + headers, + body, ) .await; } - Some(email::args::Cmd::Sort(criteria, query, max_width, page_size, page)) => { + Some(template::args::Cmd::Write(headers, body)) => { + return template::handlers::write(&account_config, &mut printer, headers, body).await; + } + Some(template::args::Cmd::Reply(id, all, headers, body)) => { let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return email::handlers::sort( + return template::handlers::reply( &account_config, &mut printer, &backend, &folder, - criteria, - query, - max_width, - page_size, - page, + id, + all, + headers, + body, ) .await; } - Some(email::args::Cmd::Send(raw_email)) => { - let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - return email::handlers::send(&account_config, &mut printer, &backend, raw_email).await; + Some(template::args::Cmd::Save(template)) => { + let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + return template::handlers::save( + &account_config, + &mut printer, + &backend, + &folder, + template, + ) + .await; } - Some(email::args::Cmd::Flag(m)) => match m { - Some(flag::args::Cmd::Set(ids, ref flags)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = - Backend::new(toml_account_config, account_config.clone(), false).await?; - return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await; - } - Some(flag::args::Cmd::Add(ids, ref flags)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = - Backend::new(toml_account_config, account_config.clone(), false).await?; - return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await; - } - Some(flag::args::Cmd::Remove(ids, ref flags)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = - Backend::new(toml_account_config, account_config.clone(), false).await?; - return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await; - } - _ => (), - }, - Some(email::args::Cmd::Tpl(m)) => match m { - Some(tpl::args::Cmd::Forward(id, headers, body)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = - Backend::new(toml_account_config, account_config.clone(), false).await?; - return tpl::handlers::forward( - &account_config, - &mut printer, - &backend, - &folder, - id, - headers, - body, - ) - .await; - } - Some(tpl::args::Cmd::Write(headers, body)) => { - return tpl::handlers::write(&account_config, &mut printer, headers, body).await; - } - Some(tpl::args::Cmd::Reply(id, all, headers, body)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = - Backend::new(toml_account_config, account_config.clone(), false).await?; - return tpl::handlers::reply( - &account_config, - &mut printer, - &backend, - &folder, - id, - all, - headers, - body, - ) - .await; - } - Some(tpl::args::Cmd::Save(tpl)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = - Backend::new(toml_account_config, account_config.clone(), false).await?; - return tpl::handlers::save(&account_config, &mut printer, &backend, &folder, tpl) - .await; - } - Some(tpl::args::Cmd::Send(tpl)) => { - let backend = - Backend::new(toml_account_config, account_config.clone(), true).await?; - return tpl::handlers::send(&account_config, &mut printer, &backend, tpl).await; - } - _ => (), - }, - Some(email::args::Cmd::Write(headers, body)) => { + Some(template::args::Cmd::Send(template)) => { let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - return email::handlers::write(&account_config, &mut printer, &backend, headers, body) + return template::handlers::send(&account_config, &mut printer, &backend, template) .await; } _ => (), From d2308221d7ee5ddd8a3d5ac3a1fd44c13aeb391a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 5 Dec 2023 22:38:08 +0100 Subject: [PATCH 15/29] refactor man and completion with clap derive api --- Cargo.lock | 30 ++++++++++-- Cargo.toml | 3 +- src/account/args.rs | 64 +++++++++++++++--------- src/cache/args.rs | 17 ++++--- src/completion/args.rs | 39 --------------- src/completion/command.rs | 10 ++++ src/completion/handler.rs | 18 +++++++ src/completion/handlers.rs | 15 ------ src/completion/mod.rs | 10 +--- src/config/args.rs | 15 ++++-- src/email/envelope/args.rs | 3 +- src/email/envelope/flag/args.rs | 3 +- src/email/message/args.rs | 3 +- src/email/message/template/args.rs | 3 +- src/folder/args.rs | 29 ++++++++--- src/main.rs | 79 +++++++++++++++++------------- src/man/args.rs | 43 ---------------- src/man/command.rs | 22 +++++++++ src/man/handler.rs | 35 +++++++++++++ src/man/handlers.rs | 29 ----------- src/man/mod.rs | 4 +- src/output/args.rs | 29 +++++------ 22 files changed, 270 insertions(+), 233 deletions(-) delete mode 100644 src/completion/args.rs create mode 100644 src/completion/command.rs create mode 100644 src/completion/handler.rs delete mode 100644 src/completion/handlers.rs delete mode 100644 src/man/args.rs create mode 100644 src/man/command.rs create mode 100644 src/man/handler.rs delete mode 100644 src/man/handlers.rs diff --git a/Cargo.lock b/Cargo.lock index 2c12a90..1e66c7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -470,6 +470,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -493,6 +494,18 @@ dependencies = [ "clap", ] +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "clap_lex" version = "0.6.0" @@ -1063,12 +1076,12 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.8.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" dependencies = [ - "atty", "humantime", + "is-terminal", "log", "regex", "termcolor", @@ -2371,6 +2384,17 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.3", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "itoa" version = "1.0.9" diff --git a/Cargo.toml b/Cargo.toml index 9759e3f..2803579 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ version = "0.4.24" [dependencies.clap] version = "4.0" +features = ["derive"] [dependencies.clap_complete] version = "4.0" @@ -80,7 +81,7 @@ version = "4.0.0" version = "0.2.4" [dependencies.env_logger] -version = "0.8" +version = "0.10" [dependencies.erased-serde] version = "0.3" diff --git a/src/account/args.rs b/src/account/args.rs index 0917fb1..da2264c 100644 --- a/src/account/args.rs +++ b/src/account/args.rs @@ -33,12 +33,20 @@ pub enum Cmd { /// Represents the account command matcher. pub fn matches(m: &ArgMatches) -> Result> { let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNT) { - if let Some(m) = m.subcommand_matches(CMD_SYNC) { + if let Some(m) = m.subcommand_matches(CMD_CONFIGURE) { + info!("configure account subcommand matched"); + let reset = parse_reset_flag(m); + Some(Cmd::Configure(reset)) + } else if let Some(m) = m.subcommand_matches(CMD_LIST) { + info!("list accounts subcommand matched"); + let max_table_width = table::args::parse_max_width(m); + Some(Cmd::List(max_table_width)) + } else if let Some(m) = m.subcommand_matches(CMD_SYNC) { info!("sync account subcommand matched"); let dry_run = parse_dry_run_arg(m); let include = folder::args::parse_include_arg(m); let exclude = folder::args::parse_exclude_arg(m); - let folders_strategy = if let Some(folder) = folder::args::parse_source_arg(m) { + let folders_strategy = if let Some(folder) = folder::args::parse_global_source_arg(m) { Some(FolderSyncStrategy::Include(HashSet::from_iter([ folder.to_owned() ]))) @@ -52,17 +60,8 @@ pub fn matches(m: &ArgMatches) -> Result> { None }; Some(Cmd::Sync(folders_strategy, dry_run)) - } else if let Some(m) = m.subcommand_matches(CMD_LIST) { - info!("list accounts subcommand matched"); - let max_table_width = table::args::parse_max_width(m); - Some(Cmd::List(max_table_width)) - } else if let Some(m) = m.subcommand_matches(CMD_CONFIGURE) { - info!("configure account subcommand matched"); - let reset = parse_reset_flag(m); - Some(Cmd::Configure(reset)) } else { - info!("no account subcommand matched, falling back to subcommand list"); - Some(Cmd::List(None)) + None } } else { None @@ -75,10 +74,26 @@ pub fn matches(m: &ArgMatches) -> Result> { pub fn subcmd() -> Command { Command::new(CMD_ACCOUNT) .about("Subcommand to manage accounts") - .subcommands([ + .long_about("Subcommand to manage accounts like configure, list or sync") + .aliases(["accounts", "acc"]) + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + Command::new(CMD_CONFIGURE) + .about("Configure the given account") + .aliases(["config", "conf", "cfg"]) + .arg(reset_flag()) + .arg(folder::args::source_arg( + "Define the account to be configured", + )), + ) + .subcommand( Command::new(CMD_LIST) - .about("List all accounts from the config file") + .about("List all accounts") + .long_about("List all accounts that are set up in the configuration file") .arg(table::args::max_width()), + ) + .subcommand( Command::new(CMD_SYNC) .about("Synchronize the given account locally") .arg(folder::args::all_arg("Synchronize all folders")) @@ -89,26 +104,27 @@ pub fn subcmd() -> Command { "Synchronize all folders except the given ones", )) .arg(dry_run()), - Command::new(CMD_CONFIGURE) - .about("Configure the current selected account") - .aliases(["config", "conf", "cfg"]) - .arg(reset_flag()), - ]) + ) } /// Represents the user account name argument. This argument allows /// the user to select a different account than the default one. -pub fn arg() -> Arg { - Arg::new(ARG_ACCOUNT) - .help("Set the account") +pub fn global_args() -> impl IntoIterator { + [Arg::new(ARG_ACCOUNT) + .help("Override the default account") + .long_help( + "Override the default account + +The given account will be used by default for all other commands (when applicable).", + ) .long("account") .short('a') .global(true) - .value_name("STRING") + .value_name("name")] } /// Represents the user account name argument parser. -pub fn parse_arg(matches: &ArgMatches) -> Option<&str> { +pub fn parse_global_arg(matches: &ArgMatches) -> Option<&str> { matches.get_one::(ARG_ACCOUNT).map(String::as_str) } diff --git a/src/cache/args.rs b/src/cache/args.rs index cd40a09..c32f6e4 100644 --- a/src/cache/args.rs +++ b/src/cache/args.rs @@ -6,19 +6,24 @@ const ARG_DISABLE_CACHE: &str = "disable-cache"; /// Represents the disable cache flag argument. This argument allows /// the user to disable any sort of cache. -pub fn arg() -> Arg { - Arg::new(ARG_DISABLE_CACHE) +pub fn global_args() -> impl IntoIterator { + [Arg::new(ARG_DISABLE_CACHE) .help("Disable any sort of cache") .long_help( - "Disable any sort of cache. The action depends on -the command it applies on.", + "Disable any sort of cache. + +The action depends on commands it apply on. For example, when listing +envelopes using the IMAP backend, this flag will ensure that envelopes +are fetched from the IMAP server and not from the synchronized local +Maildir.", ) .long("disable-cache") + .alias("no-cache") .global(true) - .action(ArgAction::SetTrue) + .action(ArgAction::SetTrue)] } /// Represents the disable cache flag parser. -pub fn parse_disable_cache_flag(m: &ArgMatches) -> bool { +pub fn parse_disable_cache_arg(m: &ArgMatches) -> bool { m.get_flag(ARG_DISABLE_CACHE) } diff --git a/src/completion/args.rs b/src/completion/args.rs deleted file mode 100644 index 00c79e9..0000000 --- a/src/completion/args.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Module related to completion CLI. -//! -//! This module provides subcommands and a command matcher related to completion. - -use anyhow::Result; -use clap::{value_parser, Arg, ArgMatches, Command}; -use clap_complete::Shell; -use log::debug; - -const ARG_SHELL: &str = "shell"; -const CMD_COMPLETION: &str = "completion"; - -type SomeShell = Shell; - -/// Completion commands. -pub enum Cmd { - /// Generate completion script for the given shell. - Generate(SomeShell), -} - -/// Completion command matcher. -pub fn matches(m: &ArgMatches) -> Result> { - if let Some(m) = m.subcommand_matches(CMD_COMPLETION) { - let shell = m.get_one::(ARG_SHELL).cloned().unwrap(); - debug!("shell: {:?}", shell); - return Ok(Some(Cmd::Generate(shell))); - }; - - Ok(None) -} - -/// Completion subcommands. -pub fn subcmd() -> Command { - Command::new(CMD_COMPLETION) - .about("Generate the completion script for the given shell") - .args(&[Arg::new(ARG_SHELL) - .value_parser(value_parser!(Shell)) - .required(true)]) -} diff --git a/src/completion/command.rs b/src/completion/command.rs new file mode 100644 index 0000000..83c4a48 --- /dev/null +++ b/src/completion/command.rs @@ -0,0 +1,10 @@ +use clap::{value_parser, Parser}; +use clap_complete::Shell; + +/// Print completion script for the given shell to stdout +#[derive(Debug, Parser)] +pub struct Generate { + /// Shell that completion script should be generated for + #[arg(value_parser = value_parser!(Shell))] + pub shell: Shell, +} diff --git a/src/completion/handler.rs b/src/completion/handler.rs new file mode 100644 index 0000000..b804a47 --- /dev/null +++ b/src/completion/handler.rs @@ -0,0 +1,18 @@ +use anyhow::Result; +use clap::Command; +use clap_complete::Shell; +use std::io::stdout; + +use crate::printer::Printer; + +pub fn generate(printer: &mut impl Printer, mut cmd: Command, shell: Shell) -> Result<()> { + let name = cmd.get_name().to_string(); + + clap_complete::generate(shell, &mut cmd, name, &mut stdout()); + + printer.print(format!( + "Shell script successfully generated for shell {shell}!" + ))?; + + Ok(()) +} diff --git a/src/completion/handlers.rs b/src/completion/handlers.rs deleted file mode 100644 index 5b36791..0000000 --- a/src/completion/handlers.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! Module related to completion handling. -//! -//! This module gathers all completion commands. - -use anyhow::Result; -use clap::Command; -use clap_complete::Shell; -use std::io::stdout; - -/// Generates completion script from the given [`clap::App`] for the given shell slice. -pub fn generate<'a>(mut cmd: Command, shell: Shell) -> Result<()> { - let name = cmd.get_name().to_string(); - clap_complete::generate(shell, &mut cmd, name, &mut stdout()); - Ok(()) -} diff --git a/src/completion/mod.rs b/src/completion/mod.rs index 12d2685..a2f4939 100644 --- a/src/completion/mod.rs +++ b/src/completion/mod.rs @@ -1,8 +1,2 @@ -//! Module related to shell completion. -//! -//! This module allows users to generate autocompletion scripts for -//! their shells. You can see the list of available shells directly on -//! the clap's [docs.rs](https://docs.rs/clap/2.33.3/clap/enum.Shell.html). - -pub mod args; -pub mod handlers; +pub mod command; +pub mod handler; diff --git a/src/config/args.rs b/src/config/args.rs index ec425f5..655ba7c 100644 --- a/src/config/args.rs +++ b/src/config/args.rs @@ -6,16 +6,21 @@ const ARG_CONFIG: &str = "config"; /// Represents the config file path argument. This argument allows the /// user to customize the config file path. -pub fn arg() -> Arg { - Arg::new(ARG_CONFIG) - .help("Set a custom configuration file path") +pub fn global_args() -> impl IntoIterator { + [Arg::new(ARG_CONFIG) + .help("Override the configuration file path") + .long_help( + "Override the configuration file path + +If the file under the given path does not exist, the wizard will propose to create it.", + ) .long("config") .short('c') .global(true) - .value_name("PATH") + .value_name("path")] } /// Represents the config file path argument parser. -pub fn parse_arg(matches: &ArgMatches) -> Option<&str> { +pub fn parse_global_arg(matches: &ArgMatches) -> Option<&str> { matches.get_one::(ARG_CONFIG).map(String::as_str) } diff --git a/src/email/envelope/args.rs b/src/email/envelope/args.rs index 717e079..8a7e8ce 100644 --- a/src/email/envelope/args.rs +++ b/src/email/envelope/args.rs @@ -43,7 +43,8 @@ pub fn matches(m: &ArgMatches) -> Result> { /// Represents the envelope subcommand. pub fn subcmd() -> Command { Command::new(CMD_ENVELOPE) - .about("Manage envelopes") + .about("Subcommand to manage envelopes") + .long_about("Subcommand to manage envelopes like list") .subcommands([Command::new(CMD_LIST) .alias("lst") .about("List envelopes") diff --git a/src/email/envelope/flag/args.rs b/src/email/envelope/flag/args.rs index fd80935..7eb528a 100644 --- a/src/email/envelope/flag/args.rs +++ b/src/email/envelope/flag/args.rs @@ -57,7 +57,8 @@ pub fn matches(m: &ArgMatches) -> Result> { /// Represents the flag subcommand. pub fn subcmd() -> Command { Command::new(CMD_FLAG) - .about("Manage flags") + .about("Subcommand to manage flags") + .long_about("Subcommand to manage flags like add, set or remove") .subcommand_required(true) .arg_required_else_help(true) .subcommand( diff --git a/src/email/message/args.rs b/src/email/message/args.rs index 92eb099..65448c0 100644 --- a/src/email/message/args.rs +++ b/src/email/message/args.rs @@ -120,7 +120,8 @@ pub fn matches(m: &ArgMatches) -> Result> { /// Represents the email subcommands. pub fn subcmd() -> Command { Command::new(CMD_MESSAGE) - .about("Manage messages") + .about("Subcommand to manage messages") + .long_about("Subcommand to manage messages like read, write, reply or send") .aliases(["msg"]) .subcommand_required(true) .arg_required_else_help(true) diff --git a/src/email/message/template/args.rs b/src/email/message/template/args.rs index 7664aae..a64e7b7 100644 --- a/src/email/message/template/args.rs +++ b/src/email/message/template/args.rs @@ -73,7 +73,8 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { pub fn subcmd() -> Command { Command::new(CMD_TPL) .alias("tpl") - .about("Manage templates") + .about("Subcommand to manage templates") + .long_about("Subcommand to manage templates like write, reply, send or save") .subcommand_required(true) .arg_required_else_help(true) .subcommand( diff --git a/src/folder/args.rs b/src/folder/args.rs index 98f57aa..f4bb7bf 100644 --- a/src/folder/args.rs +++ b/src/folder/args.rs @@ -14,6 +14,7 @@ use crate::ui::table; const ARG_ALL: &str = "all"; const ARG_EXCLUDE: &str = "exclude"; const ARG_INCLUDE: &str = "include"; +const ARG_GLOBAL_SOURCE: &str = "global-source"; const ARG_SOURCE: &str = "source"; const ARG_TARGET: &str = "target"; const CMD_CREATE: &str = "create"; @@ -61,7 +62,8 @@ pub fn matches(m: &ArgMatches) -> Result> { /// Represents the folder subcommand. pub fn subcmd() -> Command { Command::new(CMD_FOLDER) - .about("Manage folders") + .about("Subcommand to manage folders") + .long_about("Subcommand to manage folders like list, expunge or delete") .subcommands([ Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"), Command::new(CMD_CREATE) @@ -77,16 +79,31 @@ pub fn subcmd() -> Command { } /// Represents the source folder argument. -pub fn source_arg() -> Arg { - Arg::new(ARG_SOURCE) - .help("Set the source folder") +pub fn global_args() -> impl IntoIterator { + [Arg::new(ARG_GLOBAL_SOURCE) + .help("Override the default INBOX folder") + .long_help( + "Override the default INBOX folder. + +The given folder will be used by default for all other commands (when +applicable).", + ) .long("folder") .short('f') .global(true) - .value_name("SOURCE") + .value_name("name")] +} + +pub fn parse_global_source_arg(matches: &ArgMatches) -> Option<&str> { + matches + .get_one::(ARG_GLOBAL_SOURCE) + .map(String::as_str) +} + +pub fn source_arg(help: &'static str) -> Arg { + Arg::new(ARG_SOURCE).help(help).value_name("name") } -/// Represents the source folder argument parser. pub fn parse_source_arg(matches: &ArgMatches) -> Option<&str> { matches.get_one::(ARG_SOURCE).map(String::as_str) } diff --git a/src/main.rs b/src/main.rs index b399239..a4d7330 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use ::email::account::{config::DEFAULT_INBOX_FOLDER, sync::AccountSyncBuilder}; use anyhow::{anyhow, Context, Result}; -use clap::Command; +use clap::{Command, CommandFactory, Parser, Subcommand}; +use env_logger::{Builder as LoggerBuilder, Env, DEFAULT_FILTER_ENV}; use log::{debug, warn}; use std::env; use url::Url; @@ -15,20 +16,18 @@ use himalaya::{ template, }; -fn create_app() -> Command { +fn _create_app() -> Command { Command::new(env!("CARGO_PKG_NAME")) .version(env!("CARGO_PKG_VERSION")) .about(env!("CARGO_PKG_DESCRIPTION")) .author(env!("CARGO_PKG_AUTHORS")) .propagate_version(true) .infer_subcommands(true) - .arg(config::args::arg()) - .arg(account::args::arg()) - .arg(cache::args::arg()) - .args(output::args::args()) - .arg(folder::args::source_arg()) - .subcommand(completion::args::subcmd()) - .subcommand(man::args::subcmd()) + .args(config::args::global_args()) + .args(account::args::global_args()) + .args(folder::args::global_args()) + .args(cache::args::global_args()) + .args(output::args::global_args()) .subcommand(account::args::subcmd()) .subcommand(folder::args::subcmd()) .subcommand(envelope::args::subcmd()) @@ -38,7 +37,7 @@ fn create_app() -> Command { } #[tokio::main] -async fn main() -> Result<()> { +async fn _old_main() -> Result<()> { #[cfg(not(target_os = "windows"))] if let Err((_, err)) = coredump::register_panic_handler() { warn!("cannot register custom panic handler: {err}"); @@ -63,32 +62,13 @@ async fn main() -> Result<()> { return message::handlers::mailto(&account_config, &backend, &mut printer, &url).await; } - let app = create_app(); + let app = _create_app(); let m = app.get_matches(); - // check completionetion command before configs - // https://github.com/soywod/himalaya/issues/115 - #[allow(clippy::single_match)] - match completion::args::matches(&m)? { - Some(completion::args::Cmd::Generate(shell)) => { - return completion::handlers::generate(create_app(), shell); - } - _ => (), - } - - // check also man command before configs - #[allow(clippy::single_match)] - match man::args::matches(&m)? { - Some(man::args::Cmd::GenerateAll(dir)) => { - return man::handlers::generate(dir, create_app()); - } - _ => (), - } - - let some_config_path = config::args::parse_arg(&m); - let some_account_name = account::args::parse_arg(&m); - let disable_cache = cache::args::parse_disable_cache_flag(&m); - let folder = folder::args::parse_source_arg(&m); + let some_config_path = config::args::parse_global_arg(&m); + let some_account_name = account::args::parse_global_arg(&m); + let disable_cache = cache::args::parse_disable_cache_arg(&m); + let folder = folder::args::parse_global_source_arg(&m); let toml_config = TomlConfig::from_some_path_or_default(some_config_path).await?; @@ -362,3 +342,34 @@ async fn main() -> Result<()> { Ok(()) } + +#[derive(Parser, Debug)] +#[command(name= "himalaya", author, version, about, long_about = None, propagate_version = true)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + Man(man::command::Generate), + #[command(aliases = ["completions", "compl", "comp"])] + Completion(completion::command::Generate), +} + +#[tokio::main] +async fn main() -> Result<()> { + LoggerBuilder::new() + .parse_env(Env::new().filter_or(DEFAULT_FILTER_ENV, "warn")) + .format_timestamp(None) + .init(); + + let mut printer = StdoutPrinter::default(); + + match Cli::parse().command { + Commands::Man(cmd) => man::handler::generate(&mut printer, Cli::command(), cmd.dir), + Commands::Completion(cmd) => { + completion::handler::generate(&mut printer, Cli::command(), cmd.shell) + } + } +} diff --git a/src/man/args.rs b/src/man/args.rs deleted file mode 100644 index a85f953..0000000 --- a/src/man/args.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Module related to man CLI. -//! -//! This module provides subcommands and a command matcher related to -//! man. - -use anyhow::Result; -use clap::{Arg, ArgMatches, Command}; -use log::debug; - -const ARG_DIR: &str = "dir"; -const CMD_MAN: &str = "man"; - -/// Man commands. -pub enum Cmd<'a> { - /// Generates all man pages to the specified directory. - GenerateAll(&'a str), -} - -/// Man command matcher. -pub fn matches(m: &ArgMatches) -> Result> { - if let Some(m) = m.subcommand_matches(CMD_MAN) { - let dir = m.get_one::(ARG_DIR).map(String::as_str).unwrap(); - debug!("directory: {}", dir); - return Ok(Some(Cmd::GenerateAll(dir))); - }; - - Ok(None) -} - -/// Man subcommands. -pub fn subcmd() -> Command { - Command::new(CMD_MAN) - .about("Generate all man pages to the given directory") - .arg( - Arg::new(ARG_DIR) - .help("Directory to generate man files in") - .long_help( - "Represents the directory where all man files of -all commands and subcommands should be generated in.", - ) - .required(true), - ) -} diff --git a/src/man/command.rs b/src/man/command.rs new file mode 100644 index 0000000..7316b8e --- /dev/null +++ b/src/man/command.rs @@ -0,0 +1,22 @@ +use anyhow::Result; +use clap::Parser; +use shellexpand_utils::{canonicalize, expand}; +use std::path::PathBuf; + +/// Generate all man pages to the given directory +#[derive(Debug, Parser)] +pub struct Generate { + /// Directory where man files should be generated in + #[arg(value_parser = dir_parser)] + pub dir: PathBuf, +} + +/// Parse the given [`str`] as [`PathBuf`]. +/// +/// The path is first shell expanded, then canonicalized (if +/// applicable). +fn dir_parser(path: &str) -> Result { + expand::try_path(path) + .map(canonicalize::path) + .map_err(|err| err.to_string()) +} diff --git a/src/man/handler.rs b/src/man/handler.rs new file mode 100644 index 0000000..069e45e --- /dev/null +++ b/src/man/handler.rs @@ -0,0 +1,35 @@ +use anyhow::Result; +use clap::Command; +use clap_mangen::Man; +use std::{fs, path::PathBuf}; + +use crate::printer::Printer; + +pub fn generate(printer: &mut impl Printer, cmd: Command, dir: PathBuf) -> Result<()> { + let cmd_name = cmd.get_name().to_string(); + let subcmds = cmd.get_subcommands().cloned().collect::>(); + let subcmds_len = subcmds.len() + 1; + + let mut buffer = Vec::new(); + Man::new(cmd).render(&mut buffer)?; + + fs::create_dir_all(&dir)?; + printer.print_log(format!("Generating man page for command {cmd_name}…"))?; + fs::write(dir.join(format!("{}.1", cmd_name)), buffer)?; + + for subcmd in subcmds { + let subcmd_name = subcmd.get_name().to_string(); + + let mut buffer = Vec::new(); + Man::new(subcmd).render(&mut buffer)?; + + printer.print_log(format!("Generating man page for subcommand {subcmd_name}…"))?; + fs::write(dir.join(format!("{}-{}.1", cmd_name, subcmd_name)), buffer)?; + } + + printer.print(format!( + "Successfully generated {subcmds_len} man page(s) in {dir:?}!" + ))?; + + Ok(()) +} diff --git a/src/man/handlers.rs b/src/man/handlers.rs deleted file mode 100644 index bb0e7b6..0000000 --- a/src/man/handlers.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Module related to man handling. -//! -//! This module gathers all man commands. - -use anyhow::Result; -use clap::Command; -use clap_mangen::Man; -use std::{fs, path::PathBuf}; - -/// Generates all man pages of all subcommands in the given directory. -pub fn generate(dir: &str, cmd: Command) -> Result<()> { - let mut buffer = Vec::new(); - let cmd_name = cmd.get_name().to_string(); - let subcmds = cmd.get_subcommands().cloned().collect::>(); - Man::new(cmd).render(&mut buffer)?; - fs::write(PathBuf::from(dir).join(format!("{}.1", cmd_name)), buffer)?; - - for subcmd in subcmds { - let mut buffer = Vec::new(); - let subcmd_name = subcmd.get_name().to_string(); - Man::new(subcmd).render(&mut buffer)?; - fs::write( - PathBuf::from(dir).join(format!("{}-{}.1", cmd_name, subcmd_name)), - buffer, - )?; - } - - Ok(()) -} diff --git a/src/man/mod.rs b/src/man/mod.rs index b0b957b..a2f4939 100644 --- a/src/man/mod.rs +++ b/src/man/mod.rs @@ -1,2 +1,2 @@ -pub mod args; -pub mod handlers; +pub mod command; +pub mod handler; diff --git a/src/output/args.rs b/src/output/args.rs index d5a98a1..812052b 100644 --- a/src/output/args.rs +++ b/src/output/args.rs @@ -8,27 +8,28 @@ pub(crate) const ARG_COLOR: &str = "color"; pub(crate) const ARG_OUTPUT: &str = "output"; /// Output arguments. -pub fn args() -> Vec { - vec![ +pub fn global_args() -> impl IntoIterator { + [ Arg::new(ARG_OUTPUT) - .help("Set the output format") + .help("Define the output format") .long("output") .short('o') .global(true) - .value_name("FMT") + .value_name("format") .value_parser(["plain", "json"]) .default_value("plain"), Arg::new(ARG_COLOR) - .help("Control when to use colors.") + .help("Control when to use colors") .long_help( - "This flag controls when to use colors. The default -setting is 'auto', which means himalaya will try to guess when to use -colors. For example, if himalaya is printing to a terminal, then it -will use colors, but if it is redirected to a file or a pipe, then it -will suppress color output. himalaya will suppress color output in -some other circumstances as well. For example, if the TERM environment -variable is not set or set to 'dumb', then himalaya will not use -colors. + "Control when to use colors. + +The default setting is 'auto', which means himalaya will try to guess +when to use colors. For example, if himalaya is printing to a +terminal, then it will use colors, but if it is redirected to a file +or a pipe, then it will suppress color output. himalaya will suppress +color output in some other circumstances as well. For example, if the +TERM environment variable is not set or set to 'dumb', then himalaya +will not use colors. The possible values for this flag are: @@ -42,6 +43,6 @@ ansi Like 'always', but emits ANSI escapes (even in a Windows console).", .global(true) .value_parser(["never", "auto", "always", "ansi"]) .default_value("auto") - .value_name("WHEN"), + .value_name("mode"), ] } From abe4c7f4ea2181351e5cfa751e3ad0a19531904a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 6 Dec 2023 18:09:49 +0100 Subject: [PATCH 16/29] refactor account with clap derive api --- Cargo.lock | 13 +- Cargo.toml | 6 +- src/account/args.rs | 160 -------- src/account/command/configure.rs | 115 ++++++ src/account/command/list.rs | 141 +++++++ src/account/command/mod.rs | 34 ++ src/account/command/sync.rs | 233 +++++++++++ src/account/handlers.rs | 401 ------------------- src/account/mod.rs | 5 +- src/cli.rs | 113 ++++++ src/completion/command.rs | 26 +- src/completion/handler.rs | 18 - src/completion/mod.rs | 1 - src/config/mod.rs | 35 +- src/folder/args.rs | 13 - src/lib.rs | 1 + src/main.rs | 667 ++++++++++++++----------------- src/man/command.rs | 48 ++- src/man/handler.rs | 35 -- src/man/mod.rs | 1 - src/output/output.rs | 19 +- 21 files changed, 1056 insertions(+), 1029 deletions(-) delete mode 100644 src/account/args.rs create mode 100644 src/account/command/configure.rs create mode 100644 src/account/command/list.rs create mode 100644 src/account/command/mod.rs create mode 100644 src/account/command/sync.rs delete mode 100644 src/account/handlers.rs create mode 100644 src/cli.rs delete mode 100644 src/completion/handler.rs delete mode 100644 src/man/handler.rs diff --git a/Cargo.lock b/Cargo.lock index 1e66c7d..099bdf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -483,6 +483,7 @@ dependencies = [ "anstyle", "clap_lex", "strsim 0.10.0", + "terminal_size 0.3.0", ] [[package]] @@ -2094,7 +2095,7 @@ dependencies = [ "shellexpand-utils 0.1.0", "tempfile", "termcolor", - "terminal_size", + "terminal_size 0.1.17", "tokio", "toml 0.7.8", "toml_edit 0.19.15", @@ -4104,6 +4105,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "thiserror" version = "1.0.50" diff --git a/Cargo.toml b/Cargo.toml index 2803579..21a4b20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,11 +59,11 @@ version = "0.2" version = "0.4.24" [dependencies.clap] -version = "4.0" -features = ["derive"] +version = "4.4" +features = ["derive", "wrap_help"] [dependencies.clap_complete] -version = "4.0" +version = "4.4" [dependencies.clap_mangen] version = "0.2" diff --git a/src/account/args.rs b/src/account/args.rs deleted file mode 100644 index da2264c..0000000 --- a/src/account/args.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! This module provides arguments related to the user account config. - -use anyhow::Result; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use email::folder::sync::FolderSyncStrategy; -use log::info; -use std::collections::HashSet; - -use crate::{folder, ui::table}; - -const ARG_ACCOUNT: &str = "account"; -const ARG_DRY_RUN: &str = "dry-run"; -const ARG_RESET: &str = "reset"; -const CMD_ACCOUNT: &str = "account"; -const CMD_CONFIGURE: &str = "configure"; -const CMD_LIST: &str = "list"; -const CMD_SYNC: &str = "sync"; - -type DryRun = bool; -type Reset = bool; - -/// Represents the account commands. -#[derive(Debug, PartialEq, Eq)] -pub enum Cmd { - /// Represents the list accounts command. - List(table::args::MaxTableWidth), - /// Represents the sync account command. - Sync(Option, DryRun), - /// Configure the current selected account. - Configure(Reset), -} - -/// Represents the account command matcher. -pub fn matches(m: &ArgMatches) -> Result> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNT) { - if let Some(m) = m.subcommand_matches(CMD_CONFIGURE) { - info!("configure account subcommand matched"); - let reset = parse_reset_flag(m); - Some(Cmd::Configure(reset)) - } else if let Some(m) = m.subcommand_matches(CMD_LIST) { - info!("list accounts subcommand matched"); - let max_table_width = table::args::parse_max_width(m); - Some(Cmd::List(max_table_width)) - } else if let Some(m) = m.subcommand_matches(CMD_SYNC) { - info!("sync account subcommand matched"); - let dry_run = parse_dry_run_arg(m); - let include = folder::args::parse_include_arg(m); - let exclude = folder::args::parse_exclude_arg(m); - let folders_strategy = if let Some(folder) = folder::args::parse_global_source_arg(m) { - Some(FolderSyncStrategy::Include(HashSet::from_iter([ - folder.to_owned() - ]))) - } else if !include.is_empty() { - Some(FolderSyncStrategy::Include(include.to_owned())) - } else if !exclude.is_empty() { - Some(FolderSyncStrategy::Exclude(exclude)) - } else if folder::args::parse_all_arg(m) { - Some(FolderSyncStrategy::All) - } else { - None - }; - Some(Cmd::Sync(folders_strategy, dry_run)) - } else { - None - } - } else { - None - }; - - Ok(cmd) -} - -/// Represents the account subcommand. -pub fn subcmd() -> Command { - Command::new(CMD_ACCOUNT) - .about("Subcommand to manage accounts") - .long_about("Subcommand to manage accounts like configure, list or sync") - .aliases(["accounts", "acc"]) - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand( - Command::new(CMD_CONFIGURE) - .about("Configure the given account") - .aliases(["config", "conf", "cfg"]) - .arg(reset_flag()) - .arg(folder::args::source_arg( - "Define the account to be configured", - )), - ) - .subcommand( - Command::new(CMD_LIST) - .about("List all accounts") - .long_about("List all accounts that are set up in the configuration file") - .arg(table::args::max_width()), - ) - .subcommand( - Command::new(CMD_SYNC) - .about("Synchronize the given account locally") - .arg(folder::args::all_arg("Synchronize all folders")) - .arg(folder::args::include_arg( - "Synchronize only the given folders", - )) - .arg(folder::args::exclude_arg( - "Synchronize all folders except the given ones", - )) - .arg(dry_run()), - ) -} - -/// Represents the user account name argument. This argument allows -/// the user to select a different account than the default one. -pub fn global_args() -> impl IntoIterator { - [Arg::new(ARG_ACCOUNT) - .help("Override the default account") - .long_help( - "Override the default account - -The given account will be used by default for all other commands (when applicable).", - ) - .long("account") - .short('a') - .global(true) - .value_name("name")] -} - -/// Represents the user account name argument parser. -pub fn parse_global_arg(matches: &ArgMatches) -> Option<&str> { - matches.get_one::(ARG_ACCOUNT).map(String::as_str) -} - -/// Represents the user account sync dry run flag. This flag allows -/// the user to see the changes of a sync without applying them. -pub fn dry_run() -> Arg { - Arg::new(ARG_DRY_RUN) - .help("Do not apply changes of the synchronization") - .long_help( - "Do not apply changes of the synchronization. -Changes can be visualized with the RUST_LOG=trace environment variable.", - ) - .short('d') - .long("dry-run") - .action(ArgAction::SetTrue) -} - -/// Represents the user account sync dry run flag parser. -pub fn parse_dry_run_arg(m: &ArgMatches) -> bool { - m.get_flag(ARG_DRY_RUN) -} - -pub fn reset_flag() -> Arg { - Arg::new(ARG_RESET) - .help("Reset the configuration") - .short('r') - .long("reset") - .action(ArgAction::SetTrue) -} - -pub fn parse_reset_flag(m: &ArgMatches) -> bool { - m.get_flag(ARG_RESET) -} diff --git a/src/account/command/configure.rs b/src/account/command/configure.rs new file mode 100644 index 0000000..1ce3fb4 --- /dev/null +++ b/src/account/command/configure.rs @@ -0,0 +1,115 @@ +use anyhow::Result; +use clap::Parser; +#[cfg(feature = "imap")] +use email::imap::config::ImapAuthConfig; +#[cfg(feature = "smtp")] +use email::smtp::config::SmtpAuthConfig; +use log::{debug, info, warn}; + +use crate::{ + config::{ + wizard::{prompt_passwd, prompt_secret}, + TomlConfig, + }, + printer::Printer, +}; + +/// Configure the given account +#[derive(Debug, Parser)] +pub struct Command { + /// The name of the account that needs to be configured + /// + /// The account names are taken from the table at the root level + /// of your TOML configuration file. + #[arg(value_name = "NAME")] + pub account_name: String, + + /// Force the account to reconfigure, even if it is already + /// configured + #[arg(long, short)] + pub force: bool, +} + +impl Command { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing account configure command"); + + let (_, account_config) = + config.into_toml_account_config(Some(self.account_name.as_str()))?; + + if self.force { + #[cfg(feature = "imap")] + if let Some(ref config) = account_config.imap { + let reset = match &config.auth { + ImapAuthConfig::Passwd(config) => config.reset(), + ImapAuthConfig::OAuth2(config) => config.reset(), + }; + if let Err(err) = reset { + warn!("error while resetting imap secrets: {err}"); + debug!("error while resetting imap secrets: {err:?}"); + } + } + + #[cfg(feature = "smtp")] + if let Some(ref config) = account_config.smtp { + let reset = match &config.auth { + SmtpAuthConfig::Passwd(config) => config.reset(), + SmtpAuthConfig::OAuth2(config) => config.reset(), + }; + if let Err(err) = reset { + warn!("error while resetting smtp secrets: {err}"); + debug!("error while resetting smtp secrets: {err:?}"); + } + } + + #[cfg(feature = "pgp")] + if let Some(ref config) = account_config.pgp { + account_config.pgp.reset().await?; + } + } + + #[cfg(feature = "imap")] + if let Some(ref config) = account_config.imap { + match &config.auth { + ImapAuthConfig::Passwd(config) => { + config.configure(|| prompt_passwd("IMAP password")).await + } + ImapAuthConfig::OAuth2(config) => { + config + .configure(|| prompt_secret("IMAP OAuth 2.0 client secret")) + .await + } + }?; + } + + #[cfg(feature = "smtp")] + if let Some(ref config) = account_config.smtp { + match &config.auth { + SmtpAuthConfig::Passwd(config) => { + config.configure(|| prompt_passwd("SMTP password")).await + } + SmtpAuthConfig::OAuth2(config) => { + config + .configure(|| prompt_secret("SMTP OAuth 2.0 client secret")) + .await + } + }?; + } + + #[cfg(feature = "pgp")] + if let Some(ref config) = config.pgp { + config + .pgp + .configure(&config.email, || prompt_passwd("PGP secret key password")) + .await?; + } + + printer.print(format!( + "Account {} successfully {}configured!", + self.account_name, + if self.force { "re" } else { "" } + ))?; + + Ok(()) + } +} diff --git a/src/account/command/list.rs b/src/account/command/list.rs new file mode 100644 index 0000000..b0a848e --- /dev/null +++ b/src/account/command/list.rs @@ -0,0 +1,141 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::Accounts, + config::TomlConfig, + printer::{PrintTableOpts, Printer}, +}; + +/// List all accounts +#[derive(Debug, Parser)] +pub struct Command { + /// Define a maximum width for the table + #[arg(long, short = 'w', name = "PIXELS")] + pub max_width: Option, +} + +impl Command { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing account list command"); + + let accounts: Accounts = config.accounts.iter().into(); + + printer.print_table( + Box::new(accounts), + PrintTableOpts { + format: config + .email_reading_format + .as_ref() + .unwrap_or(&Default::default()), + max_width: self.max_width, + }, + )?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use email::{account::config::AccountConfig, imap::config::ImapConfig}; + use std::{collections::HashMap, fmt::Debug, io}; + use termcolor::ColorSpec; + + use crate::{ + account::TomlAccountConfig, + backend::BackendKind, + printer::{Print, PrintTable, WriteColor}, + }; + + use super::*; + + #[test] + fn it_should_match_cmds_accounts() { + #[derive(Debug, Default, Clone)] + struct StringWriter { + content: String, + } + + impl io::Write for StringWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.content + .push_str(&String::from_utf8(buf.to_vec()).unwrap()); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + self.content = String::default(); + Ok(()) + } + } + + impl termcolor::WriteColor for StringWriter { + fn supports_color(&self) -> bool { + false + } + + fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> { + io::Result::Ok(()) + } + + fn reset(&mut self) -> io::Result<()> { + io::Result::Ok(()) + } + } + + impl WriteColor for StringWriter {} + + #[derive(Debug, Default)] + struct PrinterServiceTest { + pub writer: StringWriter, + } + + impl Printer for PrinterServiceTest { + fn print_table( + &mut self, + data: Box, + opts: PrintTableOpts, + ) -> Result<()> { + data.print_table(&mut self.writer, opts)?; + Ok(()) + } + fn print_log(&mut self, _data: T) -> Result<()> { + unimplemented!() + } + fn print(&mut self, _data: T) -> Result<()> { + unimplemented!() + } + fn is_json(&self) -> bool { + unimplemented!() + } + } + + let mut printer = PrinterServiceTest::default(); + let config = AccountConfig::default(); + let deserialized_config = TomlConfig { + accounts: HashMap::from_iter([( + "account-1".into(), + TomlAccountConfig { + default: Some(true), + backend: Some(BackendKind::Imap), + imap: Some(ImapConfig::default()), + ..Default::default() + }, + )]), + ..TomlConfig::default() + }; + + assert!(list(None, &config, &deserialized_config, &mut printer).is_ok()); + assert_eq!( + concat![ + "\n", + "NAME │BACKEND │DEFAULT \n", + "account-1 │imap │yes \n", + "\n" + ], + printer.writer.content + ); + } +} diff --git a/src/account/command/mod.rs b/src/account/command/mod.rs new file mode 100644 index 0000000..ce0d776 --- /dev/null +++ b/src/account/command/mod.rs @@ -0,0 +1,34 @@ +mod configure; +mod list; +mod sync; + +use anyhow::Result; +use clap::Subcommand; + +use crate::{config::TomlConfig, printer::Printer}; + +/// Subcommand to manage accounts +#[derive(Debug, Subcommand)] +pub enum Command { + /// Configure the given account + #[command(alias = "cfg")] + Configure(configure::Command), + + /// List all exsting accounts + #[command(alias = "lst")] + List(list::Command), + + /// Synchronize the given account locally + #[command()] + Sync(sync::Command), +} + +impl Command { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + match self { + Self::Configure(cmd) => cmd.execute(printer, config).await, + Self::List(cmd) => cmd.execute(printer, config).await, + Self::Sync(cmd) => cmd.execute(printer, config).await, + } + } +} diff --git a/src/account/command/sync.rs b/src/account/command/sync.rs new file mode 100644 index 0000000..7096924 --- /dev/null +++ b/src/account/command/sync.rs @@ -0,0 +1,233 @@ +use anyhow::Result; +use clap::{ArgAction, Parser}; +use email::{ + account::sync::{AccountSyncBuilder, AccountSyncProgressEvent}, + folder::sync::FolderSyncStrategy, +}; +use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle}; +use log::info; +use once_cell::sync::Lazy; +use std::{ + collections::{HashMap, HashSet}, + sync::Mutex, +}; + +use crate::{backend::BackendBuilder, config::TomlConfig, printer::Printer}; + +const MAIN_PROGRESS_STYLE: Lazy = Lazy::new(|| { + ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap() +}); + +const SUB_PROGRESS_STYLE: Lazy = Lazy::new(|| { + ProgressStyle::with_template( + " {prefix:.bold} — {wide_msg:.dim} \n {wide_bar:.black/black} {percent}% ", + ) + .unwrap() +}); + +const SUB_PROGRESS_DONE_STYLE: Lazy = Lazy::new(|| { + ProgressStyle::with_template(" {prefix:.bold} \n {wide_bar:.green} {percent}% ").unwrap() +}); + +#[derive(Debug, Parser)] +pub struct Command { + /// The name of the account that needs to be synchronized + /// + /// The account names are taken from the table at the root level + /// of your TOML configuration file. + #[arg(value_name = "ACCOUNT")] + pub account_name: String, + + /// Run the synchronization without applying changes + /// + /// Instead, a report will be printed to stdout containing all the + /// changes the synchronization plan to do. + #[arg(long, short)] + pub dry_run: bool, + + #[arg(long, short = 'f', value_name = "FOLDER", action = ArgAction::Append, conflicts_with = "exclude_folder", conflicts_with = "all_folders")] + pub include_folder: Vec, + + #[arg(long, short = 'x', value_name = "FOLDER", action = ArgAction::Append, conflicts_with = "include_folder", conflicts_with = "all_folders")] + pub exclude_folder: Vec, + + #[arg( + long, + short = 'A', + conflicts_with = "include_folder", + conflicts_with = "exclude_folder" + )] + pub all_folders: bool, +} + +impl Command { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing account sync command"); + + let included_folders = HashSet::from_iter(self.include_folder); + let excluded_folders = HashSet::from_iter(self.exclude_folder); + + let strategy = if !included_folders.is_empty() { + Some(FolderSyncStrategy::Include(included_folders)) + } else if !excluded_folders.is_empty() { + Some(FolderSyncStrategy::Exclude(excluded_folders)) + } else if self.all_folders { + Some(FolderSyncStrategy::All) + } else { + None + }; + + let (toml_account_config, account_config) = config + .clone() + .into_account_configs(Some(self.account_name.as_str()), true)?; + + let backend_builder = + BackendBuilder::new(toml_account_config, account_config.clone(), false).await?; + let sync_builder = AccountSyncBuilder::new(backend_builder.into()) + .await? + .with_some_folders_strategy(strategy) + .with_dry_run(self.dry_run); + + if self.dry_run { + let report = sync_builder.sync().await?; + let mut hunks_count = report.folders_patch.len(); + + if !report.folders_patch.is_empty() { + printer.print_log("Folders patch:")?; + for (hunk, _) in report.folders_patch { + printer.print_log(format!(" - {hunk}"))?; + } + printer.print_log("")?; + } + + if !report.emails_patch.is_empty() { + printer.print_log("Envelopes patch:")?; + for (hunk, _) in report.emails_patch { + hunks_count += 1; + printer.print_log(format!(" - {hunk}"))?; + } + printer.print_log("")?; + } + + printer.print(format!( + "Estimated patch length for account to be synchronized: {hunks_count}", + ))?; + } else if printer.is_json() { + sync_builder.sync().await?; + printer.print("Account successfully synchronized!")?; + } else { + let multi = MultiProgress::new(); + let sub_progresses = Mutex::new(HashMap::new()); + let main_progress = multi.add( + ProgressBar::new(100) + .with_style(MAIN_PROGRESS_STYLE.clone()) + .with_message("Synchronizing folders…"), + ); + + // Force the progress bar to show + main_progress.set_position(0); + + let report = sync_builder + .with_on_progress(move |evt| { + use AccountSyncProgressEvent::*; + Ok(match evt { + ApplyFolderPatches(..) => { + main_progress.inc(3); + } + ApplyEnvelopePatches(patches) => { + let mut envelopes_progresses = sub_progresses.lock().unwrap(); + let patches_len = + patches.values().fold(0, |sum, patch| sum + patch.len()); + main_progress.set_length((110 * patches_len / 100) as u64); + main_progress.set_position((5 * patches_len / 100) as u64); + main_progress.set_message("Synchronizing envelopes…"); + + for (folder, patch) in patches { + let progress = ProgressBar::new(patch.len() as u64) + .with_style(SUB_PROGRESS_STYLE.clone()) + .with_prefix(folder.clone()) + .with_finish(ProgressFinish::AndClear); + let progress = multi.add(progress); + envelopes_progresses.insert(folder, progress.clone()); + } + } + ApplyEnvelopeHunk(hunk) => { + main_progress.inc(1); + let mut progresses = sub_progresses.lock().unwrap(); + if let Some(progress) = progresses.get_mut(hunk.folder()) { + progress.inc(1); + if progress.position() == (progress.length().unwrap() - 1) { + progress.set_style(SUB_PROGRESS_DONE_STYLE.clone()) + } else { + progress.set_message(format!("{hunk}…")); + } + } + } + ApplyEnvelopeCachePatch(_patch) => { + main_progress.set_length(100); + main_progress.set_position(95); + main_progress.set_message("Saving cache database…"); + } + ExpungeFolders(folders) => { + let mut progresses = sub_progresses.lock().unwrap(); + for progress in progresses.values() { + progress.finish_and_clear() + } + progresses.clear(); + + main_progress.set_position(100); + main_progress + .set_message(format!("Expunging {} folders…", folders.len())); + } + _ => (), + }) + }) + .sync() + .await?; + + let folders_patch_err = report + .folders_patch + .iter() + .filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err))) + .collect::>(); + if !folders_patch_err.is_empty() { + printer.print_log("")?; + printer.print_log("Errors occurred while applying the folders patch:")?; + folders_patch_err + .iter() + .try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?; + } + + if let Some(err) = report.folders_cache_patch.1 { + printer.print_log("")?; + printer.print_log(format!( + "Error occurred while applying the folder cache patch: {err}" + ))?; + } + + let envelopes_patch_err = report + .emails_patch + .iter() + .filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err))) + .collect::>(); + if !envelopes_patch_err.is_empty() { + printer.print_log("")?; + printer.print_log("Errors occurred while applying the envelopes patch:")?; + for (hunk, err) in folders_patch_err { + printer.print_log(format!(" - {hunk}: {err}"))?; + } + } + + if let Some(err) = report.emails_cache_patch.1 { + printer.print_log("")?; + printer.print_log(format!( + "Error occurred while applying the envelopes cache patch: {err}" + ))?; + } + + printer.print("Account successfully synchronized!")?; + } + + Ok(()) + } +} diff --git a/src/account/handlers.rs b/src/account/handlers.rs deleted file mode 100644 index f1f15be..0000000 --- a/src/account/handlers.rs +++ /dev/null @@ -1,401 +0,0 @@ -//! Account handlers module. -//! -//! This module gathers all account actions triggered by the CLI. - -use anyhow::Result; -use email::account::{ - config::AccountConfig, - sync::{AccountSyncBuilder, AccountSyncProgressEvent}, -}; -#[cfg(feature = "imap")] -use email::imap::config::ImapAuthConfig; -#[cfg(feature = "smtp")] -use email::smtp::config::SmtpAuthConfig; -use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle}; -use log::{debug, info, trace, warn}; -use once_cell::sync::Lazy; -use std::{collections::HashMap, sync::Mutex}; - -use crate::{ - account::Accounts, - backend::BackendContextBuilder, - config::{ - wizard::{prompt_passwd, prompt_secret}, - TomlConfig, - }, - printer::{PrintTableOpts, Printer}, -}; - -use super::TomlAccountConfig; - -const MAIN_PROGRESS_STYLE: Lazy = Lazy::new(|| { - ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap() -}); - -const SUB_PROGRESS_STYLE: Lazy = Lazy::new(|| { - ProgressStyle::with_template( - " {prefix:.bold} — {wide_msg:.dim} \n {wide_bar:.black/black} {percent}% ", - ) - .unwrap() -}); - -const SUB_PROGRESS_DONE_STYLE: Lazy = Lazy::new(|| { - ProgressStyle::with_template(" {prefix:.bold} \n {wide_bar:.green} {percent}% ").unwrap() -}); - -/// Configure the current selected account -pub async fn configure(config: &TomlAccountConfig, reset: bool) -> Result<()> { - info!("entering the configure account handler"); - - if reset { - #[cfg(feature = "imap")] - if let Some(ref config) = config.imap { - let reset = match &config.auth { - ImapAuthConfig::Passwd(config) => config.reset(), - ImapAuthConfig::OAuth2(config) => config.reset(), - }; - if let Err(err) = reset { - warn!("error while resetting imap secrets: {err}"); - debug!("error while resetting imap secrets: {err:?}"); - } - } - - #[cfg(feature = "smtp")] - if let Some(ref config) = config.smtp { - let reset = match &config.auth { - SmtpAuthConfig::Passwd(config) => config.reset(), - SmtpAuthConfig::OAuth2(config) => config.reset(), - }; - if let Err(err) = reset { - warn!("error while resetting smtp secrets: {err}"); - debug!("error while resetting smtp secrets: {err:?}"); - } - } - - #[cfg(feature = "pgp")] - if let Some(ref config) = config.pgp { - config.pgp.reset().await?; - } - } - - #[cfg(feature = "imap")] - if let Some(ref config) = config.imap { - match &config.auth { - ImapAuthConfig::Passwd(config) => { - config.configure(|| prompt_passwd("IMAP password")).await - } - ImapAuthConfig::OAuth2(config) => { - config - .configure(|| prompt_secret("IMAP OAuth 2.0 client secret")) - .await - } - }?; - } - - #[cfg(feature = "smtp")] - if let Some(ref config) = config.smtp { - match &config.auth { - SmtpAuthConfig::Passwd(config) => { - config.configure(|| prompt_passwd("SMTP password")).await - } - SmtpAuthConfig::OAuth2(config) => { - config - .configure(|| prompt_secret("SMTP OAuth 2.0 client secret")) - .await - } - }?; - } - - #[cfg(feature = "pgp")] - if let Some(ref config) = config.pgp { - config - .pgp - .configure(&config.email, || prompt_passwd("PGP secret key password")) - .await?; - } - - println!( - "Account successfully {}configured!", - if reset { "re" } else { "" } - ); - - Ok(()) -} - -/// Lists all accounts. -pub fn list<'a, P: Printer>( - max_width: Option, - config: &AccountConfig, - deserialized_config: &TomlConfig, - printer: &mut P, -) -> Result<()> { - info!("entering the list accounts handler"); - - let accounts: Accounts = deserialized_config.accounts.iter().into(); - trace!("accounts: {:?}", accounts); - - printer.print_table( - Box::new(accounts), - PrintTableOpts { - format: &config.email_reading_format, - max_width, - }, - )?; - - info!("<< account list handler"); - Ok(()) -} - -/// Synchronizes the account defined using argument `-a|--account`. If -/// no account given, synchronizes the default one. -pub async fn sync( - printer: &mut P, - sync_builder: AccountSyncBuilder, - dry_run: bool, -) -> Result<()> { - info!("entering the sync accounts handler"); - trace!("dry run: {dry_run}"); - - if dry_run { - let report = sync_builder.sync().await?; - let mut hunks_count = report.folders_patch.len(); - - if !report.folders_patch.is_empty() { - printer.print_log("Folders patch:")?; - for (hunk, _) in report.folders_patch { - printer.print_log(format!(" - {hunk}"))?; - } - printer.print_log("")?; - } - - if !report.emails_patch.is_empty() { - printer.print_log("Envelopes patch:")?; - for (hunk, _) in report.emails_patch { - hunks_count += 1; - printer.print_log(format!(" - {hunk}"))?; - } - printer.print_log("")?; - } - - printer.print(format!( - "Estimated patch length for account to be synchronized: {hunks_count}", - ))?; - } else if printer.is_json() { - sync_builder.sync().await?; - printer.print("Account successfully synchronized!")?; - } else { - let multi = MultiProgress::new(); - let sub_progresses = Mutex::new(HashMap::new()); - let main_progress = multi.add( - ProgressBar::new(100) - .with_style(MAIN_PROGRESS_STYLE.clone()) - .with_message("Synchronizing folders…"), - ); - - // Force the progress bar to show - main_progress.set_position(0); - - let report = sync_builder - .with_on_progress(move |evt| { - use AccountSyncProgressEvent::*; - Ok(match evt { - ApplyFolderPatches(..) => { - main_progress.inc(3); - } - ApplyEnvelopePatches(patches) => { - let mut envelopes_progresses = sub_progresses.lock().unwrap(); - let patches_len = patches.values().fold(0, |sum, patch| sum + patch.len()); - main_progress.set_length((110 * patches_len / 100) as u64); - main_progress.set_position((5 * patches_len / 100) as u64); - main_progress.set_message("Synchronizing envelopes…"); - - for (folder, patch) in patches { - let progress = ProgressBar::new(patch.len() as u64) - .with_style(SUB_PROGRESS_STYLE.clone()) - .with_prefix(folder.clone()) - .with_finish(ProgressFinish::AndClear); - let progress = multi.add(progress); - envelopes_progresses.insert(folder, progress.clone()); - } - } - ApplyEnvelopeHunk(hunk) => { - main_progress.inc(1); - let mut progresses = sub_progresses.lock().unwrap(); - if let Some(progress) = progresses.get_mut(hunk.folder()) { - progress.inc(1); - if progress.position() == (progress.length().unwrap() - 1) { - progress.set_style(SUB_PROGRESS_DONE_STYLE.clone()) - } else { - progress.set_message(format!("{hunk}…")); - } - } - } - ApplyEnvelopeCachePatch(_patch) => { - main_progress.set_length(100); - main_progress.set_position(95); - main_progress.set_message("Saving cache database…"); - } - ExpungeFolders(folders) => { - let mut progresses = sub_progresses.lock().unwrap(); - for progress in progresses.values() { - progress.finish_and_clear() - } - progresses.clear(); - - main_progress.set_position(100); - main_progress.set_message(format!("Expunging {} folders…", folders.len())); - } - _ => (), - }) - }) - .sync() - .await?; - - let folders_patch_err = report - .folders_patch - .iter() - .filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err))) - .collect::>(); - if !folders_patch_err.is_empty() { - printer.print_log("")?; - printer.print_log("Errors occurred while applying the folders patch:")?; - folders_patch_err - .iter() - .try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?; - } - - if let Some(err) = report.folders_cache_patch.1 { - printer.print_log("")?; - printer.print_log(format!( - "Error occurred while applying the folder cache patch: {err}" - ))?; - } - - let envelopes_patch_err = report - .emails_patch - .iter() - .filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err))) - .collect::>(); - if !envelopes_patch_err.is_empty() { - printer.print_log("")?; - printer.print_log("Errors occurred while applying the envelopes patch:")?; - for (hunk, err) in folders_patch_err { - printer.print_log(format!(" - {hunk}: {err}"))?; - } - } - - if let Some(err) = report.emails_cache_patch.1 { - printer.print_log("")?; - printer.print_log(format!( - "Error occurred while applying the envelopes cache patch: {err}" - ))?; - } - - printer.print("Account successfully synchronized!")?; - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use email::{account::config::AccountConfig, imap::config::ImapConfig}; - use std::{collections::HashMap, fmt::Debug, io}; - use termcolor::ColorSpec; - - use crate::{ - account::TomlAccountConfig, - backend::BackendKind, - printer::{Print, PrintTable, WriteColor}, - }; - - use super::*; - - #[test] - fn it_should_match_cmds_accounts() { - #[derive(Debug, Default, Clone)] - struct StringWriter { - content: String, - } - - impl io::Write for StringWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.content - .push_str(&String::from_utf8(buf.to_vec()).unwrap()); - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - self.content = String::default(); - Ok(()) - } - } - - impl termcolor::WriteColor for StringWriter { - fn supports_color(&self) -> bool { - false - } - - fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> { - io::Result::Ok(()) - } - - fn reset(&mut self) -> io::Result<()> { - io::Result::Ok(()) - } - } - - impl WriteColor for StringWriter {} - - #[derive(Debug, Default)] - struct PrinterServiceTest { - pub writer: StringWriter, - } - - impl Printer for PrinterServiceTest { - fn print_table( - &mut self, - data: Box, - opts: PrintTableOpts, - ) -> Result<()> { - data.print_table(&mut self.writer, opts)?; - Ok(()) - } - fn print_log(&mut self, _data: T) -> Result<()> { - unimplemented!() - } - fn print(&mut self, _data: T) -> Result<()> { - unimplemented!() - } - fn is_json(&self) -> bool { - unimplemented!() - } - } - - let mut printer = PrinterServiceTest::default(); - let config = AccountConfig::default(); - let deserialized_config = TomlConfig { - accounts: HashMap::from_iter([( - "account-1".into(), - TomlAccountConfig { - default: Some(true), - backend: Some(BackendKind::Imap), - imap: Some(ImapConfig::default()), - ..Default::default() - }, - )]), - ..TomlConfig::default() - }; - - assert!(list(None, &config, &deserialized_config, &mut printer).is_ok()); - assert_eq!( - concat![ - "\n", - "NAME │BACKEND │DEFAULT \n", - "account-1 │imap │yes \n", - "\n" - ], - printer.writer.content - ); - } -} diff --git a/src/account/mod.rs b/src/account/mod.rs index 1c5708d..b463195 100644 --- a/src/account/mod.rs +++ b/src/account/mod.rs @@ -1,6 +1,5 @@ -pub mod args; +pub mod command; pub mod config; -pub mod handlers; pub(crate) mod wizard; use anyhow::Result; @@ -45,7 +44,7 @@ impl Table for Account { fn head() -> Row { Row::new() .cell(Cell::new("NAME").shrinkable().bold().underline().white()) - .cell(Cell::new("BACKEND").bold().underline().white()) + .cell(Cell::new("BACKENDS").bold().underline().white()) .cell(Cell::new("DEFAULT").bold().underline().white()) } diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..481320e --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,113 @@ +use std::path::PathBuf; + +use anyhow::Result; +use clap::{Parser, Subcommand}; + +use crate::{ + account::command as account, + completion::command as completion, + config::{self, TomlConfig}, + man::command as man, + output::{ColorFmt, OutputFmt}, + printer::Printer, +}; + +#[derive(Parser, Debug)] +#[command( + name = "himalaya", + author, + version, + about, + propagate_version = true, + infer_subcommands = true +)] +pub struct Cli { + #[command(subcommand)] + pub command: Command, + + /// Override the default configuration file path + /// + /// The given path is shell-expanded then canonicalized (if + /// applicable). If the path does not point to a valid file, the + /// wizard will propose to assist you in the creation of the + /// configuration file. + #[arg(long, short, value_name = "PATH", global = true, value_parser = config::path_parser)] + pub config: Option, + + /// Customize the output format + /// + /// The output format determine how to display commands output to + /// the terminal. + /// + /// The possible values are: + /// + /// - json: output will be in a form of a JSON-compatible object + /// + /// - plain: output will be in a form of either a plain text or + /// table, depending on the command + #[arg( + long, + short, + value_name = "FORMAT", + global = true, + value_enum, + default_value_t = Default::default(), + )] + pub output: OutputFmt, + + /// Control when to use colors + /// + /// The default setting is 'auto', which means himalaya will try + /// to guess when to use colors. For example, if himalaya is + /// printing to a terminal, then it will use colors, but if it is + /// redirected to a file or a pipe, then it will suppress color + /// output. himalaya will suppress color output in some other + /// circumstances as well. For example, if the TERM environment + /// variable is not set or set to 'dumb', then himalaya will not + /// use colors. + /// + /// The possible values are: + /// + /// - never: colors will never be used + /// + /// - always: colors will always be used regardless of where output is sent + /// + /// - ansi: like 'always', but emits ANSI escapes (even in a Windows console) + /// + /// - auto: himalaya tries to be smart + #[arg( + long, + short = 'C', + value_name = "MODE", + global = true, + value_enum, + default_value_t = Default::default(), + )] + pub color: ColorFmt, +} + +/// Top-level CLI commands. +#[derive(Subcommand, Debug)] +pub enum Command { + /// Subcommand to manage accounts + #[command(subcommand)] + Account(account::Command), + + /// Generate all man pages to the given directory + #[command(arg_required_else_help = true, alias = "mans")] + Man(man::Command), + + /// Print completion script for the given shell to stdout + #[command(arg_required_else_help = true, aliases = ["completions", "compl", "comp"])] + Completion(completion::Command), +} + +impl Command { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + match self { + Self::Account(cmd) => cmd.execute(printer, config).await, + Self::Man(cmd) => cmd.execute(printer).await, + Self::Completion(cmd) => cmd.execute(printer).await, + } + } +} diff --git a/src/completion/command.rs b/src/completion/command.rs index 83c4a48..aecefcc 100644 --- a/src/completion/command.rs +++ b/src/completion/command.rs @@ -1,10 +1,32 @@ -use clap::{value_parser, Parser}; +use anyhow::Result; +use clap::{value_parser, CommandFactory, Parser}; use clap_complete::Shell; +use log::info; +use std::io; + +use crate::{cli::Cli, printer::Printer}; /// Print completion script for the given shell to stdout #[derive(Debug, Parser)] -pub struct Generate { +pub struct Command { /// Shell that completion script should be generated for #[arg(value_parser = value_parser!(Shell))] pub shell: Shell, } + +impl Command { + pub async fn execute(self, printer: &mut impl Printer) -> Result<()> { + info!("executing completion generate command"); + + let mut cmd = Cli::command(); + let name = cmd.get_name().to_string(); + clap_complete::generate(self.shell, &mut cmd, name, &mut io::stdout()); + + printer.print(format!( + "Shell script successfully generated for shell {}!", + self.shell + ))?; + + Ok(()) + } +} diff --git a/src/completion/handler.rs b/src/completion/handler.rs deleted file mode 100644 index b804a47..0000000 --- a/src/completion/handler.rs +++ /dev/null @@ -1,18 +0,0 @@ -use anyhow::Result; -use clap::Command; -use clap_complete::Shell; -use std::io::stdout; - -use crate::printer::Printer; - -pub fn generate(printer: &mut impl Printer, mut cmd: Command, shell: Shell) -> Result<()> { - let name = cmd.get_name().to_string(); - - clap_complete::generate(shell, &mut cmd, name, &mut stdout()); - - printer.print(format!( - "Shell script successfully generated for shell {shell}!" - ))?; - - Ok(()) -} diff --git a/src/completion/mod.rs b/src/completion/mod.rs index a2f4939..9fe7961 100644 --- a/src/completion/mod.rs +++ b/src/completion/mod.rs @@ -1,2 +1 @@ pub mod command; -pub mod handler; diff --git a/src/config/mod.rs b/src/config/mod.rs index e54f0df..25559c1 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,8 +1,3 @@ -//! Deserialized config module. -//! -//! This module contains the raw deserialized representation of the -//! user configuration file. - pub mod args; pub mod prelude; pub mod wizard; @@ -16,6 +11,7 @@ use email::{ email::config::{EmailHooks, EmailTextPlainFormat}, }; use serde::{Deserialize, Serialize}; +use shellexpand_utils::{canonicalize, expand}; use std::{ collections::HashMap, fs, @@ -162,12 +158,10 @@ impl TomlConfig { .filter(|p| p.exists()) } - /// Build account configurations from a given account name. - pub fn into_account_configs( - self, + pub fn into_toml_account_config( + &self, account_name: Option<&str>, - disable_cache: bool, - ) -> Result<(TomlAccountConfig, AccountConfig)> { + ) -> Result<(String, TomlAccountConfig)> { let (account_name, mut toml_account_config) = match account_name { Some("default") | Some("") | None => self .accounts @@ -200,6 +194,18 @@ impl TomlConfig { .replace_undefined_keyring_entries(&account_name); } + Ok((account_name, toml_account_config)) + } + + /// Build account configurations from a given account name. + pub fn into_account_configs( + self, + account_name: Option<&str>, + disable_cache: bool, + ) -> Result<(TomlAccountConfig, AccountConfig)> { + let (account_name, mut toml_account_config) = + self.into_toml_account_config(account_name)?; + if let Some(true) = toml_account_config.sync { if !disable_cache { toml_account_config.backend = Some(BackendKind::MaildirForSync); @@ -267,6 +273,15 @@ impl TomlConfig { } } +/// Parse a configuration file path as [`PathBuf`]. +/// +/// The path is shell-expanded then canonicalized (if applicable). +pub fn path_parser(path: &str) -> Result { + expand::try_path(path) + .map(canonicalize::path) + .map_err(|err| err.to_string()) +} + #[cfg(test)] mod tests { use email::{ diff --git a/src/folder/args.rs b/src/folder/args.rs index f4bb7bf..f9d0c7e 100644 --- a/src/folder/args.rs +++ b/src/folder/args.rs @@ -121,11 +121,6 @@ pub fn all_arg(help: &'static str) -> Arg { .conflicts_with(ARG_EXCLUDE) } -/// Represents the all folders argument parser. -pub fn parse_all_arg(m: &ArgMatches) -> bool { - m.get_flag(ARG_ALL) -} - /// Represents the folders to include argument. pub fn include_arg(help: &'static str) -> Arg { Arg::new(ARG_INCLUDE) @@ -141,14 +136,6 @@ pub fn include_arg(help: &'static str) -> Arg { .conflicts_with(ARG_EXCLUDE) } -/// Represents the folders to include argument parser. -pub fn parse_include_arg(m: &ArgMatches) -> HashSet { - m.get_many::(ARG_INCLUDE) - .unwrap_or_default() - .map(ToOwned::to_owned) - .collect() -} - /// Represents the folders to exclude argument. pub fn exclude_arg(help: &'static str) -> Arg { Arg::new(ARG_EXCLUDE) diff --git a/src/lib.rs b/src/lib.rs index 79c29ef..8cb19cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod account; pub mod backend; pub mod cache; +pub mod cli; pub mod completion; pub mod config; pub mod email; diff --git a/src/main.rs b/src/main.rs index a4d7330..d80c198 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,361 +1,7 @@ -use ::email::account::{config::DEFAULT_INBOX_FOLDER, sync::AccountSyncBuilder}; -use anyhow::{anyhow, Context, Result}; -use clap::{Command, CommandFactory, Parser, Subcommand}; +use anyhow::Result; +use clap::Parser; use env_logger::{Builder as LoggerBuilder, Env, DEFAULT_FILTER_ENV}; -use log::{debug, warn}; -use std::env; -use url::Url; - -use himalaya::{ - account, - backend::{Backend, BackendBuilder}, - cache, completion, - config::{self, TomlConfig}, - envelope, flag, folder, man, message, output, - printer::StdoutPrinter, - template, -}; - -fn _create_app() -> Command { - Command::new(env!("CARGO_PKG_NAME")) - .version(env!("CARGO_PKG_VERSION")) - .about(env!("CARGO_PKG_DESCRIPTION")) - .author(env!("CARGO_PKG_AUTHORS")) - .propagate_version(true) - .infer_subcommands(true) - .args(config::args::global_args()) - .args(account::args::global_args()) - .args(folder::args::global_args()) - .args(cache::args::global_args()) - .args(output::args::global_args()) - .subcommand(account::args::subcmd()) - .subcommand(folder::args::subcmd()) - .subcommand(envelope::args::subcmd()) - .subcommand(flag::args::subcmd()) - .subcommand(message::args::subcmd()) - .subcommand(template::args::subcmd()) -} - -#[tokio::main] -async fn _old_main() -> Result<()> { - #[cfg(not(target_os = "windows"))] - if let Err((_, err)) = coredump::register_panic_handler() { - warn!("cannot register custom panic handler: {err}"); - debug!("cannot register custom panic handler: {err:?}"); - } - - let default_env_filter = env_logger::DEFAULT_FILTER_ENV; - env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off")); - - // check mailto command before app initialization - let raw_args: Vec = env::args().collect(); - if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { - let url = Url::parse(&raw_args[1])?; - let (toml_account_config, account_config) = TomlConfig::from_default_paths() - .await? - .into_account_configs(None, false)?; - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), true).await?; - let backend = backend_builder.build().await?; - let mut printer = StdoutPrinter::default(); - - return message::handlers::mailto(&account_config, &backend, &mut printer, &url).await; - } - - let app = _create_app(); - let m = app.get_matches(); - - let some_config_path = config::args::parse_global_arg(&m); - let some_account_name = account::args::parse_global_arg(&m); - let disable_cache = cache::args::parse_disable_cache_arg(&m); - let folder = folder::args::parse_global_source_arg(&m); - - let toml_config = TomlConfig::from_some_path_or_default(some_config_path).await?; - - let mut printer = StdoutPrinter::try_from(&m)?; - - // FIXME - // #[cfg(feature = "imap")] - // if let BackendConfig::Imap(imap_config) = &account_config.backend { - // let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - // match imap::args::matches(&m)? { - // Some(imap::args::Cmd::Notify(keepalive)) => { - // let backend = - // ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?; - // imap::handlers::notify(&mut backend, &folder, keepalive).await?; - // return Ok(()); - // } - // Some(imap::args::Cmd::Watch(keepalive)) => { - // let backend = - // ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?; - // imap::handlers::watch(&mut backend, &folder, keepalive).await?; - // return Ok(()); - // } - // _ => (), - // } - // } - - match account::args::matches(&m)? { - Some(account::args::Cmd::List(max_width)) => { - let (_, account_config) = toml_config - .clone() - .into_account_configs(some_account_name, disable_cache)?; - return account::handlers::list(max_width, &account_config, &toml_config, &mut printer); - } - Some(account::args::Cmd::Sync(strategy, dry_run)) => { - let (toml_account_config, account_config) = toml_config - .clone() - .into_account_configs(some_account_name, true)?; - let backend_builder = - BackendBuilder::new(toml_account_config, account_config.clone(), false).await?; - let sync_builder = AccountSyncBuilder::new(backend_builder.into()) - .await? - .with_some_folders_strategy(strategy) - .with_dry_run(dry_run); - return account::handlers::sync(&mut printer, sync_builder, dry_run).await; - } - Some(account::args::Cmd::Configure(reset)) => { - let (toml_account_config, _) = toml_config - .clone() - .into_account_configs(some_account_name, disable_cache)?; - return account::handlers::configure(&toml_account_config, reset).await; - } - _ => (), - } - - let (toml_account_config, account_config) = toml_config - .clone() - .into_account_configs(some_account_name, disable_cache)?; - - // checks folder commands - match folder::args::matches(&m)? { - Some(folder::args::Cmd::Create) => { - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - let folder = folder - .ok_or_else(|| anyhow!("the folder argument is missing")) - .context("cannot create folder")?; - return folder::handlers::create(&mut printer, &backend, &folder).await; - } - Some(folder::args::Cmd::List(max_width)) => { - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return folder::handlers::list(&account_config, &mut printer, &backend, max_width) - .await; - } - Some(folder::args::Cmd::Expunge) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return folder::handlers::expunge(&mut printer, &backend, &folder).await; - } - Some(folder::args::Cmd::Delete) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return folder::handlers::delete(&mut printer, &backend, &folder).await; - } - _ => (), - } - - match envelope::args::matches(&m)? { - Some(envelope::args::Cmd::List(max_width, page_size, page)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return envelope::handlers::list( - &account_config, - &mut printer, - &backend, - &folder, - max_width, - page_size, - page, - ) - .await; - } - _ => (), - } - - match flag::args::matches(&m)? { - Some(flag::args::Cmd::Set(ids, ref flags)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await; - } - Some(flag::args::Cmd::Add(ids, ref flags)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await; - } - Some(flag::args::Cmd::Remove(ids, ref flags)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await; - } - _ => (), - } - - match message::args::matches(&m)? { - Some(message::args::Cmd::Attachments(ids)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return message::handlers::attachments( - &account_config, - &mut printer, - &backend, - &folder, - ids, - ) - .await; - } - Some(message::args::Cmd::Copy(ids, to_folder)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return message::handlers::copy(&mut printer, &backend, &folder, to_folder, ids).await; - } - Some(message::args::Cmd::Delete(ids)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return message::handlers::delete(&mut printer, &backend, &folder, ids).await; - } - Some(message::args::Cmd::Forward(id, headers, body)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - return message::handlers::forward( - &account_config, - &mut printer, - &backend, - &folder, - id, - headers, - body, - ) - .await; - } - Some(message::args::Cmd::Move(ids, to_folder)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return message::handlers::move_(&mut printer, &backend, &folder, to_folder, ids).await; - } - Some(message::args::Cmd::Read(ids, text_mime, raw, headers)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return message::handlers::read( - &account_config, - &mut printer, - &backend, - &folder, - ids, - text_mime, - raw, - headers, - ) - .await; - } - Some(message::args::Cmd::Reply(id, all, headers, body)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - return message::handlers::reply( - &account_config, - &mut printer, - &backend, - &folder, - id, - all, - headers, - body, - ) - .await; - } - Some(message::args::Cmd::Save(raw_email)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return message::handlers::save(&mut printer, &backend, &folder, raw_email).await; - } - Some(message::args::Cmd::Send(raw_email)) => { - let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - return message::handlers::send(&account_config, &mut printer, &backend, raw_email) - .await; - } - Some(message::args::Cmd::Write(headers, body)) => { - let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - return message::handlers::write( - &account_config, - &mut printer, - &backend, - headers, - body, - ) - .await; - } - _ => (), - } - - match template::args::matches(&m)? { - Some(template::args::Cmd::Forward(id, headers, body)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return template::handlers::forward( - &account_config, - &mut printer, - &backend, - &folder, - id, - headers, - body, - ) - .await; - } - Some(template::args::Cmd::Write(headers, body)) => { - return template::handlers::write(&account_config, &mut printer, headers, body).await; - } - Some(template::args::Cmd::Reply(id, all, headers, body)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return template::handlers::reply( - &account_config, - &mut printer, - &backend, - &folder, - id, - all, - headers, - body, - ) - .await; - } - Some(template::args::Cmd::Save(template)) => { - let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); - let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - return template::handlers::save( - &account_config, - &mut printer, - &backend, - &folder, - template, - ) - .await; - } - Some(template::args::Cmd::Send(template)) => { - let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - return template::handlers::send(&account_config, &mut printer, &backend, template) - .await; - } - _ => (), - } - - Ok(()) -} - -#[derive(Parser, Debug)] -#[command(name= "himalaya", author, version, about, long_about = None, propagate_version = true)] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand, Debug)] -enum Commands { - Man(man::command::Generate), - #[command(aliases = ["completions", "compl", "comp"])] - Completion(completion::command::Generate), -} +use himalaya::{cli::Cli, config::TomlConfig, printer::StdoutPrinter}; #[tokio::main] async fn main() -> Result<()> { @@ -364,12 +10,305 @@ async fn main() -> Result<()> { .format_timestamp(None) .init(); - let mut printer = StdoutPrinter::default(); + let cli = Cli::parse(); - match Cli::parse().command { - Commands::Man(cmd) => man::handler::generate(&mut printer, Cli::command(), cmd.dir), - Commands::Completion(cmd) => { - completion::handler::generate(&mut printer, Cli::command(), cmd.shell) - } - } + let mut printer = StdoutPrinter::new(cli.output, cli.color); + let config = TomlConfig::from_some_path_or_default(cli.config.as_ref()).await?; + + cli.command.execute(&mut printer, &config).await } + +// fn create_app() -> clap::Command { +// clap::Command::new(env!("CARGO_PKG_NAME")) +// .version(env!("CARGO_PKG_VERSION")) +// .about(env!("CARGO_PKG_DESCRIPTION")) +// .author(env!("CARGO_PKG_AUTHORS")) +// .propagate_version(true) +// .infer_subcommands(true) +// .args(folder::args::global_args()) +// .args(cache::args::global_args()) +// .args(output::args::global_args()) +// .subcommand(folder::args::subcmd()) +// .subcommand(envelope::args::subcmd()) +// .subcommand(flag::args::subcmd()) +// .subcommand(message::args::subcmd()) +// .subcommand(template::args::subcmd()) +// } + +// #[tokio::main] +// async fn main() -> Result<()> { +// #[cfg(not(target_os = "windows"))] +// if let Err((_, err)) = coredump::register_panic_handler() { +// warn!("cannot register custom panic handler: {err}"); +// debug!("cannot register custom panic handler: {err:?}"); +// } + +// let default_env_filter = env_logger::DEFAULT_FILTER_ENV; +// env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off")); + +// // check mailto command before app initialization +// let raw_args: Vec = env::args().collect(); +// if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { +// let url = Url::parse(&raw_args[1])?; +// let (toml_account_config, account_config) = TomlConfig::from_default_paths() +// .await? +// .into_account_configs(None, false)?; +// let backend_builder = +// BackendBuilder::new(toml_account_config, account_config.clone(), true).await?; +// let backend = backend_builder.build().await?; +// let mut printer = StdoutPrinter::default(); + +// return message::handlers::mailto(&account_config, &backend, &mut printer, &url).await; +// } + +// let app = _create_app(); +// let m = app.get_matches(); + +// let some_config_path = config::args::parse_global_arg(&m); +// let some_account_name = account::command::parse_global_arg(&m); +// let disable_cache = cache::args::parse_disable_cache_arg(&m); +// let folder = folder::args::parse_global_source_arg(&m); + +// let toml_config = TomlConfig::from_some_path_or_default(some_config_path).await?; + +// let mut printer = StdoutPrinter::try_from(&m)?; + +// #[cfg(feature = "imap")] +// if let BackendConfig::Imap(imap_config) = &account_config.backend { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// match imap::args::matches(&m)? { +// Some(imap::args::Cmd::Notify(keepalive)) => { +// let backend = +// ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?; +// imap::handlers::notify(&mut backend, &folder, keepalive).await?; +// return Ok(()); +// } +// Some(imap::args::Cmd::Watch(keepalive)) => { +// let backend = +// ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?; +// imap::handlers::watch(&mut backend, &folder, keepalive).await?; +// return Ok(()); +// } +// _ => (), +// } +// } + +// let (toml_account_config, account_config) = toml_config +// .clone() +// .into_account_configs(some_account_name, disable_cache)?; + +// // checks folder commands +// match folder::args::matches(&m)? { +// Some(folder::args::Cmd::Create) => { +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// let folder = folder +// .ok_or_else(|| anyhow!("the folder argument is missing")) +// .context("cannot create folder")?; +// return folder::handlers::create(&mut printer, &backend, &folder).await; +// } +// Some(folder::args::Cmd::List(max_width)) => { +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return folder::handlers::list(&account_config, &mut printer, &backend, max_width) +// .await; +// } +// Some(folder::args::Cmd::Expunge) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return folder::handlers::expunge(&mut printer, &backend, &folder).await; +// } +// Some(folder::args::Cmd::Delete) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return folder::handlers::delete(&mut printer, &backend, &folder).await; +// } +// _ => (), +// } + +// match envelope::args::matches(&m)? { +// Some(envelope::args::Cmd::List(max_width, page_size, page)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return envelope::handlers::list( +// &account_config, +// &mut printer, +// &backend, +// &folder, +// max_width, +// page_size, +// page, +// ) +// .await; +// } +// _ => (), +// } + +// match flag::args::matches(&m)? { +// Some(flag::args::Cmd::Set(ids, ref flags)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await; +// } +// Some(flag::args::Cmd::Add(ids, ref flags)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await; +// } +// Some(flag::args::Cmd::Remove(ids, ref flags)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await; +// } +// _ => (), +// } + +// match message::args::matches(&m)? { +// Some(message::args::Cmd::Attachments(ids)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return message::handlers::attachments( +// &account_config, +// &mut printer, +// &backend, +// &folder, +// ids, +// ) +// .await; +// } +// Some(message::args::Cmd::Copy(ids, to_folder)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return message::handlers::copy(&mut printer, &backend, &folder, to_folder, ids).await; +// } +// Some(message::args::Cmd::Delete(ids)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return message::handlers::delete(&mut printer, &backend, &folder, ids).await; +// } +// Some(message::args::Cmd::Forward(id, headers, body)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; +// return message::handlers::forward( +// &account_config, +// &mut printer, +// &backend, +// &folder, +// id, +// headers, +// body, +// ) +// .await; +// } +// Some(message::args::Cmd::Move(ids, to_folder)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return message::handlers::move_(&mut printer, &backend, &folder, to_folder, ids).await; +// } +// Some(message::args::Cmd::Read(ids, text_mime, raw, headers)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return message::handlers::read( +// &account_config, +// &mut printer, +// &backend, +// &folder, +// ids, +// text_mime, +// raw, +// headers, +// ) +// .await; +// } +// Some(message::args::Cmd::Reply(id, all, headers, body)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; +// return message::handlers::reply( +// &account_config, +// &mut printer, +// &backend, +// &folder, +// id, +// all, +// headers, +// body, +// ) +// .await; +// } +// Some(message::args::Cmd::Save(raw_email)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return message::handlers::save(&mut printer, &backend, &folder, raw_email).await; +// } +// Some(message::args::Cmd::Send(raw_email)) => { +// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; +// return message::handlers::send(&account_config, &mut printer, &backend, raw_email) +// .await; +// } +// Some(message::args::Cmd::Write(headers, body)) => { +// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; +// return message::handlers::write( +// &account_config, +// &mut printer, +// &backend, +// headers, +// body, +// ) +// .await; +// } +// _ => (), +// } + +// match template::args::matches(&m)? { +// Some(template::args::Cmd::Forward(id, headers, body)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return template::handlers::forward( +// &account_config, +// &mut printer, +// &backend, +// &folder, +// id, +// headers, +// body, +// ) +// .await; +// } +// Some(template::args::Cmd::Write(headers, body)) => { +// return template::handlers::write(&account_config, &mut printer, headers, body).await; +// } +// Some(template::args::Cmd::Reply(id, all, headers, body)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return template::handlers::reply( +// &account_config, +// &mut printer, +// &backend, +// &folder, +// id, +// all, +// headers, +// body, +// ) +// .await; +// } +// Some(template::args::Cmd::Save(template)) => { +// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); +// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; +// return template::handlers::save( +// &account_config, +// &mut printer, +// &backend, +// &folder, +// template, +// ) +// .await; +// } +// Some(template::args::Cmd::Send(template)) => { +// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; +// return template::handlers::send(&account_config, &mut printer, &backend, template) +// .await; +// } +// _ => (), +// } + +// Ok(()) +// } diff --git a/src/man/command.rs b/src/man/command.rs index 7316b8e..85d17a3 100644 --- a/src/man/command.rs +++ b/src/man/command.rs @@ -1,16 +1,58 @@ use anyhow::Result; -use clap::Parser; +use clap::{CommandFactory, Parser}; +use clap_mangen::Man; +use log::info; use shellexpand_utils::{canonicalize, expand}; -use std::path::PathBuf; +use std::{fs, path::PathBuf}; + +use crate::{cli::Cli, printer::Printer}; /// Generate all man pages to the given directory #[derive(Debug, Parser)] -pub struct Generate { +pub struct Command { /// Directory where man files should be generated in #[arg(value_parser = dir_parser)] pub dir: PathBuf, } +impl Command { + pub async fn execute(self, printer: &mut impl Printer) -> Result<()> { + info!("executing man generate command"); + + let cmd = Cli::command(); + let cmd_name = cmd.get_name().to_string(); + let subcmds = cmd.get_subcommands().cloned().collect::>(); + let subcmds_len = subcmds.len() + 1; + + let mut buffer = Vec::new(); + Man::new(cmd).render(&mut buffer)?; + + fs::create_dir_all(&self.dir)?; + printer.print_log(format!("Generating man page for command {cmd_name}…"))?; + fs::write(self.dir.join(format!("{}.1", cmd_name)), buffer)?; + + for subcmd in subcmds { + let subcmd_name = subcmd.get_name().to_string(); + + let mut buffer = Vec::new(); + Man::new(subcmd).render(&mut buffer)?; + + printer.print_log(format!("Generating man page for subcommand {subcmd_name}…"))?; + fs::write( + self.dir.join(format!("{}-{}.1", cmd_name, subcmd_name)), + buffer, + )?; + } + + printer.print(format!( + "{subcmds_len} man page(s) successfully generated in {:?}!", + self.dir + ))?; + + Ok(()) + } +} + /// Parse the given [`str`] as [`PathBuf`]. /// /// The path is first shell expanded, then canonicalized (if diff --git a/src/man/handler.rs b/src/man/handler.rs deleted file mode 100644 index 069e45e..0000000 --- a/src/man/handler.rs +++ /dev/null @@ -1,35 +0,0 @@ -use anyhow::Result; -use clap::Command; -use clap_mangen::Man; -use std::{fs, path::PathBuf}; - -use crate::printer::Printer; - -pub fn generate(printer: &mut impl Printer, cmd: Command, dir: PathBuf) -> Result<()> { - let cmd_name = cmd.get_name().to_string(); - let subcmds = cmd.get_subcommands().cloned().collect::>(); - let subcmds_len = subcmds.len() + 1; - - let mut buffer = Vec::new(); - Man::new(cmd).render(&mut buffer)?; - - fs::create_dir_all(&dir)?; - printer.print_log(format!("Generating man page for command {cmd_name}…"))?; - fs::write(dir.join(format!("{}.1", cmd_name)), buffer)?; - - for subcmd in subcmds { - let subcmd_name = subcmd.get_name().to_string(); - - let mut buffer = Vec::new(); - Man::new(subcmd).render(&mut buffer)?; - - printer.print_log(format!("Generating man page for subcommand {subcmd_name}…"))?; - fs::write(dir.join(format!("{}-{}.1", cmd_name, subcmd_name)), buffer)?; - } - - printer.print(format!( - "Successfully generated {subcmds_len} man page(s) in {dir:?}!" - ))?; - - Ok(()) -} diff --git a/src/man/mod.rs b/src/man/mod.rs index a2f4939..9fe7961 100644 --- a/src/man/mod.rs +++ b/src/man/mod.rs @@ -1,2 +1 @@ pub mod command; -pub mod handler; diff --git a/src/output/output.rs b/src/output/output.rs index 9b1d19b..278c59d 100644 --- a/src/output/output.rs +++ b/src/output/output.rs @@ -1,22 +1,18 @@ use anyhow::{anyhow, Error, Result}; use atty::Stream; +use clap::ValueEnum; use serde::Serialize; use std::{fmt, str::FromStr}; use termcolor::ColorChoice; /// Represents the available output formats. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, ValueEnum)] pub enum OutputFmt { + #[default] Plain, Json, } -impl Default for OutputFmt { - fn default() -> Self { - Self::Plain - } -} - impl FromStr for OutputFmt { type Err = Error; @@ -52,20 +48,15 @@ impl OutputJson { } /// Represent the available color configs. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, ValueEnum)] pub enum ColorFmt { Never, Always, Ansi, + #[default] Auto, } -impl Default for ColorFmt { - fn default() -> Self { - Self::Auto - } -} - impl FromStr for ColorFmt { type Err = Error; From 4a77253c1de2d076388d1116554e762b8f43f648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 6 Dec 2023 21:46:31 +0100 Subject: [PATCH 17/29] refactor folder with clap derive api --- src/account/arg/mod.rs | 1 + src/account/arg/name.rs | 26 +++++++++++++++ src/account/command/configure.rs | 19 +++++------ src/account/command/list.rs | 12 +++---- src/account/command/mod.rs | 20 +++++++----- src/account/command/sync.rs | 32 +++++++++++-------- src/account/mod.rs | 1 + src/cache/arg/disable.rs | 19 +++++++++++ src/cache/arg/mod.rs | 1 + src/cache/mod.rs | 1 + src/cli.rs | 38 ++++++++++++---------- src/completion/command.rs | 8 ++--- src/folder/arg/mod.rs | 1 + src/folder/arg/name.rs | 9 ++++++ src/folder/command/create.rs | 40 +++++++++++++++++++++++ src/folder/command/delete.rs | 55 ++++++++++++++++++++++++++++++++ src/folder/command/expunge.rs | 44 +++++++++++++++++++++++++ src/folder/command/list.rs | 50 +++++++++++++++++++++++++++++ src/folder/command/mod.rs | 51 +++++++++++++++++++++++++++++ src/folder/command/purge.rs | 55 ++++++++++++++++++++++++++++++++ src/folder/mod.rs | 2 ++ src/lib.rs | 2 +- src/main.rs | 50 ----------------------------- src/{man => manual}/command.rs | 8 ++--- src/{man => manual}/mod.rs | 0 src/printer/printer.rs | 4 +++ src/ui/table/arg/max_width.rs | 9 ++++++ src/ui/table/arg/mod.rs | 1 + src/ui/table/mod.rs | 1 + 29 files changed, 446 insertions(+), 114 deletions(-) create mode 100644 src/account/arg/mod.rs create mode 100644 src/account/arg/name.rs create mode 100644 src/cache/arg/disable.rs create mode 100644 src/cache/arg/mod.rs create mode 100644 src/folder/arg/mod.rs create mode 100644 src/folder/arg/name.rs create mode 100644 src/folder/command/create.rs create mode 100644 src/folder/command/delete.rs create mode 100644 src/folder/command/expunge.rs create mode 100644 src/folder/command/list.rs create mode 100644 src/folder/command/mod.rs create mode 100644 src/folder/command/purge.rs rename src/{man => manual}/command.rs (91%) rename src/{man => manual}/mod.rs (100%) create mode 100644 src/ui/table/arg/max_width.rs create mode 100644 src/ui/table/arg/mod.rs diff --git a/src/account/arg/mod.rs b/src/account/arg/mod.rs new file mode 100644 index 0000000..c427f91 --- /dev/null +++ b/src/account/arg/mod.rs @@ -0,0 +1 @@ +pub mod name; diff --git a/src/account/arg/name.rs b/src/account/arg/name.rs new file mode 100644 index 0000000..4456e14 --- /dev/null +++ b/src/account/arg/name.rs @@ -0,0 +1,26 @@ +use clap::Parser; + +/// The account name argument parser +#[derive(Debug, Parser)] +pub struct AccountNameArg { + /// The name of the account + /// + /// The account names are taken from the table at the root level + /// of your TOML configuration file. + #[arg(value_name = "ACCOUNT")] + pub name: String, +} + +/// The account name flag parser +#[derive(Debug, Parser)] +pub struct AccountNameFlag { + /// Override the default account + #[arg( + long = "account", + short = 'a', + name = "account-name", + value_name = "NAME", + global = true + )] + pub name: Option, +} diff --git a/src/account/command/configure.rs b/src/account/command/configure.rs index 1ce3fb4..bcfbda1 100644 --- a/src/account/command/configure.rs +++ b/src/account/command/configure.rs @@ -7,6 +7,7 @@ use email::smtp::config::SmtpAuthConfig; use log::{debug, info, warn}; use crate::{ + account::arg::name::AccountNameArg, config::{ wizard::{prompt_passwd, prompt_secret}, TomlConfig, @@ -16,26 +17,22 @@ use crate::{ /// Configure the given account #[derive(Debug, Parser)] -pub struct Command { - /// The name of the account that needs to be configured - /// - /// The account names are taken from the table at the root level - /// of your TOML configuration file. - #[arg(value_name = "NAME")] - pub account_name: String, +pub struct AccountConfigureCommand { + #[command(flatten)] + pub account: AccountNameArg, - /// Force the account to reconfigure, even if it is already + /// Force the account to reconfigure, even if it has already been /// configured #[arg(long, short)] pub force: bool, } -impl Command { +impl AccountConfigureCommand { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { info!("executing account configure command"); let (_, account_config) = - config.into_toml_account_config(Some(self.account_name.as_str()))?; + config.into_toml_account_config(Some(self.account.name.as_str()))?; if self.force { #[cfg(feature = "imap")] @@ -106,7 +103,7 @@ impl Command { printer.print(format!( "Account {} successfully {}configured!", - self.account_name, + self.account.name, if self.force { "re" } else { "" } ))?; diff --git a/src/account/command/list.rs b/src/account/command/list.rs index b0a848e..ee70b04 100644 --- a/src/account/command/list.rs +++ b/src/account/command/list.rs @@ -6,17 +6,17 @@ use crate::{ account::Accounts, config::TomlConfig, printer::{PrintTableOpts, Printer}, + ui::arg::max_width::MaxTableWidthFlag, }; /// List all accounts #[derive(Debug, Parser)] -pub struct Command { - /// Define a maximum width for the table - #[arg(long, short = 'w', name = "PIXELS")] - pub max_width: Option, +pub struct AccountListCommand { + #[command(flatten)] + pub table: MaxTableWidthFlag, } -impl Command { +impl AccountListCommand { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { info!("executing account list command"); @@ -29,7 +29,7 @@ impl Command { .email_reading_format .as_ref() .unwrap_or(&Default::default()), - max_width: self.max_width, + max_width: self.table.max_width, }, )?; diff --git a/src/account/command/mod.rs b/src/account/command/mod.rs index ce0d776..2677935 100644 --- a/src/account/command/mod.rs +++ b/src/account/command/mod.rs @@ -7,23 +7,27 @@ use clap::Subcommand; use crate::{config::TomlConfig, printer::Printer}; +use self::{ + configure::AccountConfigureCommand, list::AccountListCommand, sync::AccountSyncCommand, +}; + /// Subcommand to manage accounts #[derive(Debug, Subcommand)] -pub enum Command { - /// Configure the given account +pub enum AccountSubcommand { + /// Configure an account #[command(alias = "cfg")] - Configure(configure::Command), + Configure(AccountConfigureCommand), - /// List all exsting accounts + /// List all accounts #[command(alias = "lst")] - List(list::Command), + List(AccountListCommand), - /// Synchronize the given account locally + /// Synchronize an account locally #[command()] - Sync(sync::Command), + Sync(AccountSyncCommand), } -impl Command { +impl AccountSubcommand { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { match self { Self::Configure(cmd) => cmd.execute(printer, config).await, diff --git a/src/account/command/sync.rs b/src/account/command/sync.rs index 7096924..286d05f 100644 --- a/src/account/command/sync.rs +++ b/src/account/command/sync.rs @@ -12,7 +12,10 @@ use std::{ sync::Mutex, }; -use crate::{backend::BackendBuilder, config::TomlConfig, printer::Printer}; +use crate::{ + account::arg::name::AccountNameArg, backend::BackendBuilder, config::TomlConfig, + printer::Printer, +}; const MAIN_PROGRESS_STYLE: Lazy = Lazy::new(|| { ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap() @@ -30,13 +33,9 @@ const SUB_PROGRESS_DONE_STYLE: Lazy = Lazy::new(|| { }); #[derive(Debug, Parser)] -pub struct Command { - /// The name of the account that needs to be synchronized - /// - /// The account names are taken from the table at the root level - /// of your TOML configuration file. - #[arg(value_name = "ACCOUNT")] - pub account_name: String, +pub struct AccountSyncCommand { + #[command(flatten)] + pub account: AccountNameArg, /// Run the synchronization without applying changes /// @@ -60,7 +59,7 @@ pub struct Command { pub all_folders: bool, } -impl Command { +impl AccountSyncCommand { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { info!("executing account sync command"); @@ -79,7 +78,7 @@ impl Command { let (toml_account_config, account_config) = config .clone() - .into_account_configs(Some(self.account_name.as_str()), true)?; + .into_account_configs(Some(self.account.name.as_str()), true)?; let backend_builder = BackendBuilder::new(toml_account_config, account_config.clone(), false).await?; @@ -110,11 +109,15 @@ impl Command { } printer.print(format!( - "Estimated patch length for account to be synchronized: {hunks_count}", + "Estimated patch length for account {} to be synchronized: {hunks_count}", + self.account.name ))?; } else if printer.is_json() { sync_builder.sync().await?; - printer.print("Account successfully synchronized!")?; + printer.print(format!( + "Account {} successfully synchronized!", + self.account.name + ))?; } else { let multi = MultiProgress::new(); let sub_progresses = Mutex::new(HashMap::new()); @@ -225,7 +228,10 @@ impl Command { ))?; } - printer.print("Account successfully synchronized!")?; + printer.print(format!( + "Account {} successfully synchronized!", + self.account.name + ))?; } Ok(()) diff --git a/src/account/mod.rs b/src/account/mod.rs index b463195..1910174 100644 --- a/src/account/mod.rs +++ b/src/account/mod.rs @@ -1,3 +1,4 @@ +pub mod arg; pub mod command; pub mod config; pub(crate) mod wizard; diff --git a/src/cache/arg/disable.rs b/src/cache/arg/disable.rs new file mode 100644 index 0000000..54d5b8e --- /dev/null +++ b/src/cache/arg/disable.rs @@ -0,0 +1,19 @@ +use clap::Parser; + +/// The disable cache flag parser +#[derive(Debug, Parser)] +pub struct DisableCacheFlag { + /// Disable any sort of cache + /// + /// The action depends on commands it apply on. For example, when + /// listing envelopes using the IMAP backend, this flag will + /// ensure that envelopes are fetched from the IMAP server and not + /// from the synchronized local Maildir. + #[arg( + long = "disable-cache", + alias = "no-cache", + name = "disable-cache", + global = true + )] + pub disable: bool, +} diff --git a/src/cache/arg/mod.rs b/src/cache/arg/mod.rs new file mode 100644 index 0000000..a13d9f3 --- /dev/null +++ b/src/cache/arg/mod.rs @@ -0,0 +1 @@ +pub mod disable; diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 04ac835..4b51a20 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1,3 +1,4 @@ +pub mod arg; pub mod args; use anyhow::{anyhow, Context, Result}; diff --git a/src/cli.rs b/src/cli.rs index 481320e..2b86351 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,13 +1,13 @@ -use std::path::PathBuf; - use anyhow::Result; use clap::{Parser, Subcommand}; +use std::path::PathBuf; use crate::{ - account::command as account, - completion::command as completion, + account::command::AccountSubcommand, + completion::command::CompletionGenerateCommand, config::{self, TomlConfig}, - man::command as man, + folder::command::FolderSubcommand, + manual::command::ManualGenerateCommand, output::{ColorFmt, OutputFmt}, printer::Printer, }; @@ -23,7 +23,7 @@ use crate::{ )] pub struct Cli { #[command(subcommand)] - pub command: Command, + pub command: HimalayaCommand, /// Override the default configuration file path /// @@ -86,27 +86,31 @@ pub struct Cli { pub color: ColorFmt, } -/// Top-level CLI commands. #[derive(Subcommand, Debug)] -pub enum Command { +pub enum HimalayaCommand { /// Subcommand to manage accounts #[command(subcommand)] - Account(account::Command), + Account(AccountSubcommand), - /// Generate all man pages to the given directory - #[command(arg_required_else_help = true, alias = "mans")] - Man(man::Command), + /// Subcommand to manage folders + #[command(subcommand)] + Folder(FolderSubcommand), - /// Print completion script for the given shell to stdout - #[command(arg_required_else_help = true, aliases = ["completions", "compl", "comp"])] - Completion(completion::Command), + /// Generate manual pages to a directory + #[command(arg_required_else_help = true)] + Manual(ManualGenerateCommand), + + /// Print completion script for a shell to stdout + #[command(arg_required_else_help = true)] + Completion(CompletionGenerateCommand), } -impl Command { +impl HimalayaCommand { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { match self { Self::Account(cmd) => cmd.execute(printer, config).await, - Self::Man(cmd) => cmd.execute(printer).await, + Self::Folder(cmd) => cmd.execute(printer, config).await, + Self::Manual(cmd) => cmd.execute(printer).await, Self::Completion(cmd) => cmd.execute(printer).await, } } diff --git a/src/completion/command.rs b/src/completion/command.rs index aecefcc..67a3a7f 100644 --- a/src/completion/command.rs +++ b/src/completion/command.rs @@ -6,15 +6,15 @@ use std::io; use crate::{cli::Cli, printer::Printer}; -/// Print completion script for the given shell to stdout +/// Print completion script for a shell to stdout #[derive(Debug, Parser)] -pub struct Command { - /// Shell that completion script should be generated for +pub struct CompletionGenerateCommand { + /// Shell for which completion script should be generated for #[arg(value_parser = value_parser!(Shell))] pub shell: Shell, } -impl Command { +impl CompletionGenerateCommand { pub async fn execute(self, printer: &mut impl Printer) -> Result<()> { info!("executing completion generate command"); diff --git a/src/folder/arg/mod.rs b/src/folder/arg/mod.rs new file mode 100644 index 0000000..c427f91 --- /dev/null +++ b/src/folder/arg/mod.rs @@ -0,0 +1 @@ +pub mod name; diff --git a/src/folder/arg/name.rs b/src/folder/arg/name.rs new file mode 100644 index 0000000..fe80733 --- /dev/null +++ b/src/folder/arg/name.rs @@ -0,0 +1,9 @@ +use clap::Parser; + +/// The folder name argument parser +#[derive(Debug, Parser)] +pub struct FolderNameArg { + /// The name of the folder + #[arg(name = "folder-name", value_name = "FOLDER")] + pub name: String, +} diff --git a/src/folder/command/create.rs b/src/folder/command/create.rs new file mode 100644 index 0000000..02c409e --- /dev/null +++ b/src/folder/command/create.rs @@ -0,0 +1,40 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer, +}; + +/// Create a new folder +#[derive(Debug, Parser)] +pub struct FolderCreateCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub account: AccountNameFlag, + + #[command(flatten)] + pub cache: DisableCacheFlag, +} + +impl FolderCreateCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing folder create command"); + + let folder = &self.folder.name; + + let some_account_name = self.account.name.as_ref().map(String::as_str); + let (toml_account_config, account_config) = config + .clone() + .into_account_configs(some_account_name, self.cache.disable)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + backend.add_folder(&folder).await?; + printer.print(format!("Folder {folder} successfully created!"))?; + + Ok(()) + } +} diff --git a/src/folder/command/delete.rs b/src/folder/command/delete.rs new file mode 100644 index 0000000..e0786db --- /dev/null +++ b/src/folder/command/delete.rs @@ -0,0 +1,55 @@ +use anyhow::Result; +use clap::Parser; +use dialoguer::Confirm; +use log::info; +use std::process; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer, +}; + +/// Delete a folder +/// +/// All emails from a given folder are definitely deleted. The folder +/// is also deleted after execution of the command. +#[derive(Debug, Parser)] +pub struct FolderDeleteCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub account: AccountNameFlag, + + #[command(flatten)] + pub cache: DisableCacheFlag, +} + +impl FolderDeleteCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing folder delete command"); + + let folder = &self.folder.name; + + let confirm_msg = format!("Do you really want to delete the folder {folder}? All emails will be definitely deleted."); + let confirm = Confirm::new() + .with_prompt(confirm_msg) + .default(false) + .report(false) + .interact_opt()?; + if let Some(false) | None = confirm { + process::exit(0); + }; + + let some_account_name = self.account.name.as_ref().map(String::as_str); + let (toml_account_config, account_config) = config + .clone() + .into_account_configs(some_account_name, self.cache.disable)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + backend.delete_folder(&folder).await?; + printer.print(format!("Folder {folder} successfully deleted!"))?; + + Ok(()) + } +} diff --git a/src/folder/command/expunge.rs b/src/folder/command/expunge.rs new file mode 100644 index 0000000..ac2c0b3 --- /dev/null +++ b/src/folder/command/expunge.rs @@ -0,0 +1,44 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer, +}; + +/// Expunge a folder +/// +/// The concept of expunging is similar to the IMAP one: it definitely +/// deletes emails from a given folder that contain the "deleted" +/// flag. +#[derive(Debug, Parser)] +pub struct FolderExpungeCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub account: AccountNameFlag, + + #[command(flatten)] + pub cache: DisableCacheFlag, +} + +impl FolderExpungeCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing folder expunge command"); + + let folder = &self.folder.name; + + let some_account_name = self.account.name.as_ref().map(String::as_str); + let (toml_account_config, account_config) = config + .clone() + .into_account_configs(some_account_name, self.cache.disable)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + backend.expunge_folder(&folder).await?; + printer.print(format!("Folder {folder} successfully expunged!"))?; + + Ok(()) + } +} diff --git a/src/folder/command/list.rs b/src/folder/command/list.rs new file mode 100644 index 0000000..03e0a65 --- /dev/null +++ b/src/folder/command/list.rs @@ -0,0 +1,50 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + folder::Folders, + printer::{PrintTableOpts, Printer}, + ui::arg::max_width::MaxTableWidthFlag, +}; + +/// List all folders +#[derive(Debug, Parser)] +pub struct FolderListCommand { + #[command(flatten)] + pub table: MaxTableWidthFlag, + + #[command(flatten)] + pub account: AccountNameFlag, + + #[command(flatten)] + pub cache: DisableCacheFlag, +} + +impl FolderListCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing folder list command"); + + let some_account_name = self.account.name.as_ref().map(String::as_str); + let (toml_account_config, account_config) = config + .clone() + .into_account_configs(some_account_name, self.cache.disable)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + let folders: Folders = backend.list_folders().await?.into(); + + printer.print_table( + Box::new(folders), + PrintTableOpts { + format: &account_config.email_reading_format, + max_width: self.table.max_width, + }, + )?; + + Ok(()) + } +} diff --git a/src/folder/command/mod.rs b/src/folder/command/mod.rs new file mode 100644 index 0000000..e00b0f9 --- /dev/null +++ b/src/folder/command/mod.rs @@ -0,0 +1,51 @@ +mod create; +mod delete; +mod expunge; +mod list; +mod purge; + +use anyhow::Result; +use clap::Subcommand; + +use crate::{config::TomlConfig, printer::Printer}; + +use self::{ + create::FolderCreateCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand, + list::FolderListCommand, purge::FolderPurgeCommand, +}; + +/// Subcommand to manage accounts +#[derive(Debug, Subcommand)] +pub enum FolderSubcommand { + /// Create a new folder + #[command(alias = "add")] + Create(FolderCreateCommand), + + /// List all folders + #[command(alias = "lst")] + List(FolderListCommand), + + /// Expunge a folder + #[command()] + Expunge(FolderExpungeCommand), + + /// Purge a folder + #[command()] + Purge(FolderPurgeCommand), + + /// Delete a folder + #[command(alias = "remove", alias = "rm")] + Delete(FolderDeleteCommand), +} + +impl FolderSubcommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + match self { + Self::Create(cmd) => cmd.execute(printer, config).await, + Self::List(cmd) => cmd.execute(printer, config).await, + Self::Expunge(cmd) => cmd.execute(printer, config).await, + Self::Purge(cmd) => cmd.execute(printer, config).await, + Self::Delete(cmd) => cmd.execute(printer, config).await, + } + } +} diff --git a/src/folder/command/purge.rs b/src/folder/command/purge.rs new file mode 100644 index 0000000..3d870f7 --- /dev/null +++ b/src/folder/command/purge.rs @@ -0,0 +1,55 @@ +use anyhow::Result; +use clap::Parser; +use dialoguer::Confirm; +use log::info; +use std::process; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer, +}; + +/// Purge a folder +/// +/// All emails from a given folder are definitely deleted. The purged +/// folder will remain empty after executing of the command. +#[derive(Debug, Parser)] +pub struct FolderPurgeCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub account: AccountNameFlag, + + #[command(flatten)] + pub cache: DisableCacheFlag, +} + +impl FolderPurgeCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing folder purge command"); + + let folder = &self.folder.name; + + let confirm_msg = format!("Do you really want to purge the folder {folder}? All emails will be definitely deleted."); + let confirm = Confirm::new() + .with_prompt(confirm_msg) + .default(false) + .report(false) + .interact_opt()?; + if let Some(false) | None = confirm { + process::exit(0); + }; + + let some_account_name = self.account.name.as_ref().map(String::as_str); + let (toml_account_config, account_config) = config + .clone() + .into_account_configs(some_account_name, self.cache.disable)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + backend.purge_folder(&folder).await?; + printer.print(format!("Folder {folder} successfully purged!"))?; + + Ok(()) + } +} diff --git a/src/folder/mod.rs b/src/folder/mod.rs index e448920..67fd4c0 100644 --- a/src/folder/mod.rs +++ b/src/folder/mod.rs @@ -1,4 +1,6 @@ +pub mod arg; pub mod args; +pub mod command; pub mod config; pub mod handlers; diff --git a/src/lib.rs b/src/lib.rs index 8cb19cd..0cb7c67 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ pub mod folder; pub mod imap; #[cfg(feature = "maildir")] pub mod maildir; -pub mod man; +pub mod manual; #[cfg(feature = "notmuch")] pub mod notmuch; pub mod output; diff --git a/src/main.rs b/src/main.rs index d80c198..8a2a343 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,10 +25,8 @@ async fn main() -> Result<()> { // .author(env!("CARGO_PKG_AUTHORS")) // .propagate_version(true) // .infer_subcommands(true) -// .args(folder::args::global_args()) // .args(cache::args::global_args()) // .args(output::args::global_args()) -// .subcommand(folder::args::subcmd()) // .subcommand(envelope::args::subcmd()) // .subcommand(flag::args::subcmd()) // .subcommand(message::args::subcmd()) @@ -67,63 +65,15 @@ async fn main() -> Result<()> { // let some_config_path = config::args::parse_global_arg(&m); // let some_account_name = account::command::parse_global_arg(&m); // let disable_cache = cache::args::parse_disable_cache_arg(&m); -// let folder = folder::args::parse_global_source_arg(&m); // let toml_config = TomlConfig::from_some_path_or_default(some_config_path).await?; // let mut printer = StdoutPrinter::try_from(&m)?; -// #[cfg(feature = "imap")] -// if let BackendConfig::Imap(imap_config) = &account_config.backend { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// match imap::args::matches(&m)? { -// Some(imap::args::Cmd::Notify(keepalive)) => { -// let backend = -// ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?; -// imap::handlers::notify(&mut backend, &folder, keepalive).await?; -// return Ok(()); -// } -// Some(imap::args::Cmd::Watch(keepalive)) => { -// let backend = -// ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?; -// imap::handlers::watch(&mut backend, &folder, keepalive).await?; -// return Ok(()); -// } -// _ => (), -// } -// } - // let (toml_account_config, account_config) = toml_config // .clone() // .into_account_configs(some_account_name, disable_cache)?; -// // checks folder commands -// match folder::args::matches(&m)? { -// Some(folder::args::Cmd::Create) => { -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// let folder = folder -// .ok_or_else(|| anyhow!("the folder argument is missing")) -// .context("cannot create folder")?; -// return folder::handlers::create(&mut printer, &backend, &folder).await; -// } -// Some(folder::args::Cmd::List(max_width)) => { -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return folder::handlers::list(&account_config, &mut printer, &backend, max_width) -// .await; -// } -// Some(folder::args::Cmd::Expunge) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return folder::handlers::expunge(&mut printer, &backend, &folder).await; -// } -// Some(folder::args::Cmd::Delete) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return folder::handlers::delete(&mut printer, &backend, &folder).await; -// } -// _ => (), -// } - // match envelope::args::matches(&m)? { // Some(envelope::args::Cmd::List(max_width, page_size, page)) => { // let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); diff --git a/src/man/command.rs b/src/manual/command.rs similarity index 91% rename from src/man/command.rs rename to src/manual/command.rs index 85d17a3..f55ca93 100644 --- a/src/man/command.rs +++ b/src/manual/command.rs @@ -7,17 +7,17 @@ use std::{fs, path::PathBuf}; use crate::{cli::Cli, printer::Printer}; -/// Generate all man pages to the given directory +/// Generate manual pages to a directory #[derive(Debug, Parser)] -pub struct Command { +pub struct ManualGenerateCommand { /// Directory where man files should be generated in #[arg(value_parser = dir_parser)] pub dir: PathBuf, } -impl Command { +impl ManualGenerateCommand { pub async fn execute(self, printer: &mut impl Printer) -> Result<()> { - info!("executing man generate command"); + info!("executing manual generate command"); let cmd = Cli::command(); let cmd_name = cmd.get_name().to_string(); diff --git a/src/man/mod.rs b/src/manual/mod.rs similarity index 100% rename from src/man/mod.rs rename to src/manual/mod.rs diff --git a/src/printer/printer.rs b/src/printer/printer.rs index 872176d..ef00dd0 100644 --- a/src/printer/printer.rs +++ b/src/printer/printer.rs @@ -9,10 +9,14 @@ use crate::{ }; pub trait Printer { + // TODO: rename end fn print(&mut self, data: T) -> Result<()>; + // TODO: rename log fn print_log(&mut self, data: T) -> Result<()>; + // TODO: rename table fn print_table( &mut self, + // TODO: remove Box data: Box, opts: PrintTableOpts, ) -> Result<()>; diff --git a/src/ui/table/arg/max_width.rs b/src/ui/table/arg/max_width.rs new file mode 100644 index 0000000..bbcd977 --- /dev/null +++ b/src/ui/table/arg/max_width.rs @@ -0,0 +1,9 @@ +use clap::Parser; + +/// The table max width argument parser +#[derive(Debug, Parser)] +pub struct MaxTableWidthFlag { + /// The maximum width the table should not exceed + #[arg(long, short = 'w', value_name = "PIXELS")] + pub max_width: Option, +} diff --git a/src/ui/table/arg/mod.rs b/src/ui/table/arg/mod.rs new file mode 100644 index 0000000..e5a0593 --- /dev/null +++ b/src/ui/table/arg/mod.rs @@ -0,0 +1 @@ +pub mod max_width; diff --git a/src/ui/table/mod.rs b/src/ui/table/mod.rs index d9e4ee3..b07ca63 100644 --- a/src/ui/table/mod.rs +++ b/src/ui/table/mod.rs @@ -1,3 +1,4 @@ +pub mod arg; pub mod args; pub mod table; From 2c33dd2f9fef8a805824e5259b4f002e2b78fc7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 6 Dec 2023 23:12:06 +0100 Subject: [PATCH 18/29] refactor envelope with clap derive api --- src/cli.rs | 6 + src/email/envelope/args.rs | 91 ----------- src/email/envelope/command/list.rs | 68 ++++++++ src/email/envelope/command/mod.rs | 24 +++ src/email/envelope/handlers.rs | 32 ---- src/email/envelope/mod.rs | 3 +- src/email/message/args.rs | 10 +- src/folder/arg/name.rs | 9 ++ src/folder/args.rs | 241 ---------------------------- src/folder/command/mod.rs | 191 ++++++++++++++++++++++ src/folder/handlers.rs | 247 ----------------------------- src/folder/mod.rs | 2 - src/ui/table/args.rs | 21 --- src/ui/table/mod.rs | 1 - 14 files changed, 304 insertions(+), 642 deletions(-) delete mode 100644 src/email/envelope/args.rs create mode 100644 src/email/envelope/command/list.rs create mode 100644 src/email/envelope/command/mod.rs delete mode 100644 src/email/envelope/handlers.rs delete mode 100644 src/folder/args.rs delete mode 100644 src/folder/handlers.rs delete mode 100644 src/ui/table/args.rs diff --git a/src/cli.rs b/src/cli.rs index 2b86351..0670587 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,6 +6,7 @@ use crate::{ account::command::AccountSubcommand, completion::command::CompletionGenerateCommand, config::{self, TomlConfig}, + envelope::command::EnvelopeSubcommand, folder::command::FolderSubcommand, manual::command::ManualGenerateCommand, output::{ColorFmt, OutputFmt}, @@ -96,6 +97,10 @@ pub enum HimalayaCommand { #[command(subcommand)] Folder(FolderSubcommand), + /// Subcommand to manage envelopes + #[command(subcommand)] + Envelope(EnvelopeSubcommand), + /// Generate manual pages to a directory #[command(arg_required_else_help = true)] Manual(ManualGenerateCommand), @@ -110,6 +115,7 @@ impl HimalayaCommand { match self { Self::Account(cmd) => cmd.execute(printer, config).await, Self::Folder(cmd) => cmd.execute(printer, config).await, + Self::Envelope(cmd) => cmd.execute(printer, config).await, Self::Manual(cmd) => cmd.execute(printer).await, Self::Completion(cmd) => cmd.execute(printer).await, } diff --git a/src/email/envelope/args.rs b/src/email/envelope/args.rs deleted file mode 100644 index 8a7e8ce..0000000 --- a/src/email/envelope/args.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! Email CLI module. -//! -//! This module contains the command matcher, the subcommands and the -//! arguments related to the email domain. - -use anyhow::Result; -use clap::{Arg, ArgMatches, Command}; - -use crate::ui::table; - -const ARG_PAGE: &str = "page"; -const ARG_PAGE_SIZE: &str = "page-size"; -const CMD_LIST: &str = "list"; -const CMD_ENVELOPE: &str = "envelope"; - -pub type Page = usize; -pub type PageSize = usize; - -/// Represents the email commands. -#[derive(Debug, PartialEq, Eq)] -pub enum Cmd { - List(table::args::MaxTableWidth, Option, Page), -} - -/// Email command matcher. -pub fn matches(m: &ArgMatches) -> Result> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_ENVELOPE) { - if let Some(m) = m.subcommand_matches(CMD_LIST) { - let max_table_width = table::args::parse_max_width(m); - let page_size = parse_page_size_arg(m); - let page = parse_page_arg(m); - Some(Cmd::List(max_table_width, page_size, page)) - } else { - Some(Cmd::List(None, None, 0)) - } - } else { - None - }; - - Ok(cmd) -} - -/// Represents the envelope subcommand. -pub fn subcmd() -> Command { - Command::new(CMD_ENVELOPE) - .about("Subcommand to manage envelopes") - .long_about("Subcommand to manage envelopes like list") - .subcommands([Command::new(CMD_LIST) - .alias("lst") - .about("List envelopes") - .arg(page_size_arg()) - .arg(page_arg()) - .arg(table::args::max_width())]) -} - -/// Represents the page size argument. -fn page_size_arg() -> Arg { - Arg::new(ARG_PAGE_SIZE) - .help("Page size") - .long("page-size") - .short('s') - .value_name("INT") -} - -/// Represents the page size argument parser. -fn parse_page_size_arg(matches: &ArgMatches) -> Option { - matches - .get_one::(ARG_PAGE_SIZE) - .and_then(|s| s.parse().ok()) -} - -/// Represents the page argument. -fn page_arg() -> Arg { - Arg::new(ARG_PAGE) - .help("Page number") - .short('p') - .long("page") - .value_name("INT") - .default_value("1") -} - -/// Represents the page argument parser. -fn parse_page_arg(matches: &ArgMatches) -> usize { - matches - .get_one::(ARG_PAGE) - .unwrap() - .parse() - .ok() - .map(|page| 1.max(page) - 1) - .unwrap_or_default() -} diff --git a/src/email/envelope/command/list.rs b/src/email/envelope/command/list.rs new file mode 100644 index 0000000..6e9ba1b --- /dev/null +++ b/src/email/envelope/command/list.rs @@ -0,0 +1,68 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + folder::arg::name::FolderNameOptionalArg, + printer::{PrintTableOpts, Printer}, + ui::arg::max_width::MaxTableWidthFlag, +}; + +/// List all envelopes from a folder +#[derive(Debug, Parser)] +pub struct EnvelopeListCommand { + #[command(flatten)] + pub folder: FolderNameOptionalArg, + + /// The page number + #[arg(long, short, value_name = "NUMBER", default_value = "1")] + pub page: usize, + + /// The page size + #[arg(long, short = 's', value_name = "NUMBER")] + pub page_size: Option, + + #[command(flatten)] + pub table: MaxTableWidthFlag, + + #[command(flatten)] + pub account: AccountNameFlag, + + #[command(flatten)] + pub cache: DisableCacheFlag, +} + +impl EnvelopeListCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing envelope list command"); + + let folder = &self.folder.name; + + let some_account_name = self.account.name.as_ref().map(String::as_str); + let (toml_account_config, account_config) = config + .clone() + .into_account_configs(some_account_name, self.cache.disable)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + let page_size = self + .page_size + .unwrap_or(account_config.email_listing_page_size()); + let page = 1.max(self.page) - 1; + + let envelopes = backend.list_envelopes(folder, page_size, page).await?; + + printer.print_table( + Box::new(envelopes), + PrintTableOpts { + format: &account_config.email_reading_format, + max_width: self.table.max_width, + }, + )?; + + Ok(()) + } +} diff --git a/src/email/envelope/command/mod.rs b/src/email/envelope/command/mod.rs new file mode 100644 index 0000000..8bda915 --- /dev/null +++ b/src/email/envelope/command/mod.rs @@ -0,0 +1,24 @@ +pub mod list; + +use anyhow::Result; +use clap::Subcommand; + +use crate::{config::TomlConfig, printer::Printer}; + +use self::list::EnvelopeListCommand; + +/// Subcommand to manage envelopes +#[derive(Debug, Subcommand)] +pub enum EnvelopeSubcommand { + /// List all envelopes from a folder + #[command(alias = "lst")] + List(EnvelopeListCommand), +} + +impl EnvelopeSubcommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + match self { + Self::List(cmd) => cmd.execute(printer, config).await, + } + } +} diff --git a/src/email/envelope/handlers.rs b/src/email/envelope/handlers.rs deleted file mode 100644 index 0477a66..0000000 --- a/src/email/envelope/handlers.rs +++ /dev/null @@ -1,32 +0,0 @@ -use anyhow::Result; -use email::account::config::AccountConfig; -use log::{debug, trace}; - -use crate::{ - backend::Backend, - printer::{PrintTableOpts, Printer}, -}; - -pub async fn list( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - folder: &str, - max_width: Option, - page_size: Option, - page: usize, -) -> Result<()> { - let page_size = page_size.unwrap_or(config.email_listing_page_size()); - debug!("page size: {}", page_size); - - let envelopes = backend.list_envelopes(&folder, page_size, page).await?; - trace!("envelopes: {:?}", envelopes); - - printer.print_table( - Box::new(envelopes), - PrintTableOpts { - format: &config.email_reading_format, - max_width, - }, - ) -} diff --git a/src/email/envelope/mod.rs b/src/email/envelope/mod.rs index b43c520..9b025e7 100644 --- a/src/email/envelope/mod.rs +++ b/src/email/envelope/mod.rs @@ -1,7 +1,6 @@ -pub mod args; +pub mod command; pub mod config; pub mod flag; -pub mod handlers; use anyhow::Result; use email::account::config::AccountConfig; diff --git a/src/email/message/args.rs b/src/email/message/args.rs index 65448c0..80dbb43 100644 --- a/src/email/message/args.rs +++ b/src/email/message/args.rs @@ -6,7 +6,7 @@ use anyhow::Result; use clap::{Arg, ArgAction, ArgMatches, Command}; -use crate::{folder, template}; +use crate::template; const ARG_CRITERIA: &str = "criterion"; const ARG_HEADERS: &str = "headers"; @@ -71,7 +71,7 @@ pub fn matches(m: &ArgMatches) -> Result> { Some(Cmd::Attachments(ids)) } else if let Some(m) = m.subcommand_matches(CMD_COPY) { let ids = parse_ids_arg(m); - let folder = folder::args::parse_target_arg(m); + let folder = "INBOX"; Some(Cmd::Copy(ids, folder)) } else if let Some(m) = m.subcommand_matches(CMD_DELETE) { let ids = parse_ids_arg(m); @@ -83,7 +83,7 @@ pub fn matches(m: &ArgMatches) -> Result> { Some(Cmd::Forward(id, headers, body)) } else if let Some(m) = m.subcommand_matches(CMD_MOVE) { let ids = parse_ids_arg(m); - let folder = folder::args::parse_target_arg(m); + let folder = "INBOX"; Some(Cmd::Move(ids, folder)) } else if let Some(m) = m.subcommand_matches(CMD_READ) { let ids = parse_ids_arg(m); @@ -158,12 +158,12 @@ pub fn subcmd() -> Command { Command::new(CMD_COPY) .alias("cp") .about("Copy emails to the given folder") - .arg(folder::args::target_arg()) + // .arg(folder::args::target_arg()) .arg(ids_arg()), Command::new(CMD_MOVE) .alias("mv") .about("Move emails to the given folder") - .arg(folder::args::target_arg()) + // .arg(folder::args::target_arg()) .arg(ids_arg()), Command::new(CMD_DELETE) .aliases(["remove", "rm"]) diff --git a/src/folder/arg/name.rs b/src/folder/arg/name.rs index fe80733..9004fec 100644 --- a/src/folder/arg/name.rs +++ b/src/folder/arg/name.rs @@ -1,4 +1,5 @@ use clap::Parser; +use email::account::config::DEFAULT_INBOX_FOLDER; /// The folder name argument parser #[derive(Debug, Parser)] @@ -7,3 +8,11 @@ pub struct FolderNameArg { #[arg(name = "folder-name", value_name = "FOLDER")] pub name: String, } + +/// The optional folder name argument parser +#[derive(Debug, Parser)] +pub struct FolderNameOptionalArg { + /// The name of the folder + #[arg(name = "folder-name", value_name = "FOLDER", default_value = DEFAULT_INBOX_FOLDER)] + pub name: String, +} diff --git a/src/folder/args.rs b/src/folder/args.rs deleted file mode 100644 index f9d0c7e..0000000 --- a/src/folder/args.rs +++ /dev/null @@ -1,241 +0,0 @@ -//! Folder CLI module. -//! -//! This module provides subcommands, arguments and a command matcher -//! related to the folder domain. - -use std::collections::HashSet; - -use anyhow::Result; -use clap::{self, Arg, ArgAction, ArgMatches, Command}; -use log::{debug, info}; - -use crate::ui::table; - -const ARG_ALL: &str = "all"; -const ARG_EXCLUDE: &str = "exclude"; -const ARG_INCLUDE: &str = "include"; -const ARG_GLOBAL_SOURCE: &str = "global-source"; -const ARG_SOURCE: &str = "source"; -const ARG_TARGET: &str = "target"; -const CMD_CREATE: &str = "create"; -const CMD_DELETE: &str = "delete"; -const CMD_EXPUNGE: &str = "expunge"; -const CMD_FOLDER: &str = "folder"; -const CMD_LIST: &str = "list"; - -/// Represents the folder commands. -#[derive(Debug, PartialEq, Eq)] -pub enum Cmd { - Create, - List(table::args::MaxTableWidth), - Expunge, - Delete, -} - -/// Represents the folder command matcher. -pub fn matches(m: &ArgMatches) -> Result> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDER) { - if let Some(_) = m.subcommand_matches(CMD_EXPUNGE) { - info!("expunge folder subcommand matched"); - Some(Cmd::Expunge) - } else if let Some(_) = m.subcommand_matches(CMD_CREATE) { - debug!("create folder command matched"); - Some(Cmd::Create) - } else if let Some(m) = m.subcommand_matches(CMD_LIST) { - debug!("list folders command matched"); - let max_table_width = table::args::parse_max_width(m); - Some(Cmd::List(max_table_width)) - } else if let Some(_) = m.subcommand_matches(CMD_DELETE) { - debug!("delete folder command matched"); - Some(Cmd::Delete) - } else { - info!("no folder subcommand matched, falling back to subcommand list"); - Some(Cmd::List(None)) - } - } else { - None - }; - - Ok(cmd) -} - -/// Represents the folder subcommand. -pub fn subcmd() -> Command { - Command::new(CMD_FOLDER) - .about("Subcommand to manage folders") - .long_about("Subcommand to manage folders like list, expunge or delete") - .subcommands([ - Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"), - Command::new(CMD_CREATE) - .aliases(["add", "new"]) - .about("Create a new folder"), - Command::new(CMD_LIST) - .about("List folders") - .arg(table::args::max_width()), - Command::new(CMD_DELETE) - .aliases(["remove", "rm"]) - .about("Delete a folder with all its emails"), - ]) -} - -/// Represents the source folder argument. -pub fn global_args() -> impl IntoIterator { - [Arg::new(ARG_GLOBAL_SOURCE) - .help("Override the default INBOX folder") - .long_help( - "Override the default INBOX folder. - -The given folder will be used by default for all other commands (when -applicable).", - ) - .long("folder") - .short('f') - .global(true) - .value_name("name")] -} - -pub fn parse_global_source_arg(matches: &ArgMatches) -> Option<&str> { - matches - .get_one::(ARG_GLOBAL_SOURCE) - .map(String::as_str) -} - -pub fn source_arg(help: &'static str) -> Arg { - Arg::new(ARG_SOURCE).help(help).value_name("name") -} - -pub fn parse_source_arg(matches: &ArgMatches) -> Option<&str> { - matches.get_one::(ARG_SOURCE).map(String::as_str) -} - -/// Represents the all folders argument. -pub fn all_arg(help: &'static str) -> Arg { - Arg::new(ARG_ALL) - .help(help) - .long("all-folders") - .alias("all") - .short('A') - .action(ArgAction::SetTrue) - .conflicts_with(ARG_SOURCE) - .conflicts_with(ARG_INCLUDE) - .conflicts_with(ARG_EXCLUDE) -} - -/// Represents the folders to include argument. -pub fn include_arg(help: &'static str) -> Arg { - Arg::new(ARG_INCLUDE) - .help(help) - .long("include-folder") - .alias("only") - .short('F') - .value_name("FOLDER") - .num_args(1..) - .action(ArgAction::Append) - .conflicts_with(ARG_SOURCE) - .conflicts_with(ARG_ALL) - .conflicts_with(ARG_EXCLUDE) -} - -/// Represents the folders to exclude argument. -pub fn exclude_arg(help: &'static str) -> Arg { - Arg::new(ARG_EXCLUDE) - .help(help) - .long("exclude-folder") - .alias("except") - .short('x') - .value_name("FOLDER") - .num_args(1..) - .action(ArgAction::Append) - .conflicts_with(ARG_SOURCE) - .conflicts_with(ARG_ALL) - .conflicts_with(ARG_INCLUDE) -} - -/// Represents the folders to exclude argument parser. -pub fn parse_exclude_arg(m: &ArgMatches) -> HashSet { - m.get_many::(ARG_EXCLUDE) - .unwrap_or_default() - .map(ToOwned::to_owned) - .collect() -} - -/// Represents the target folder argument. -pub fn target_arg() -> Arg { - Arg::new(ARG_TARGET) - .help("Specifies the target folder") - .value_name("TARGET") - .required(true) -} - -/// Represents the target folder argument parser. -pub fn parse_target_arg(matches: &ArgMatches) -> &str { - matches.get_one::(ARG_TARGET).unwrap().as_str() -} - -#[cfg(test)] -mod tests { - use clap::{error::ErrorKind, Command}; - - use super::*; - - #[test] - fn it_should_match_cmds() { - let arg = Command::new("himalaya") - .subcommand(subcmd()) - .get_matches_from(&["himalaya", "folders"]); - assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap()); - - let arg = Command::new("himalaya") - .subcommand(subcmd()) - .get_matches_from(&["himalaya", "folders", "list", "--max-width", "20"]); - assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap()); - } - - #[test] - fn it_should_match_source_arg() { - macro_rules! get_matches_from { - ($($arg:expr),*) => { - Command::new("himalaya") - .arg(source_arg()) - .get_matches_from(&["himalaya", $($arg,)*]) - }; - } - - let app = get_matches_from![]; - assert_eq!(None, app.get_one::(ARG_SOURCE).map(String::as_str)); - - let app = get_matches_from!["-f", "SOURCE"]; - assert_eq!( - Some("SOURCE"), - app.get_one::(ARG_SOURCE).map(String::as_str) - ); - - let app = get_matches_from!["--folder", "SOURCE"]; - assert_eq!( - Some("SOURCE"), - app.get_one::(ARG_SOURCE).map(String::as_str) - ); - } - - #[test] - fn it_should_match_target_arg() { - macro_rules! get_matches_from { - ($($arg:expr),*) => { - Command::new("himalaya") - .arg(target_arg()) - .try_get_matches_from_mut(&["himalaya", $($arg,)*]) - }; - } - - let app = get_matches_from![]; - assert_eq!(ErrorKind::MissingRequiredArgument, app.unwrap_err().kind()); - - let app = get_matches_from!["TARGET"]; - assert_eq!( - Some("TARGET"), - app.unwrap() - .get_one::(ARG_TARGET) - .map(String::as_str) - ); - } -} diff --git a/src/folder/command/mod.rs b/src/folder/command/mod.rs index e00b0f9..54868ae 100644 --- a/src/folder/command/mod.rs +++ b/src/folder/command/mod.rs @@ -49,3 +49,194 @@ impl FolderSubcommand { } } } + +#[cfg(test)] +mod tests { + use async_trait::async_trait; + use email::{ + account::config::AccountConfig, + backend::Backend, + envelope::{Envelope, Envelopes}, + flag::Flags, + folder::{Folder, Folders}, + message::Messages, + }; + use std::{any::Any, fmt::Debug, io}; + use termcolor::ColorSpec; + + use crate::printer::{Print, PrintTable, WriteColor}; + + use super::*; + + #[tokio::test] + async fn it_should_list_mboxes() { + #[derive(Debug, Default, Clone)] + struct StringWriter { + content: String, + } + + impl io::Write for StringWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.content + .push_str(&String::from_utf8(buf.to_vec()).unwrap()); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + self.content = String::default(); + Ok(()) + } + } + + impl termcolor::WriteColor for StringWriter { + fn supports_color(&self) -> bool { + false + } + + fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> { + io::Result::Ok(()) + } + + fn reset(&mut self) -> io::Result<()> { + io::Result::Ok(()) + } + } + + impl WriteColor for StringWriter {} + + #[derive(Debug, Default)] + struct PrinterServiceTest { + pub writer: StringWriter, + } + + impl Printer for PrinterServiceTest { + fn print_table( + &mut self, + data: Box, + opts: PrintTableOpts, + ) -> anyhow::Result<()> { + data.print_table(&mut self.writer, opts)?; + Ok(()) + } + fn print_log(&mut self, _data: T) -> anyhow::Result<()> { + unimplemented!() + } + fn print( + &mut self, + _data: T, + ) -> anyhow::Result<()> { + unimplemented!() + } + fn is_json(&self) -> bool { + unimplemented!() + } + } + + struct TestBackend; + + #[async_trait] + impl Backend for TestBackend { + fn name(&self) -> String { + unimplemented!(); + } + async fn add_folder(&mut self, _: &str) -> email::Result<()> { + unimplemented!(); + } + async fn list_folders(&mut self) -> email::Result { + Ok(Folders::from_iter([ + Folder { + name: "INBOX".into(), + desc: "desc".into(), + }, + Folder { + name: "Sent".into(), + desc: "desc".into(), + }, + ])) + } + async fn expunge_folder(&mut self, _: &str) -> email::Result<()> { + unimplemented!(); + } + async fn purge_folder(&mut self, _: &str) -> email::Result<()> { + unimplemented!(); + } + async fn delete_folder(&mut self, _: &str) -> email::Result<()> { + unimplemented!(); + } + async fn get_envelope(&mut self, _: &str, _: &str) -> email::Result { + unimplemented!(); + } + async fn list_envelopes( + &mut self, + _: &str, + _: usize, + _: usize, + ) -> email::Result { + unimplemented!() + } + async fn search_envelopes( + &mut self, + _: &str, + _: &str, + _: &str, + _: usize, + _: usize, + ) -> email::Result { + unimplemented!() + } + async fn add_email(&mut self, _: &str, _: &[u8], _: &Flags) -> email::Result { + unimplemented!() + } + async fn get_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result { + unimplemented!() + } + async fn preview_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result { + unimplemented!() + } + async fn copy_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> { + unimplemented!() + } + async fn move_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> { + unimplemented!() + } + async fn delete_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<()> { + unimplemented!() + } + async fn add_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> { + unimplemented!() + } + async fn set_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> { + unimplemented!() + } + async fn remove_flags( + &mut self, + _: &str, + _: Vec<&str>, + _: &Flags, + ) -> email::Result<()> { + unimplemented!() + } + fn as_any(&self) -> &dyn Any { + unimplemented!() + } + } + + let account_config = AccountConfig::default(); + let mut printer = PrinterServiceTest::default(); + let mut backend = TestBackend {}; + + assert!(list(&account_config, &mut printer, &mut backend, None) + .await + .is_ok()); + assert_eq!( + concat![ + "\n", + "NAME │DESC \n", + "INBOX │desc \n", + "Sent │desc \n", + "\n" + ], + printer.writer.content + ); + } +} diff --git a/src/folder/handlers.rs b/src/folder/handlers.rs deleted file mode 100644 index e43d34a..0000000 --- a/src/folder/handlers.rs +++ /dev/null @@ -1,247 +0,0 @@ -//! Folder handling module. -//! -//! This module gathers all folder actions triggered by the CLI. - -use anyhow::Result; -use dialoguer::Confirm; -use email::account::config::AccountConfig; -use std::process; - -use crate::{ - backend::Backend, - printer::{PrintTableOpts, Printer}, -}; - -use super::Folders; - -pub async fn expunge(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> { - backend.expunge_folder(folder).await?; - printer.print(format!("Folder {folder} successfully expunged!")) -} - -pub async fn list( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - max_width: Option, -) -> Result<()> { - let folders: Folders = backend.list_folders().await?.into(); - printer.print_table( - // TODO: remove Box - Box::new(folders), - PrintTableOpts { - format: &config.email_reading_format, - max_width, - }, - ) -} - -pub async fn create(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> { - backend.add_folder(folder).await?; - printer.print("Folder successfully created!") -} - -pub async fn delete(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> { - if let Some(false) | None = Confirm::new() - .with_prompt(format!("Confirm deletion of folder {folder}?")) - .default(false) - .report(false) - .interact_opt()? - { - process::exit(0); - }; - - backend.delete_folder(folder).await?; - printer.print("Folder successfully deleted!") -} - -#[cfg(test)] -mod tests { - use async_trait::async_trait; - use email::{ - account::config::AccountConfig, - backend::Backend, - envelope::{Envelope, Envelopes}, - flag::Flags, - folder::{Folder, Folders}, - message::Messages, - }; - use std::{any::Any, fmt::Debug, io}; - use termcolor::ColorSpec; - - use crate::printer::{Print, PrintTable, WriteColor}; - - use super::*; - - #[tokio::test] - async fn it_should_list_mboxes() { - #[derive(Debug, Default, Clone)] - struct StringWriter { - content: String, - } - - impl io::Write for StringWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.content - .push_str(&String::from_utf8(buf.to_vec()).unwrap()); - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - self.content = String::default(); - Ok(()) - } - } - - impl termcolor::WriteColor for StringWriter { - fn supports_color(&self) -> bool { - false - } - - fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> { - io::Result::Ok(()) - } - - fn reset(&mut self) -> io::Result<()> { - io::Result::Ok(()) - } - } - - impl WriteColor for StringWriter {} - - #[derive(Debug, Default)] - struct PrinterServiceTest { - pub writer: StringWriter, - } - - impl Printer for PrinterServiceTest { - fn print_table( - &mut self, - data: Box, - opts: PrintTableOpts, - ) -> anyhow::Result<()> { - data.print_table(&mut self.writer, opts)?; - Ok(()) - } - fn print_log(&mut self, _data: T) -> anyhow::Result<()> { - unimplemented!() - } - fn print( - &mut self, - _data: T, - ) -> anyhow::Result<()> { - unimplemented!() - } - fn is_json(&self) -> bool { - unimplemented!() - } - } - - struct TestBackend; - - #[async_trait] - impl Backend for TestBackend { - fn name(&self) -> String { - unimplemented!(); - } - async fn add_folder(&mut self, _: &str) -> email::Result<()> { - unimplemented!(); - } - async fn list_folders(&mut self) -> email::Result { - Ok(Folders::from_iter([ - Folder { - name: "INBOX".into(), - desc: "desc".into(), - }, - Folder { - name: "Sent".into(), - desc: "desc".into(), - }, - ])) - } - async fn expunge_folder(&mut self, _: &str) -> email::Result<()> { - unimplemented!(); - } - async fn purge_folder(&mut self, _: &str) -> email::Result<()> { - unimplemented!(); - } - async fn delete_folder(&mut self, _: &str) -> email::Result<()> { - unimplemented!(); - } - async fn get_envelope(&mut self, _: &str, _: &str) -> email::Result { - unimplemented!(); - } - async fn list_envelopes( - &mut self, - _: &str, - _: usize, - _: usize, - ) -> email::Result { - unimplemented!() - } - async fn search_envelopes( - &mut self, - _: &str, - _: &str, - _: &str, - _: usize, - _: usize, - ) -> email::Result { - unimplemented!() - } - async fn add_email(&mut self, _: &str, _: &[u8], _: &Flags) -> email::Result { - unimplemented!() - } - async fn get_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result { - unimplemented!() - } - async fn preview_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result { - unimplemented!() - } - async fn copy_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> { - unimplemented!() - } - async fn move_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> { - unimplemented!() - } - async fn delete_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<()> { - unimplemented!() - } - async fn add_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> { - unimplemented!() - } - async fn set_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> { - unimplemented!() - } - async fn remove_flags( - &mut self, - _: &str, - _: Vec<&str>, - _: &Flags, - ) -> email::Result<()> { - unimplemented!() - } - fn as_any(&self) -> &dyn Any { - unimplemented!() - } - } - - let account_config = AccountConfig::default(); - let mut printer = PrinterServiceTest::default(); - let mut backend = TestBackend {}; - - assert!(list(&account_config, &mut printer, &mut backend, None) - .await - .is_ok()); - assert_eq!( - concat![ - "\n", - "NAME │DESC \n", - "INBOX │desc \n", - "Sent │desc \n", - "\n" - ], - printer.writer.content - ); - } -} diff --git a/src/folder/mod.rs b/src/folder/mod.rs index 67fd4c0..577abab 100644 --- a/src/folder/mod.rs +++ b/src/folder/mod.rs @@ -1,8 +1,6 @@ pub mod arg; -pub mod args; pub mod command; pub mod config; -pub mod handlers; use anyhow::Result; use serde::Serialize; diff --git a/src/ui/table/args.rs b/src/ui/table/args.rs deleted file mode 100644 index e29a8f0..0000000 --- a/src/ui/table/args.rs +++ /dev/null @@ -1,21 +0,0 @@ -use clap::{Arg, ArgMatches}; - -const ARG_MAX_TABLE_WIDTH: &str = "max-table-width"; - -pub(crate) type MaxTableWidth = Option; - -/// Represents the max table width argument. -pub fn max_width() -> Arg { - Arg::new(ARG_MAX_TABLE_WIDTH) - .help("Defines a maximum width for the table") - .long("max-width") - .short('w') - .value_name("INT") -} - -/// Represents the max table width argument parser. -pub fn parse_max_width(matches: &ArgMatches) -> Option { - matches - .get_one::(ARG_MAX_TABLE_WIDTH) - .and_then(|s| s.parse().ok()) -} diff --git a/src/ui/table/mod.rs b/src/ui/table/mod.rs index b07ca63..fb49b3c 100644 --- a/src/ui/table/mod.rs +++ b/src/ui/table/mod.rs @@ -1,5 +1,4 @@ pub mod arg; -pub mod args; pub mod table; pub use table::*; From 5e1a03e3c1facff6dcded573ac5c2c9bfd68d62c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Thu, 7 Dec 2023 10:10:18 +0100 Subject: [PATCH 19/29] refactor flag with clap derive api --- src/cli.rs | 12 ++- src/email/envelope/flag/arg/ids_and_flags.rs | 51 +++++++++++++ src/email/envelope/flag/arg/mod.rs | 1 + src/email/envelope/flag/command/add.rs | 48 ++++++++++++ src/email/envelope/flag/command/mod.rs | 39 ++++++++++ src/email/envelope/flag/command/remove.rs | 48 ++++++++++++ src/email/envelope/flag/command/set.rs | 48 ++++++++++++ src/email/envelope/flag/mod.rs | 4 +- src/main.rs | 79 ++------------------ 9 files changed, 254 insertions(+), 76 deletions(-) create mode 100644 src/email/envelope/flag/arg/ids_and_flags.rs create mode 100644 src/email/envelope/flag/arg/mod.rs create mode 100644 src/email/envelope/flag/command/add.rs create mode 100644 src/email/envelope/flag/command/mod.rs create mode 100644 src/email/envelope/flag/command/remove.rs create mode 100644 src/email/envelope/flag/command/set.rs diff --git a/src/cli.rs b/src/cli.rs index 0670587..5745387 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,6 +7,7 @@ use crate::{ completion::command::CompletionGenerateCommand, config::{self, TomlConfig}, envelope::command::EnvelopeSubcommand, + flag::command::FlagSubcommand, folder::command::FolderSubcommand, manual::command::ManualGenerateCommand, output::{ColorFmt, OutputFmt}, @@ -90,17 +91,21 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum HimalayaCommand { /// Subcommand to manage accounts - #[command(subcommand)] + #[command(subcommand, alias = "accounts")] Account(AccountSubcommand), /// Subcommand to manage folders - #[command(subcommand)] + #[command(subcommand, alias = "folders")] Folder(FolderSubcommand), /// Subcommand to manage envelopes - #[command(subcommand)] + #[command(subcommand, alias = "envelopes")] Envelope(EnvelopeSubcommand), + /// Subcommand to manage flags + #[command(subcommand, alias = "flags")] + Flag(FlagSubcommand), + /// Generate manual pages to a directory #[command(arg_required_else_help = true)] Manual(ManualGenerateCommand), @@ -116,6 +121,7 @@ impl HimalayaCommand { Self::Account(cmd) => cmd.execute(printer, config).await, Self::Folder(cmd) => cmd.execute(printer, config).await, Self::Envelope(cmd) => cmd.execute(printer, config).await, + Self::Flag(cmd) => cmd.execute(printer, config).await, Self::Manual(cmd) => cmd.execute(printer).await, Self::Completion(cmd) => cmd.execute(printer).await, } diff --git a/src/email/envelope/flag/arg/ids_and_flags.rs b/src/email/envelope/flag/arg/ids_and_flags.rs new file mode 100644 index 0000000..f1eb40f --- /dev/null +++ b/src/email/envelope/flag/arg/ids_and_flags.rs @@ -0,0 +1,51 @@ +use clap::Parser; +use email::flag::{Flag, Flags}; +use log::debug; + +/// The ids and/or flags arguments parser +#[derive(Debug, Parser)] +pub struct IdsAndFlagsArgs { + /// The list of ids and/or flags + /// + /// Every argument that can be parsed as an integer is considered + /// an id, otherwise it is considered as a flag. + #[arg(value_name = "ID-OR-FLAG", required = true)] + pub ids_and_flags: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub enum IdOrFlag { + Id(String), + Flag(Flag), +} + +impl From<&str> for IdOrFlag { + fn from(value: &str) -> Self { + value + .parse::() + .map(|_| Self::Id(value.to_owned())) + .unwrap_or_else(|err| { + let flag = Flag::from(value); + debug!("cannot parse {value} as usize, parsing it as flag {flag}"); + debug!("{err:?}"); + Self::Flag(flag) + }) + } +} + +pub fn to_tuple<'a>(ids_and_flags: &'a [IdOrFlag]) -> (Vec<&'a str>, Flags) { + ids_and_flags.iter().fold( + (Vec::default(), Flags::default()), + |(mut ids, mut flags), arg| { + match arg { + IdOrFlag::Id(id) => { + ids.push(id.as_str()); + } + IdOrFlag::Flag(flag) => { + flags.insert(flag.to_owned()); + } + }; + (ids, flags) + }, + ) +} diff --git a/src/email/envelope/flag/arg/mod.rs b/src/email/envelope/flag/arg/mod.rs new file mode 100644 index 0000000..c98b755 --- /dev/null +++ b/src/email/envelope/flag/arg/mod.rs @@ -0,0 +1 @@ +pub mod ids_and_flags; diff --git a/src/email/envelope/flag/command/add.rs b/src/email/envelope/flag/command/add.rs new file mode 100644 index 0000000..c09538e --- /dev/null +++ b/src/email/envelope/flag/command/add.rs @@ -0,0 +1,48 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + flag::arg::ids_and_flags::{to_tuple, IdsAndFlagsArgs}, + folder::arg::name::FolderNameArg, + printer::Printer, +}; + +/// Add flag(s) to an envelope +#[derive(Debug, Parser)] +pub struct FlagAddCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub args: IdsAndFlagsArgs, + + #[command(flatten)] + pub account: AccountNameFlag, + + #[command(flatten)] + pub cache: DisableCacheFlag, +} + +impl FlagAddCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing flag add command"); + + let folder = &self.folder.name; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + let (ids, flags) = to_tuple(&self.args.ids_and_flags); + backend.add_flags(folder, &ids, &flags).await?; + + printer.print(format!("Flag(s) {flags} successfully added!")) + } +} diff --git a/src/email/envelope/flag/command/mod.rs b/src/email/envelope/flag/command/mod.rs new file mode 100644 index 0000000..05c7a2f --- /dev/null +++ b/src/email/envelope/flag/command/mod.rs @@ -0,0 +1,39 @@ +mod add; +mod remove; +mod set; + +use anyhow::Result; +use clap::Subcommand; + +use crate::{config::TomlConfig, printer::Printer}; + +use self::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand}; + +/// Subcommand to manage flags +#[derive(Debug, Subcommand)] +pub enum FlagSubcommand { + /// Add flag(s) to an envelope + #[command(arg_required_else_help = true)] + #[command(alias = "create")] + Add(FlagAddCommand), + + /// Replace flag(s) of an envelope + #[command(arg_required_else_help = true)] + #[command(aliases = ["update", "change", "replace"])] + Set(FlagSetCommand), + + /// Remove flag(s) from an envelope + #[command(arg_required_else_help = true)] + #[command(aliases = ["rm", "delete", "del"])] + Remove(FlagRemoveCommand), +} + +impl FlagSubcommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + match self { + Self::Add(cmd) => cmd.execute(printer, config).await, + Self::Set(cmd) => cmd.execute(printer, config).await, + Self::Remove(cmd) => cmd.execute(printer, config).await, + } + } +} diff --git a/src/email/envelope/flag/command/remove.rs b/src/email/envelope/flag/command/remove.rs new file mode 100644 index 0000000..fae6c22 --- /dev/null +++ b/src/email/envelope/flag/command/remove.rs @@ -0,0 +1,48 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + flag::arg::ids_and_flags::{to_tuple, IdsAndFlagsArgs}, + folder::arg::name::FolderNameArg, + printer::Printer, +}; + +/// Remove flag(s) from an envelope +#[derive(Debug, Parser)] +pub struct FlagRemoveCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub args: IdsAndFlagsArgs, + + #[command(flatten)] + pub account: AccountNameFlag, + + #[command(flatten)] + pub cache: DisableCacheFlag, +} + +impl FlagRemoveCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing flag remove command"); + + let folder = &self.folder.name; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + let (ids, flags) = to_tuple(&self.args.ids_and_flags); + backend.remove_flags(folder, &ids, &flags).await?; + + printer.print(format!("Flag(s) {flags} successfully removed!")) + } +} diff --git a/src/email/envelope/flag/command/set.rs b/src/email/envelope/flag/command/set.rs new file mode 100644 index 0000000..f93edae --- /dev/null +++ b/src/email/envelope/flag/command/set.rs @@ -0,0 +1,48 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + flag::arg::ids_and_flags::{to_tuple, IdsAndFlagsArgs}, + folder::arg::name::FolderNameArg, + printer::Printer, +}; + +/// Replace flag(s) of an envelope +#[derive(Debug, Parser)] +pub struct FlagSetCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub args: IdsAndFlagsArgs, + + #[command(flatten)] + pub account: AccountNameFlag, + + #[command(flatten)] + pub cache: DisableCacheFlag, +} + +impl FlagSetCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing flag set command"); + + let folder = &self.folder.name; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + let (ids, flags) = to_tuple(&self.args.ids_and_flags); + backend.set_flags(folder, &ids, &flags).await?; + + printer.print(format!("Flag(s) {flags} successfully set!")) + } +} diff --git a/src/email/envelope/flag/mod.rs b/src/email/envelope/flag/mod.rs index 2416abc..bd3e0ec 100644 --- a/src/email/envelope/flag/mod.rs +++ b/src/email/envelope/flag/mod.rs @@ -1,4 +1,6 @@ +pub mod arg; pub mod args; +pub mod command; pub mod config; pub mod handlers; @@ -6,7 +8,7 @@ use serde::Serialize; use std::{collections::HashSet, ops}; /// Represents the flag variants. -#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)] +#[derive(Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd, Serialize)] pub enum Flag { Seen, Answered, diff --git a/src/main.rs b/src/main.rs index 8a2a343..cbba050 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,16 @@ use anyhow::Result; use clap::Parser; use env_logger::{Builder as LoggerBuilder, Env, DEFAULT_FILTER_ENV}; use himalaya::{cli::Cli, config::TomlConfig, printer::StdoutPrinter}; +use log::{debug, warn}; #[tokio::main] async fn main() -> Result<()> { + #[cfg(not(target_os = "windows"))] + if let Err((_, err)) = coredump::register_panic_handler() { + warn!("cannot register custom panic handler: {err}"); + debug!("{err:?}"); + } + LoggerBuilder::new() .parse_env(Env::new().filter_or(DEFAULT_FILTER_ENV, "warn")) .format_timestamp(None) @@ -20,30 +27,12 @@ async fn main() -> Result<()> { // fn create_app() -> clap::Command { // clap::Command::new(env!("CARGO_PKG_NAME")) -// .version(env!("CARGO_PKG_VERSION")) -// .about(env!("CARGO_PKG_DESCRIPTION")) -// .author(env!("CARGO_PKG_AUTHORS")) -// .propagate_version(true) -// .infer_subcommands(true) -// .args(cache::args::global_args()) -// .args(output::args::global_args()) -// .subcommand(envelope::args::subcmd()) -// .subcommand(flag::args::subcmd()) // .subcommand(message::args::subcmd()) // .subcommand(template::args::subcmd()) // } // #[tokio::main] // async fn main() -> Result<()> { -// #[cfg(not(target_os = "windows"))] -// if let Err((_, err)) = coredump::register_panic_handler() { -// warn!("cannot register custom panic handler: {err}"); -// debug!("cannot register custom panic handler: {err:?}"); -// } - -// let default_env_filter = env_logger::DEFAULT_FILTER_ENV; -// env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off")); - // // check mailto command before app initialization // let raw_args: Vec = env::args().collect(); // if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { @@ -59,58 +48,6 @@ async fn main() -> Result<()> { // return message::handlers::mailto(&account_config, &backend, &mut printer, &url).await; // } -// let app = _create_app(); -// let m = app.get_matches(); - -// let some_config_path = config::args::parse_global_arg(&m); -// let some_account_name = account::command::parse_global_arg(&m); -// let disable_cache = cache::args::parse_disable_cache_arg(&m); - -// let toml_config = TomlConfig::from_some_path_or_default(some_config_path).await?; - -// let mut printer = StdoutPrinter::try_from(&m)?; - -// let (toml_account_config, account_config) = toml_config -// .clone() -// .into_account_configs(some_account_name, disable_cache)?; - -// match envelope::args::matches(&m)? { -// Some(envelope::args::Cmd::List(max_width, page_size, page)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return envelope::handlers::list( -// &account_config, -// &mut printer, -// &backend, -// &folder, -// max_width, -// page_size, -// page, -// ) -// .await; -// } -// _ => (), -// } - -// match flag::args::matches(&m)? { -// Some(flag::args::Cmd::Set(ids, ref flags)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await; -// } -// Some(flag::args::Cmd::Add(ids, ref flags)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await; -// } -// Some(flag::args::Cmd::Remove(ids, ref flags)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await; -// } -// _ => (), -// } - // match message::args::matches(&m)? { // Some(message::args::Cmd::Attachments(ids)) => { // let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); @@ -259,6 +196,4 @@ async fn main() -> Result<()> { // } // _ => (), // } - -// Ok(()) // } From a47902af7d9ea9a9ca43bbd23c252f8eb8ba400c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Thu, 7 Dec 2023 12:19:45 +0100 Subject: [PATCH 20/29] refactor message with clap derive api (part 1) --- src/backend/mod.rs | 21 ++- src/cache/mod.rs | 6 +- src/cli.rs | 44 ++++--- src/email/envelope/arg/ids.rs | 9 ++ src/email/envelope/arg/mod.rs | 1 + src/email/envelope/flag/arg/ids_and_flags.rs | 21 ++- src/email/envelope/flag/args.rs | 110 ---------------- src/email/envelope/flag/command/add.rs | 4 +- src/email/envelope/flag/command/remove.rs | 4 +- src/email/envelope/flag/command/set.rs | 4 +- src/email/envelope/flag/handlers.rs | 37 ------ src/email/envelope/flag/mod.rs | 2 - src/email/envelope/mod.rs | 1 + src/email/message/command/copy.rs | 52 ++++++++ src/email/message/command/delete.rs | 44 +++++++ src/email/message/command/mod.rs | 58 ++++++++ src/email/message/command/move_.rs | 52 ++++++++ src/email/message/command/read.rs | 117 ++++++++++++++++ src/email/message/command/save.rs | 61 +++++++++ src/email/message/command/send.rs | 65 +++++++++ src/email/message/handlers.rs | 132 ------------------- src/email/message/mod.rs | 7 +- src/email/mod.rs | 3 +- src/folder/arg/name.rs | 16 +++ src/lib.rs | 3 +- src/main.rs | 30 ----- 26 files changed, 539 insertions(+), 365 deletions(-) create mode 100644 src/email/envelope/arg/ids.rs create mode 100644 src/email/envelope/arg/mod.rs delete mode 100644 src/email/envelope/flag/args.rs delete mode 100644 src/email/envelope/flag/handlers.rs create mode 100644 src/email/message/command/copy.rs create mode 100644 src/email/message/command/delete.rs create mode 100644 src/email/message/command/mod.rs create mode 100644 src/email/message/command/move_.rs create mode 100644 src/email/message/command/read.rs create mode 100644 src/email/message/command/save.rs create mode 100644 src/email/message/command/send.rs diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 4084e0b..b659b8f 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -723,28 +723,35 @@ impl Backend { Ok(envelopes) } - pub async fn add_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { + pub async fn add_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> { let backend_kind = self.toml_account_config.add_flags_kind(); let id_mapper = self.build_id_mapper(folder, backend_kind)?; let ids = Id::multiple(id_mapper.get_ids(ids)?); self.backend.add_flags(folder, &ids, flags).await } - pub async fn set_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { + pub async fn set_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> { let backend_kind = self.toml_account_config.set_flags_kind(); let id_mapper = self.build_id_mapper(folder, backend_kind)?; let ids = Id::multiple(id_mapper.get_ids(ids)?); self.backend.set_flags(folder, &ids, flags).await } - pub async fn remove_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { + pub async fn remove_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> { let backend_kind = self.toml_account_config.remove_flags_kind(); let id_mapper = self.build_id_mapper(folder, backend_kind)?; let ids = Id::multiple(id_mapper.get_ids(ids)?); self.backend.remove_flags(folder, &ids, flags).await } - pub async fn get_messages(&self, folder: &str, ids: &[&str]) -> Result { + pub async fn peek_messages(&self, folder: &str, ids: &[usize]) -> Result { + let backend_kind = self.toml_account_config.get_messages_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.peek_messages(folder, &ids).await + } + + pub async fn get_messages(&self, folder: &str, ids: &[usize]) -> Result { let backend_kind = self.toml_account_config.get_messages_kind(); let id_mapper = self.build_id_mapper(folder, backend_kind)?; let ids = Id::multiple(id_mapper.get_ids(ids)?); @@ -755,7 +762,7 @@ impl Backend { &self, from_folder: &str, to_folder: &str, - ids: &[&str], + ids: &[usize], ) -> Result<()> { let backend_kind = self.toml_account_config.move_messages_kind(); let id_mapper = self.build_id_mapper(from_folder, backend_kind)?; @@ -769,7 +776,7 @@ impl Backend { &self, from_folder: &str, to_folder: &str, - ids: &[&str], + ids: &[usize], ) -> Result<()> { let backend_kind = self.toml_account_config.move_messages_kind(); let id_mapper = self.build_id_mapper(from_folder, backend_kind)?; @@ -779,7 +786,7 @@ impl Backend { .await } - pub async fn delete_messages(&self, folder: &str, ids: &[&str]) -> Result<()> { + pub async fn delete_messages(&self, folder: &str, ids: &[usize]) -> Result<()> { let backend_kind = self.toml_account_config.delete_messages_kind(); let id_mapper = self.build_id_mapper(folder, backend_kind)?; let ids = Id::multiple(id_mapper.get_ids(ids)?); diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 4b51a20..ed6d4ad 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -122,9 +122,9 @@ impl IdMapper { pub fn get_id(&self, alias: A) -> Result where - A: AsRef, + A: ToString, { - let alias = alias.as_ref(); + let alias = alias.to_string(); let alias = alias .parse::() .context(format!("cannot parse id mapper alias {alias}"))?; @@ -158,7 +158,7 @@ impl IdMapper { pub fn get_ids(&self, aliases: I) -> Result> where - A: AsRef, + A: ToString, I: IntoIterator, { aliases diff --git a/src/cli.rs b/src/cli.rs index 5745387..4ae7964 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,6 +10,7 @@ use crate::{ flag::command::FlagSubcommand, folder::command::FolderSubcommand, manual::command::ManualGenerateCommand, + message::command::MessageSubcommand, output::{ColorFmt, OutputFmt}, printer::Printer, }; @@ -33,7 +34,8 @@ pub struct Cli { /// applicable). If the path does not point to a valid file, the /// wizard will propose to assist you in the creation of the /// configuration file. - #[arg(long, short, value_name = "PATH", global = true, value_parser = config::path_parser)] + #[arg(long, short, global = true)] + #[arg(value_name = "PATH", value_parser = config::path_parser)] pub config: Option, /// Customize the output format @@ -47,14 +49,8 @@ pub struct Cli { /// /// - plain: output will be in a form of either a plain text or /// table, depending on the command - #[arg( - long, - short, - value_name = "FORMAT", - global = true, - value_enum, - default_value_t = Default::default(), - )] + #[arg(long, short, global = true)] + #[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())] pub output: OutputFmt, /// Control when to use colors @@ -77,41 +73,46 @@ pub struct Cli { /// - ansi: like 'always', but emits ANSI escapes (even in a Windows console) /// /// - auto: himalaya tries to be smart - #[arg( - long, - short = 'C', - value_name = "MODE", - global = true, - value_enum, - default_value_t = Default::default(), - )] + #[arg(long, short = 'C', global = true)] + #[arg(value_name = "MODE", value_enum, default_value_t = Default::default())] pub color: ColorFmt, } #[derive(Subcommand, Debug)] pub enum HimalayaCommand { /// Subcommand to manage accounts - #[command(subcommand, alias = "accounts")] + #[command(subcommand)] + #[command(alias = "accounts")] Account(AccountSubcommand), /// Subcommand to manage folders - #[command(subcommand, alias = "folders")] + #[command(subcommand)] + #[command(alias = "folders")] Folder(FolderSubcommand), /// Subcommand to manage envelopes - #[command(subcommand, alias = "envelopes")] + #[command(subcommand)] + #[command(alias = "envelopes")] Envelope(EnvelopeSubcommand), /// Subcommand to manage flags - #[command(subcommand, alias = "flags")] + #[command(subcommand)] + #[command(alias = "flags")] Flag(FlagSubcommand), + /// Subcommand to manage messages + #[command(subcommand)] + #[command(alias = "messages", alias = "msgs", alias = "msg")] + Message(MessageSubcommand), + /// Generate manual pages to a directory #[command(arg_required_else_help = true)] + #[command(alias = "manuals", alias = "mans")] Manual(ManualGenerateCommand), /// Print completion script for a shell to stdout #[command(arg_required_else_help = true)] + #[command(alias = "completions")] Completion(CompletionGenerateCommand), } @@ -122,6 +123,7 @@ impl HimalayaCommand { Self::Folder(cmd) => cmd.execute(printer, config).await, Self::Envelope(cmd) => cmd.execute(printer, config).await, Self::Flag(cmd) => cmd.execute(printer, config).await, + Self::Message(cmd) => cmd.execute(printer, config).await, Self::Manual(cmd) => cmd.execute(printer).await, Self::Completion(cmd) => cmd.execute(printer).await, } diff --git a/src/email/envelope/arg/ids.rs b/src/email/envelope/arg/ids.rs new file mode 100644 index 0000000..8fee631 --- /dev/null +++ b/src/email/envelope/arg/ids.rs @@ -0,0 +1,9 @@ +use clap::Parser; + +/// The envelopes ids arguments parser +#[derive(Debug, Parser)] +pub struct EnvelopeIdsArgs { + /// The list of envelopes ids + #[arg(value_name = "ID", required = true)] + pub ids: Vec, +} diff --git a/src/email/envelope/arg/mod.rs b/src/email/envelope/arg/mod.rs new file mode 100644 index 0000000..ff6b00d --- /dev/null +++ b/src/email/envelope/arg/mod.rs @@ -0,0 +1 @@ +pub mod ids; diff --git a/src/email/envelope/flag/arg/ids_and_flags.rs b/src/email/envelope/flag/arg/ids_and_flags.rs index f1eb40f..fe0e146 100644 --- a/src/email/envelope/flag/arg/ids_and_flags.rs +++ b/src/email/envelope/flag/arg/ids_and_flags.rs @@ -15,31 +15,28 @@ pub struct IdsAndFlagsArgs { #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] pub enum IdOrFlag { - Id(String), + Id(usize), Flag(Flag), } impl From<&str> for IdOrFlag { fn from(value: &str) -> Self { - value - .parse::() - .map(|_| Self::Id(value.to_owned())) - .unwrap_or_else(|err| { - let flag = Flag::from(value); - debug!("cannot parse {value} as usize, parsing it as flag {flag}"); - debug!("{err:?}"); - Self::Flag(flag) - }) + value.parse::().map(Self::Id).unwrap_or_else(|err| { + let flag = Flag::from(value); + debug!("cannot parse {value} as usize, parsing it as flag {flag}"); + debug!("{err:?}"); + Self::Flag(flag) + }) } } -pub fn to_tuple<'a>(ids_and_flags: &'a [IdOrFlag]) -> (Vec<&'a str>, Flags) { +pub fn into_tuple(ids_and_flags: &[IdOrFlag]) -> (Vec, Flags) { ids_and_flags.iter().fold( (Vec::default(), Flags::default()), |(mut ids, mut flags), arg| { match arg { IdOrFlag::Id(id) => { - ids.push(id.as_str()); + ids.push(*id); } IdOrFlag::Flag(flag) => { flags.insert(flag.to_owned()); diff --git a/src/email/envelope/flag/args.rs b/src/email/envelope/flag/args.rs deleted file mode 100644 index 7eb528a..0000000 --- a/src/email/envelope/flag/args.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! Email flag CLI module. -//! -//! This module contains the command matcher, the subcommands and the -//! arguments related to the email flag domain. - -use ::email::flag::{Flag, Flags}; -use anyhow::Result; -use clap::{Arg, ArgMatches, Command}; -use log::{debug, info}; - -use crate::message; - -const ARG_FLAGS: &str = "flag"; - -const CMD_ADD: &str = "add"; -const CMD_REMOVE: &str = "remove"; -const CMD_SET: &str = "set"; - -pub(crate) const CMD_FLAG: &str = "flags"; - -/// Represents the flag commands. -#[derive(Debug, PartialEq, Eq)] -pub enum Cmd<'a> { - Add(message::args::Ids<'a>, Flags), - Remove(message::args::Ids<'a>, Flags), - Set(message::args::Ids<'a>, Flags), -} - -/// Represents the flag command matcher. -pub fn matches(m: &ArgMatches) -> Result> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_FLAG) { - if let Some(m) = m.subcommand_matches(CMD_ADD) { - debug!("add flags command matched"); - let ids = message::args::parse_ids_arg(m); - let flags = parse_flags_arg(m); - Some(Cmd::Add(ids, flags)) - } else if let Some(m) = m.subcommand_matches(CMD_REMOVE) { - info!("remove flags command matched"); - let ids = message::args::parse_ids_arg(m); - let flags = parse_flags_arg(m); - Some(Cmd::Remove(ids, flags)) - } else if let Some(m) = m.subcommand_matches(CMD_SET) { - debug!("set flags command matched"); - let ids = message::args::parse_ids_arg(m); - let flags = parse_flags_arg(m); - Some(Cmd::Set(ids, flags)) - } else { - None - } - } else { - None - }; - - Ok(cmd) -} - -/// Represents the flag subcommand. -pub fn subcmd() -> Command { - Command::new(CMD_FLAG) - .about("Subcommand to manage flags") - .long_about("Subcommand to manage flags like add, set or remove") - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand( - Command::new(CMD_ADD) - .about("Adds flags to an email") - .arg(message::args::ids_arg()) - .arg(flags_arg()), - ) - .subcommand( - Command::new(CMD_REMOVE) - .aliases(["delete", "del", "d"]) - .about("Removes flags from an email") - .arg(message::args::ids_arg()) - .arg(flags_arg()), - ) - .subcommand( - Command::new(CMD_SET) - .aliases(["change", "c"]) - .about("Sets flags of an email") - .arg(message::args::ids_arg()) - .arg(flags_arg()), - ) -} - -/// Represents the flags argument. -pub fn flags_arg() -> Arg { - Arg::new(ARG_FLAGS) - .value_name("FLAGS") - .help("The flags") - .long_help( - "The list of flags. -It can be one of: seen, answered, flagged, deleted, or draft. -Other flags are considered custom.", - ) - .num_args(1..) - .required(true) - .last(true) -} - -/// Represents the flags argument parser. -pub fn parse_flags_arg(matches: &ArgMatches) -> Flags { - Flags::from_iter( - matches - .get_many::(ARG_FLAGS) - .unwrap_or_default() - .map(String::as_str) - .map(Flag::from), - ) -} diff --git a/src/email/envelope/flag/command/add.rs b/src/email/envelope/flag/command/add.rs index c09538e..63d7dd3 100644 --- a/src/email/envelope/flag/command/add.rs +++ b/src/email/envelope/flag/command/add.rs @@ -7,7 +7,7 @@ use crate::{ backend::Backend, cache::arg::disable::DisableCacheFlag, config::TomlConfig, - flag::arg::ids_and_flags::{to_tuple, IdsAndFlagsArgs}, + flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, folder::arg::name::FolderNameArg, printer::Printer, }; @@ -40,7 +40,7 @@ impl FlagAddCommand { config.clone().into_account_configs(account, cache)?; let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - let (ids, flags) = to_tuple(&self.args.ids_and_flags); + let (ids, flags) = into_tuple(&self.args.ids_and_flags); backend.add_flags(folder, &ids, &flags).await?; printer.print(format!("Flag(s) {flags} successfully added!")) diff --git a/src/email/envelope/flag/command/remove.rs b/src/email/envelope/flag/command/remove.rs index fae6c22..59583f5 100644 --- a/src/email/envelope/flag/command/remove.rs +++ b/src/email/envelope/flag/command/remove.rs @@ -7,7 +7,7 @@ use crate::{ backend::Backend, cache::arg::disable::DisableCacheFlag, config::TomlConfig, - flag::arg::ids_and_flags::{to_tuple, IdsAndFlagsArgs}, + flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, folder::arg::name::FolderNameArg, printer::Printer, }; @@ -40,7 +40,7 @@ impl FlagRemoveCommand { config.clone().into_account_configs(account, cache)?; let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - let (ids, flags) = to_tuple(&self.args.ids_and_flags); + let (ids, flags) = into_tuple(&self.args.ids_and_flags); backend.remove_flags(folder, &ids, &flags).await?; printer.print(format!("Flag(s) {flags} successfully removed!")) diff --git a/src/email/envelope/flag/command/set.rs b/src/email/envelope/flag/command/set.rs index f93edae..cb04482 100644 --- a/src/email/envelope/flag/command/set.rs +++ b/src/email/envelope/flag/command/set.rs @@ -7,7 +7,7 @@ use crate::{ backend::Backend, cache::arg::disable::DisableCacheFlag, config::TomlConfig, - flag::arg::ids_and_flags::{to_tuple, IdsAndFlagsArgs}, + flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, folder::arg::name::FolderNameArg, printer::Printer, }; @@ -40,7 +40,7 @@ impl FlagSetCommand { config.clone().into_account_configs(account, cache)?; let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - let (ids, flags) = to_tuple(&self.args.ids_and_flags); + let (ids, flags) = into_tuple(&self.args.ids_and_flags); backend.set_flags(folder, &ids, &flags).await?; printer.print(format!("Flag(s) {flags} successfully set!")) diff --git a/src/email/envelope/flag/handlers.rs b/src/email/envelope/flag/handlers.rs deleted file mode 100644 index dc3812c..0000000 --- a/src/email/envelope/flag/handlers.rs +++ /dev/null @@ -1,37 +0,0 @@ -use anyhow::Result; -use email::flag::Flags; - -use crate::{backend::Backend, printer::Printer}; - -pub async fn add( - printer: &mut P, - backend: &Backend, - folder: &str, - ids: Vec<&str>, - flags: &Flags, -) -> Result<()> { - backend.add_flags(folder, &ids, flags).await?; - printer.print(format!("Flag(s) {flags} successfully added!")) -} - -pub async fn set( - printer: &mut P, - backend: &Backend, - folder: &str, - ids: Vec<&str>, - flags: &Flags, -) -> Result<()> { - backend.set_flags(folder, &ids, flags).await?; - printer.print(format!("Flag(s) {flags} successfully set!")) -} - -pub async fn remove( - printer: &mut P, - backend: &Backend, - folder: &str, - ids: Vec<&str>, - flags: &Flags, -) -> Result<()> { - backend.remove_flags(folder, &ids, flags).await?; - printer.print(format!("Flag(s) {flags} successfully removed!")) -} diff --git a/src/email/envelope/flag/mod.rs b/src/email/envelope/flag/mod.rs index bd3e0ec..75db6d3 100644 --- a/src/email/envelope/flag/mod.rs +++ b/src/email/envelope/flag/mod.rs @@ -1,8 +1,6 @@ pub mod arg; -pub mod args; pub mod command; pub mod config; -pub mod handlers; use serde::Serialize; use std::{collections::HashSet, ops}; diff --git a/src/email/envelope/mod.rs b/src/email/envelope/mod.rs index 9b025e7..ab856b9 100644 --- a/src/email/envelope/mod.rs +++ b/src/email/envelope/mod.rs @@ -1,3 +1,4 @@ +pub mod arg; pub mod command; pub mod config; pub mod flag; diff --git a/src/email/message/command/copy.rs b/src/email/message/command/copy.rs new file mode 100644 index 0000000..d99e4c2 --- /dev/null +++ b/src/email/message/command/copy.rs @@ -0,0 +1,52 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + envelope::arg::ids::EnvelopeIdsArgs, + folder::arg::name::{SourceFolderNameArg, TargetFolderNameArg}, + printer::Printer, +}; + +/// Copy a message from a source folder to a target folder +#[derive(Debug, Parser)] +pub struct MessageCopyCommand { + #[command(flatten)] + pub source_folder: SourceFolderNameArg, + + #[command(flatten)] + pub target_folder: TargetFolderNameArg, + + #[command(flatten)] + pub envelopes: EnvelopeIdsArgs, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageCopyCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message copy command"); + + let from_folder = &self.source_folder.name; + let to_folder = &self.target_folder.name; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + let ids = &self.envelopes.ids; + backend.copy_messages(from_folder, to_folder, ids).await?; + + printer.print("Message(s) successfully copied from {from_folder} to {to_folder}!") + } +} diff --git a/src/email/message/command/delete.rs b/src/email/message/command/delete.rs new file mode 100644 index 0000000..55dece0 --- /dev/null +++ b/src/email/message/command/delete.rs @@ -0,0 +1,44 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameArg, + printer::Printer, +}; + +/// Delete a message from a folder +#[derive(Debug, Parser)] +pub struct MessageDeleteCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub envelopes: EnvelopeIdsArgs, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageDeleteCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message delete command"); + + let folder = &self.folder.name; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + let ids = &self.envelopes.ids; + backend.delete_messages(folder, ids).await?; + + printer.print("Message(s) successfully deleted from {from_folder} to {to_folder}!") + } +} diff --git a/src/email/message/command/mod.rs b/src/email/message/command/mod.rs new file mode 100644 index 0000000..903c88a --- /dev/null +++ b/src/email/message/command/mod.rs @@ -0,0 +1,58 @@ +pub mod copy; +pub mod delete; +pub mod move_; +pub mod read; +pub mod save; +pub mod send; + +use anyhow::Result; +use clap::Subcommand; + +use crate::{config::TomlConfig, printer::Printer}; + +use self::{ + copy::MessageCopyCommand, delete::MessageDeleteCommand, move_::MessageMoveCommand, + read::MessageReadCommand, save::MessageSaveCommand, send::MessageSendCommand, +}; + +/// Subcommand to manage messages +#[derive(Debug, Subcommand)] +pub enum MessageSubcommand { + /// Read a message + #[command(arg_required_else_help = true)] + Read(MessageReadCommand), + + /// Save a message to a folder + #[command(arg_required_else_help = true)] + #[command(alias = "add", alias = "create")] + Save(MessageSaveCommand), + + /// Send a message + #[command(arg_required_else_help = true)] + Send(MessageSendCommand), + + /// Copy a message from a source folder to a target folder + #[command(arg_required_else_help = true)] + Copy(MessageCopyCommand), + + /// Move a message from a source folder to a target folder + #[command(arg_required_else_help = true)] + Move(MessageMoveCommand), + + /// Delete a message from a folder + #[command(arg_required_else_help = true)] + Delete(MessageDeleteCommand), +} + +impl MessageSubcommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + match self { + Self::Read(cmd) => cmd.execute(printer, config).await, + Self::Save(cmd) => cmd.execute(printer, config).await, + Self::Send(cmd) => cmd.execute(printer, config).await, + Self::Copy(cmd) => cmd.execute(printer, config).await, + Self::Move(cmd) => cmd.execute(printer, config).await, + Self::Delete(cmd) => cmd.execute(printer, config).await, + } + } +} diff --git a/src/email/message/command/move_.rs b/src/email/message/command/move_.rs new file mode 100644 index 0000000..69b19ac --- /dev/null +++ b/src/email/message/command/move_.rs @@ -0,0 +1,52 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + envelope::arg::ids::EnvelopeIdsArgs, + folder::arg::name::{SourceFolderNameArg, TargetFolderNameArg}, + printer::Printer, +}; + +/// Move a message from a source folder to a target folder +#[derive(Debug, Parser)] +pub struct MessageMoveCommand { + #[command(flatten)] + pub source_folder: SourceFolderNameArg, + + #[command(flatten)] + pub target_folder: TargetFolderNameArg, + + #[command(flatten)] + pub envelopes: EnvelopeIdsArgs, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageMoveCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message move command"); + + let from_folder = &self.source_folder.name; + let to_folder = &self.target_folder.name; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + let ids = &self.envelopes.ids; + backend.move_messages(from_folder, to_folder, ids).await?; + + printer.print("Message(s) successfully moved from {from_folder} to {to_folder}!") + } +} diff --git a/src/email/message/command/read.rs b/src/email/message/command/read.rs new file mode 100644 index 0000000..373547a --- /dev/null +++ b/src/email/message/command/read.rs @@ -0,0 +1,117 @@ +use anyhow::Result; +use clap::Parser; +use log::info; +use mml::message::FilterParts; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameArg, + printer::Printer, +}; + +/// Read a message from a folder +#[derive(Debug, Parser)] +pub struct MessageReadCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub envelopes: EnvelopeIdsArgs, + + /// Read the raw version of the message + /// + /// The raw message represents the message as it is on the + /// backend, unedited: not decoded nor decrypted. This is useful + /// for debugging faulty messages, but also for + /// saving/sending/transfering messages. + #[arg(long, short)] + #[arg(conflicts_with = "no_headers")] + #[arg(conflicts_with = "headers")] + pub raw: bool, + + /// Read only body of text/html parts + /// + /// This argument is useful when you need to read the HTML version + /// of a message. Combined with --no-headers, you can write it to + /// a .html file and open it with your favourite browser. + #[arg(long)] + #[arg(conflicts_with = "raw")] + pub html: bool, + + /// Read only the body of the message + /// + /// All headers will be removed from the message. + #[arg(long)] + #[arg(conflicts_with = "raw")] + #[arg(conflicts_with = "headers")] + pub no_headers: bool, + + /// List of headers that should be visible at the top of the + /// message + /// + /// If a given header is not found in the message, it will not be + /// visible. If no header is given, defaults to the one set up in + /// your TOML configuration file. + #[arg(long = "header", short = 'H', value_name = "NAME")] + #[arg(conflicts_with = "raw")] + #[arg(conflicts_with = "no_headers")] + pub headers: Vec, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageReadCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message read command"); + + let folder = &self.folder.name; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + let ids = &self.envelopes.ids; + let emails = backend.get_messages(&folder, &ids).await?; + + let mut glue = ""; + let mut bodies = String::default(); + + for email in emails.to_vec() { + bodies.push_str(glue); + + if self.raw { + // emails do not always have valid utf8, uses "lossy" to + // display what can be displayed + bodies.push_str(&String::from_utf8_lossy(email.raw()?).into_owned()); + } else { + let tpl: String = email + .to_read_tpl(&account_config, |mut tpl| { + if self.no_headers { + tpl = tpl.with_hide_all_headers(); + } else if !self.headers.is_empty() { + tpl = tpl.with_show_only_headers(&self.headers); + } + + if self.html { + tpl = tpl.with_filter_parts(FilterParts::Only("text/html".into())); + } + + tpl + }) + .await? + .into(); + bodies.push_str(&tpl); + } + + glue = "\n\n"; + } + + printer.print(bodies) + } +} diff --git a/src/email/message/command/save.rs b/src/email/message/command/save.rs new file mode 100644 index 0000000..0fca634 --- /dev/null +++ b/src/email/message/command/save.rs @@ -0,0 +1,61 @@ +use anyhow::Result; +use atty::Stream; +use clap::Parser; +use log::info; +use std::io::{self, BufRead}; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer, +}; + +/// Save a message to a folder +#[derive(Debug, Parser)] +pub struct MessageSaveCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + /// The raw message to save + #[arg(value_name = "MESSAGE", raw = true)] + pub raw: String, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageSaveCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message save command"); + + let folder = &self.folder.name; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + let raw_msg = &self.raw; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + + let is_tty = atty::is(Stream::Stdin); + let is_json = printer.is_json(); + let raw_email = if is_tty || is_json { + raw_msg.replace("\r", "").replace("\n", "\r\n") + } else { + io::stdin() + .lock() + .lines() + .filter_map(Result::ok) + .collect::>() + .join("\r\n") + }; + + backend + .add_raw_message(folder, raw_email.as_bytes()) + .await?; + + printer.print("Message successfully saved to {folder}!") + } +} diff --git a/src/email/message/command/send.rs b/src/email/message/command/send.rs new file mode 100644 index 0000000..f8563eb --- /dev/null +++ b/src/email/message/command/send.rs @@ -0,0 +1,65 @@ +use anyhow::Result; +use atty::Stream; +use clap::Parser; +use email::flag::Flag; +use log::info; +use std::io::{self, BufRead}; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, printer::Printer, +}; + +/// Send a message from a folder +#[derive(Debug, Parser)] +pub struct MessageSendCommand { + /// The raw message to send + #[arg(value_name = "MESSAGE", raw = true)] + pub raw: String, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageSendCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message send command"); + + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + let raw_msg = &self.raw; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + let folder = account_config.sent_folder_alias()?; + + let is_tty = atty::is(Stream::Stdin); + let is_json = printer.is_json(); + let raw_email = if is_tty || is_json { + raw_msg.replace("\r", "").replace("\n", "\r\n") + } else { + io::stdin() + .lock() + .lines() + .filter_map(Result::ok) + .collect::>() + .join("\r\n") + }; + + backend.send_raw_message(raw_email.as_bytes()).await?; + + if account_config.email_sending_save_copy.unwrap_or_default() { + backend + .add_raw_message_with_flag(&folder, raw_email.as_bytes(), Flag::Seen) + .await?; + + printer.print("Message successfully sent and saved to {folder}!") + } else { + printer.print("Message successfully sent!") + } + } +} diff --git a/src/email/message/handlers.rs b/src/email/message/handlers.rs index 01fa6f2..0918e9f 100644 --- a/src/email/message/handlers.rs +++ b/src/email/message/handlers.rs @@ -69,29 +69,6 @@ pub async fn attachments( } } -pub async fn copy( - printer: &mut P, - backend: &Backend, - from_folder: &str, - to_folder: &str, - ids: Vec<&str>, -) -> Result<()> { - backend - .copy_messages(&from_folder, &to_folder, &ids) - .await?; - printer.print("Email(s) successfully copied!") -} - -pub async fn delete( - printer: &mut P, - backend: &Backend, - folder: &str, - ids: Vec<&str>, -) -> Result<()> { - backend.delete_messages(&folder, &ids).await?; - printer.print("Email(s) successfully deleted!") -} - pub async fn forward( config: &AccountConfig, printer: &mut P, @@ -147,60 +124,6 @@ pub async fn mailto( editor::edit_tpl_with_editor(config, printer, backend, tpl).await } -pub async fn move_( - printer: &mut P, - backend: &Backend, - from_folder: &str, - to_folder: &str, - ids: Vec<&str>, -) -> Result<()> { - backend - .move_messages(&from_folder, &to_folder, &ids) - .await?; - printer.print("Email(s) successfully moved!") -} - -pub async fn read( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - folder: &str, - ids: Vec<&str>, - text_mime: &str, - raw: bool, - headers: Vec<&str>, -) -> Result<()> { - let emails = backend.get_messages(&folder, &ids).await?; - - let mut glue = ""; - let mut bodies = String::default(); - - for email in emails.to_vec() { - bodies.push_str(glue); - - if raw { - // emails do not always have valid utf8, uses "lossy" to - // display what can be displayed - bodies.push_str(&String::from_utf8_lossy(email.raw()?).into_owned()); - } else { - let tpl: String = email - .to_read_tpl(&config, |tpl| match text_mime { - "html" => tpl - .with_hide_all_headers() - .with_filter_parts(FilterParts::Only("text/html".into())), - _ => tpl.with_show_additional_headers(&headers), - }) - .await? - .into(); - bodies.push_str(&tpl); - } - - glue = "\n\n"; - } - - printer.print(bodies) -} - pub async fn reply( config: &AccountConfig, printer: &mut P, @@ -230,61 +153,6 @@ pub async fn reply( Ok(()) } -pub async fn save( - printer: &mut P, - backend: &Backend, - folder: &str, - raw_email: String, -) -> Result<()> { - let is_tty = atty::is(Stream::Stdin); - let is_json = printer.is_json(); - let raw_email = if is_tty || is_json { - raw_email.replace("\r", "").replace("\n", "\r\n") - } else { - io::stdin() - .lock() - .lines() - .filter_map(Result::ok) - .collect::>() - .join("\r\n") - }; - - backend - .add_raw_message(&folder, raw_email.as_bytes()) - .await?; - - Ok(()) -} - -pub async fn send( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - raw_email: String, -) -> Result<()> { - let folder = config.sent_folder_alias()?; - let is_tty = atty::is(Stream::Stdin); - let is_json = printer.is_json(); - let raw_email = if is_tty || is_json { - raw_email.replace("\r", "").replace("\n", "\r\n") - } else { - io::stdin() - .lock() - .lines() - .filter_map(Result::ok) - .collect::>() - .join("\r\n") - }; - trace!("raw email: {:?}", raw_email); - backend.send_raw_message(raw_email.as_bytes()).await?; - if config.email_sending_save_copy.unwrap_or_default() { - backend - .add_raw_message_with_flag(&folder, raw_email.as_bytes(), Flag::Seen) - .await?; - } - Ok(()) -} - pub async fn write( config: &AccountConfig, printer: &mut P, diff --git a/src/email/message/mod.rs b/src/email/message/mod.rs index c101d77..b0fb56e 100644 --- a/src/email/message/mod.rs +++ b/src/email/message/mod.rs @@ -1,4 +1,5 @@ -pub mod args; +// pub mod args; +pub mod command; pub mod config; -pub mod handlers; -pub mod template; +// pub mod handlers; +// pub mod template; diff --git a/src/email/mod.rs b/src/email/mod.rs index 7eae3d8..0e144c4 100644 --- a/src/email/mod.rs +++ b/src/email/mod.rs @@ -2,4 +2,5 @@ pub mod envelope; pub mod message; #[doc(inline)] -pub use self::{envelope::flag, message::template}; +// pub use self::{envelope::flag, message::template}; +pub use self::envelope::flag; diff --git a/src/folder/arg/name.rs b/src/folder/arg/name.rs index 9004fec..730c1a3 100644 --- a/src/folder/arg/name.rs +++ b/src/folder/arg/name.rs @@ -16,3 +16,19 @@ pub struct FolderNameOptionalArg { #[arg(name = "folder-name", value_name = "FOLDER", default_value = DEFAULT_INBOX_FOLDER)] pub name: String, } + +/// The source folder name argument parser +#[derive(Debug, Parser)] +pub struct SourceFolderNameArg { + /// The name of the source folder + #[arg(name = "from-folder-name", value_name = "FROM")] + pub name: String, +} + +/// The target folder name argument parser +#[derive(Debug, Parser)] +pub struct TargetFolderNameArg { + /// The name of the target folder + #[arg(name = "to-folder-name", value_name = "TO")] + pub name: String, +} diff --git a/src/lib.rs b/src/lib.rs index 0cb7c67..eea4ee3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,4 +22,5 @@ pub mod smtp; pub mod ui; #[doc(inline)] -pub use email::{envelope, flag, message, template}; +// pub use email::{envelope, flag, message, template}; +pub use email::{envelope, flag, message}; diff --git a/src/main.rs b/src/main.rs index cbba050..bac06bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,16 +61,6 @@ async fn main() -> Result<()> { // ) // .await; // } -// Some(message::args::Cmd::Copy(ids, to_folder)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return message::handlers::copy(&mut printer, &backend, &folder, to_folder, ids).await; -// } -// Some(message::args::Cmd::Delete(ids)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return message::handlers::delete(&mut printer, &backend, &folder, ids).await; -// } // Some(message::args::Cmd::Forward(id, headers, body)) => { // let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); // let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; @@ -85,26 +75,6 @@ async fn main() -> Result<()> { // ) // .await; // } -// Some(message::args::Cmd::Move(ids, to_folder)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return message::handlers::move_(&mut printer, &backend, &folder, to_folder, ids).await; -// } -// Some(message::args::Cmd::Read(ids, text_mime, raw, headers)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return message::handlers::read( -// &account_config, -// &mut printer, -// &backend, -// &folder, -// ids, -// text_mime, -// raw, -// headers, -// ) -// .await; -// } // Some(message::args::Cmd::Reply(id, all, headers, body)) => { // let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); // let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; From b8ef771614bc284b9b4a3bb448cc6cf4dbe64851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Thu, 7 Dec 2023 18:50:46 +0100 Subject: [PATCH 21/29] refactor message with clap derive api (part 2) --- src/backend/mod.rs | 23 +++++++- src/cli.rs | 10 ++-- src/email/envelope/arg/ids.rs | 8 +++ src/email/message/arg/body.rs | 26 +++++++++ src/email/message/arg/header.rs | 20 +++++++ src/email/message/arg/mod.rs | 2 + src/email/message/command/copy.rs | 4 +- src/email/message/command/forward.rs | 83 +++++++++++++++++++++++++++ src/email/message/command/mailto.rs | 46 +++++++++++++++ src/email/message/command/mod.rs | 30 +++++++++- src/email/message/command/move_.rs | 4 +- src/email/message/command/reply.rs | 86 ++++++++++++++++++++++++++++ src/email/message/command/save.rs | 2 +- src/email/message/command/send.rs | 2 +- src/email/message/command/write.rs | 66 +++++++++++++++++++++ src/email/message/handlers.rs | 70 ---------------------- src/email/message/mod.rs | 1 + src/main.rs | 62 ++++---------------- 18 files changed, 413 insertions(+), 132 deletions(-) create mode 100644 src/email/message/arg/body.rs create mode 100644 src/email/message/arg/header.rs create mode 100644 src/email/message/arg/mod.rs create mode 100644 src/email/message/command/forward.rs create mode 100644 src/email/message/command/mailto.rs create mode 100644 src/email/message/command/reply.rs create mode 100644 src/email/message/command/write.rs diff --git a/src/backend/mod.rs b/src/backend/mod.rs index b659b8f..cae0b9b 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -20,7 +20,7 @@ use email::{ add::{imap::AddFlagsImap, maildir::AddFlagsMaildir}, remove::{imap::RemoveFlagsImap, maildir::RemoveFlagsMaildir}, set::{imap::SetFlagsImap, maildir::SetFlagsMaildir}, - Flags, + Flag, Flags, }, folder::{ add::{imap::AddFolderImap, maildir::AddFolderMaildir}, @@ -800,6 +800,27 @@ impl Backend { id_mapper.create_alias(&*id)?; Ok(id) } + + pub async fn add_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> { + let backend_kind = self.toml_account_config.add_flags_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.add_flag(folder, &ids, flag).await + } + + pub async fn set_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> { + let backend_kind = self.toml_account_config.set_flags_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.set_flag(folder, &ids, flag).await + } + + pub async fn remove_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> { + let backend_kind = self.toml_account_config.remove_flags_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.remove_flag(folder, &ids, flag).await + } } impl Deref for Backend { diff --git a/src/cli.rs b/src/cli.rs index 4ae7964..1f0826d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -80,27 +80,27 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum HimalayaCommand { - /// Subcommand to manage accounts + /// Manage accounts #[command(subcommand)] #[command(alias = "accounts")] Account(AccountSubcommand), - /// Subcommand to manage folders + /// Manage folders #[command(subcommand)] #[command(alias = "folders")] Folder(FolderSubcommand), - /// Subcommand to manage envelopes + /// Manage envelopes #[command(subcommand)] #[command(alias = "envelopes")] Envelope(EnvelopeSubcommand), - /// Subcommand to manage flags + /// Manage flags #[command(subcommand)] #[command(alias = "flags")] Flag(FlagSubcommand), - /// Subcommand to manage messages + /// Manage messages #[command(subcommand)] #[command(alias = "messages", alias = "msgs", alias = "msg")] Message(MessageSubcommand), diff --git a/src/email/envelope/arg/ids.rs b/src/email/envelope/arg/ids.rs index 8fee631..6a29438 100644 --- a/src/email/envelope/arg/ids.rs +++ b/src/email/envelope/arg/ids.rs @@ -1,5 +1,13 @@ use clap::Parser; +/// The envelope id argument parser +#[derive(Debug, Parser)] +pub struct EnvelopeIdArg { + /// The envelope id + #[arg(value_name = "ID", required = true)] + pub id: usize, +} + /// The envelopes ids arguments parser #[derive(Debug, Parser)] pub struct EnvelopeIdsArgs { diff --git a/src/email/message/arg/body.rs b/src/email/message/arg/body.rs new file mode 100644 index 0000000..b06446d --- /dev/null +++ b/src/email/message/arg/body.rs @@ -0,0 +1,26 @@ +use std::ops::Deref; + +use clap::Parser; + +/// The raw message body argument parser +#[derive(Debug, Parser)] +pub struct BodyRawArg { + /// Prefill the template with a custom body + #[arg(raw = true, required = false)] + #[arg(name = "body-raw", value_delimiter = ' ')] + pub raw: Vec, +} + +impl BodyRawArg { + pub fn raw(self) -> String { + self.raw.join(" ").replace("\r", "").replace("\n", "\r\n") + } +} + +impl Deref for BodyRawArg { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.raw + } +} diff --git a/src/email/message/arg/header.rs b/src/email/message/arg/header.rs new file mode 100644 index 0000000..0f4b34d --- /dev/null +++ b/src/email/message/arg/header.rs @@ -0,0 +1,20 @@ +use clap::Parser; + +/// The envelope id argument parser +#[derive(Debug, Parser)] +pub struct HeaderRawArgs { + /// Prefill the template with custom headers + /// + /// A raw header should follow the pattern KEY:VAL. + #[arg(long = "header", short = 'H', required = false)] + #[arg(name = "header-raw", value_name = "KEY:VAL", value_parser = raw_header_parser)] + pub raw: Vec<(String, String)>, +} + +pub fn raw_header_parser(raw_header: &str) -> Result<(String, String), String> { + if let Some((key, val)) = raw_header.split_once(":") { + Ok((key.trim().to_owned(), val.trim().to_owned())) + } else { + Err(format!("cannot parse raw header {raw_header:?}")) + } +} diff --git a/src/email/message/arg/mod.rs b/src/email/message/arg/mod.rs new file mode 100644 index 0000000..53bf2e6 --- /dev/null +++ b/src/email/message/arg/mod.rs @@ -0,0 +1,2 @@ +pub mod body; +pub mod header; diff --git a/src/email/message/command/copy.rs b/src/email/message/command/copy.rs index d99e4c2..07d4b00 100644 --- a/src/email/message/command/copy.rs +++ b/src/email/message/command/copy.rs @@ -47,6 +47,8 @@ impl MessageCopyCommand { let ids = &self.envelopes.ids; backend.copy_messages(from_folder, to_folder, ids).await?; - printer.print("Message(s) successfully copied from {from_folder} to {to_folder}!") + printer.print(format!( + "Message(s) successfully copied from {from_folder} to {to_folder}!" + )) } } diff --git a/src/email/message/command/forward.rs b/src/email/message/command/forward.rs new file mode 100644 index 0000000..f6199cc --- /dev/null +++ b/src/email/message/command/forward.rs @@ -0,0 +1,83 @@ +use anyhow::{anyhow, Result}; +use atty::Stream; +use clap::Parser; +use log::info; +use std::io::{self, BufRead}; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + envelope::arg::ids::EnvelopeIdArg, + folder::arg::name::FolderNameArg, + message::arg::{body::BodyRawArg, header::HeaderRawArgs}, + printer::Printer, + ui::editor, +}; + +/// Forward a new message +#[derive(Debug, Parser)] +pub struct MessageForwardCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub envelope: EnvelopeIdArg, + + /// Forward to all recipients + #[arg(long, short = 'A')] + pub all: bool, + + #[command(flatten)] + pub headers: HeaderRawArgs, + + #[command(flatten)] + pub body: BodyRawArg, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageForwardCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message forward command"); + + let folder = &self.folder.name; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + + let is_tty = atty::is(Stream::Stdin); + let is_json = printer.is_json(); + let body = if !self.body.is_empty() && (is_tty || is_json) { + self.body.raw() + } else { + io::stdin() + .lock() + .lines() + .filter_map(Result::ok) + .collect::>() + .join("\r\n") + }; + + let id = self.envelope.id; + let tpl = backend + .get_messages(folder, &[id]) + .await? + .first() + .ok_or(anyhow!("cannot find message"))? + .to_forward_tpl_builder(&account_config) + .with_headers(self.headers.raw) + .with_body(body) + .build() + .await?; + editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await + } +} diff --git a/src/email/message/command/mailto.rs b/src/email/message/command/mailto.rs new file mode 100644 index 0000000..4041317 --- /dev/null +++ b/src/email/message/command/mailto.rs @@ -0,0 +1,46 @@ +use anyhow::Result; +use clap::Parser; +use log::info; +use mail_builder::MessageBuilder; +use url::Url; + +use crate::{backend::Backend, config::TomlConfig, printer::Printer, ui::editor}; + +/// Parse and edit a message from a mailto URL string +#[derive(Debug, Parser)] +pub struct MessageMailtoCommand { + /// The mailto url + #[arg()] + pub url: Url, +} + +impl MessageMailtoCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message mailto command"); + + let (toml_account_config, account_config) = + config.clone().into_account_configs(None, false)?; + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + + let mut builder = MessageBuilder::new().to(self.url.path()); + + for (key, val) in self.url.query_pairs() { + match key.to_lowercase().as_bytes() { + b"cc" => builder = builder.cc(val.to_string()), + b"bcc" => builder = builder.bcc(val.to_string()), + b"subject" => builder = builder.subject(val), + b"body" => builder = builder.text_body(val), + _ => (), + } + } + + let tpl = account_config + .generate_tpl_interpreter() + .with_show_only_headers(account_config.email_writing_headers()) + .build() + .from_msg_builder(builder) + .await?; + + editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await + } +} diff --git a/src/email/message/command/mod.rs b/src/email/message/command/mod.rs index 903c88a..dc2c5d5 100644 --- a/src/email/message/command/mod.rs +++ b/src/email/message/command/mod.rs @@ -1,9 +1,13 @@ pub mod copy; pub mod delete; +pub mod forward; +pub mod mailto; pub mod move_; pub mod read; +pub mod reply; pub mod save; pub mod send; +pub mod write; use anyhow::Result; use clap::Subcommand; @@ -11,8 +15,10 @@ use clap::Subcommand; use crate::{config::TomlConfig, printer::Printer}; use self::{ - copy::MessageCopyCommand, delete::MessageDeleteCommand, move_::MessageMoveCommand, - read::MessageReadCommand, save::MessageSaveCommand, send::MessageSendCommand, + copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand, + mailto::MessageMailtoCommand, move_::MessageMoveCommand, read::MessageReadCommand, + reply::MessageReplyCommand, save::MessageSaveCommand, send::MessageSendCommand, + write::MessageWriteCommand, }; /// Subcommand to manage messages @@ -22,6 +28,22 @@ pub enum MessageSubcommand { #[command(arg_required_else_help = true)] Read(MessageReadCommand), + /// Write a new message + #[command(alias = "new", alias = "compose")] + Write(MessageWriteCommand), + + /// Reply to a message + #[command()] + Reply(MessageReplyCommand), + + /// Forward a message + #[command(alias = "fwd")] + Forward(MessageForwardCommand), + + /// Parse and edit a message from a mailto URL string + #[command()] + Mailto(MessageMailtoCommand), + /// Save a message to a folder #[command(arg_required_else_help = true)] #[command(alias = "add", alias = "create")] @@ -48,6 +70,10 @@ impl MessageSubcommand { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { match self { Self::Read(cmd) => cmd.execute(printer, config).await, + Self::Write(cmd) => cmd.execute(printer, config).await, + Self::Reply(cmd) => cmd.execute(printer, config).await, + Self::Forward(cmd) => cmd.execute(printer, config).await, + Self::Mailto(cmd) => cmd.execute(printer, config).await, Self::Save(cmd) => cmd.execute(printer, config).await, Self::Send(cmd) => cmd.execute(printer, config).await, Self::Copy(cmd) => cmd.execute(printer, config).await, diff --git a/src/email/message/command/move_.rs b/src/email/message/command/move_.rs index 69b19ac..76d2744 100644 --- a/src/email/message/command/move_.rs +++ b/src/email/message/command/move_.rs @@ -47,6 +47,8 @@ impl MessageMoveCommand { let ids = &self.envelopes.ids; backend.move_messages(from_folder, to_folder, ids).await?; - printer.print("Message(s) successfully moved from {from_folder} to {to_folder}!") + printer.print(format!( + "Message(s) successfully moved from {from_folder} to {to_folder}!" + )) } } diff --git a/src/email/message/command/reply.rs b/src/email/message/command/reply.rs new file mode 100644 index 0000000..d9cff8b --- /dev/null +++ b/src/email/message/command/reply.rs @@ -0,0 +1,86 @@ +use anyhow::{anyhow, Result}; +use atty::Stream; +use clap::Parser; +use email::flag::Flag; +use log::info; +use std::io::{self, BufRead}; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + envelope::arg::ids::EnvelopeIdArg, + folder::arg::name::FolderNameArg, + message::arg::{body::BodyRawArg, header::HeaderRawArgs}, + printer::Printer, + ui::editor, +}; + +/// Reply a new message +#[derive(Debug, Parser)] +pub struct MessageReplyCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub envelope: EnvelopeIdArg, + + /// Reply to all recipients + #[arg(long, short = 'A')] + pub all: bool, + + #[command(flatten)] + pub headers: HeaderRawArgs, + + #[command(flatten)] + pub body: BodyRawArg, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageReplyCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message reply command"); + + let folder = &self.folder.name; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + + let is_tty = atty::is(Stream::Stdin); + let is_json = printer.is_json(); + let body = if !self.body.is_empty() && (is_tty || is_json) { + self.body.raw() + } else { + io::stdin() + .lock() + .lines() + .filter_map(Result::ok) + .collect::>() + .join("\r\n") + }; + + let id = self.envelope.id; + let tpl = backend + .get_messages(folder, &[id]) + .await? + .first() + .ok_or(anyhow!("cannot find message"))? + .to_reply_tpl_builder(&account_config) + .with_headers(self.headers.raw) + .with_body(body) + .with_reply_all(self.all) + .build() + .await?; + editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await?; + backend.add_flag(&folder, &[id], Flag::Answered).await + } +} diff --git a/src/email/message/command/save.rs b/src/email/message/command/save.rs index 0fca634..afd3813 100644 --- a/src/email/message/command/save.rs +++ b/src/email/message/command/save.rs @@ -56,6 +56,6 @@ impl MessageSaveCommand { .add_raw_message(folder, raw_email.as_bytes()) .await?; - printer.print("Message successfully saved to {folder}!") + printer.print(format!("Message successfully saved to {folder}!")) } } diff --git a/src/email/message/command/send.rs b/src/email/message/command/send.rs index f8563eb..d83e6e5 100644 --- a/src/email/message/command/send.rs +++ b/src/email/message/command/send.rs @@ -57,7 +57,7 @@ impl MessageSendCommand { .add_raw_message_with_flag(&folder, raw_email.as_bytes(), Flag::Seen) .await?; - printer.print("Message successfully sent and saved to {folder}!") + printer.print(format!("Message successfully sent and saved to {folder}!")) } else { printer.print("Message successfully sent!") } diff --git a/src/email/message/command/write.rs b/src/email/message/command/write.rs new file mode 100644 index 0000000..c0d5aca --- /dev/null +++ b/src/email/message/command/write.rs @@ -0,0 +1,66 @@ +use anyhow::Result; +use atty::Stream; +use clap::Parser; +use email::message::Message; +use log::info; +use std::io::{self, BufRead}; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + message::arg::{body::BodyRawArg, header::HeaderRawArgs}, + printer::Printer, + ui::editor, +}; + +/// Write a new message +#[derive(Debug, Parser)] +pub struct MessageWriteCommand { + #[command(flatten)] + pub headers: HeaderRawArgs, + + #[command(flatten)] + pub body: BodyRawArg, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageWriteCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message write command"); + + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + + let is_tty = atty::is(Stream::Stdin); + let is_json = printer.is_json(); + let body = if !self.body.is_empty() && (is_tty || is_json) { + self.body.raw() + } else { + io::stdin() + .lock() + .lines() + .filter_map(Result::ok) + .collect::>() + .join("\r\n") + }; + + let tpl = Message::new_tpl_builder(&account_config) + .with_headers(self.headers.raw) + .with_body(body) + .build() + .await?; + + editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await + } +} diff --git a/src/email/message/handlers.rs b/src/email/message/handlers.rs index 0918e9f..72a99f9 100644 --- a/src/email/message/handlers.rs +++ b/src/email/message/handlers.rs @@ -69,30 +69,6 @@ pub async fn attachments( } } -pub async fn forward( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - folder: &str, - id: &str, - headers: Option>, - body: Option<&str>, -) -> Result<()> { - let tpl = backend - .get_messages(&folder, &[id]) - .await? - .first() - .ok_or_else(|| anyhow!("cannot find email {}", id))? - .to_forward_tpl_builder(config) - .with_some_headers(headers) - .with_some_body(body) - .build() - .await?; - trace!("initial template: {tpl}"); - editor::edit_tpl_with_editor(config, printer, backend, tpl).await?; - Ok(()) -} - /// Parses and edits a message from a [mailto] URL string. /// /// [mailto]: https://en.wikipedia.org/wiki/Mailto @@ -123,49 +99,3 @@ pub async fn mailto( editor::edit_tpl_with_editor(config, printer, backend, tpl).await } - -pub async fn reply( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - folder: &str, - id: &str, - all: bool, - headers: Option>, - body: Option<&str>, -) -> Result<()> { - let tpl = backend - .get_messages(folder, &[id]) - .await? - .first() - .ok_or_else(|| anyhow!("cannot find email {}", id))? - .to_reply_tpl_builder(config) - .with_some_headers(headers) - .with_some_body(body) - .with_reply_all(all) - .build() - .await?; - trace!("initial template: {tpl}"); - editor::edit_tpl_with_editor(config, printer, backend, tpl).await?; - backend - .add_flag(&folder, &Id::single(id), Flag::Answered) - .await?; - Ok(()) -} - -pub async fn write( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - headers: Option>, - body: Option<&str>, -) -> Result<()> { - let tpl = Message::new_tpl_builder(config) - .with_some_headers(headers) - .with_some_body(body) - .build() - .await?; - trace!("initial template: {tpl}"); - editor::edit_tpl_with_editor(config, printer, backend, tpl).await?; - Ok(()) -} diff --git a/src/email/message/mod.rs b/src/email/message/mod.rs index b0fb56e..754da7b 100644 --- a/src/email/message/mod.rs +++ b/src/email/message/mod.rs @@ -1,4 +1,5 @@ // pub mod args; +pub mod arg; pub mod command; pub mod config; // pub mod handlers; diff --git a/src/main.rs b/src/main.rs index bac06bf..611770d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use clap::Parser; use env_logger::{Builder as LoggerBuilder, Env, DEFAULT_FILTER_ENV}; use himalaya::{cli::Cli, config::TomlConfig, printer::StdoutPrinter}; use log::{debug, warn}; +use std::env; #[tokio::main] async fn main() -> Result<()> { @@ -17,6 +18,17 @@ async fn main() -> Result<()> { .format_timestamp(None) .init(); + let raw_args: Vec = env::args().collect(); + if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { + // TODO + // let cmd = MessageMailtoCommand::command() + // .no_binary_name(true) + // .try_get_matches_from([&raw_args[1]]); + // match cmd { + // Ok(m) => m.exec + // } + } + let cli = Cli::parse(); let mut printer = StdoutPrinter::new(cli.output, cli.color); @@ -61,56 +73,6 @@ async fn main() -> Result<()> { // ) // .await; // } -// Some(message::args::Cmd::Forward(id, headers, body)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; -// return message::handlers::forward( -// &account_config, -// &mut printer, -// &backend, -// &folder, -// id, -// headers, -// body, -// ) -// .await; -// } -// Some(message::args::Cmd::Reply(id, all, headers, body)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; -// return message::handlers::reply( -// &account_config, -// &mut printer, -// &backend, -// &folder, -// id, -// all, -// headers, -// body, -// ) -// .await; -// } -// Some(message::args::Cmd::Save(raw_email)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return message::handlers::save(&mut printer, &backend, &folder, raw_email).await; -// } -// Some(message::args::Cmd::Send(raw_email)) => { -// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; -// return message::handlers::send(&account_config, &mut printer, &backend, raw_email) -// .await; -// } -// Some(message::args::Cmd::Write(headers, body)) => { -// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; -// return message::handlers::write( -// &account_config, -// &mut printer, -// &backend, -// headers, -// body, -// ) -// .await; -// } // _ => (), // } From b28f12c36756f73a5d1df80ca25cdd186fd99b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Thu, 7 Dec 2023 21:59:12 +0100 Subject: [PATCH 22/29] refactor attachment with clap derive api --- src/cli.rs | 7 +- src/email/message/args.rs | 356 ------------------ .../message/attachment/command/download.rs | 85 +++++ src/email/message/attachment/command/mod.rs | 24 ++ src/email/message/attachment/mod.rs | 1 + src/email/message/handlers.rs | 101 ----- src/email/message/mod.rs | 3 +- 7 files changed, 117 insertions(+), 460 deletions(-) delete mode 100644 src/email/message/args.rs create mode 100644 src/email/message/attachment/command/download.rs create mode 100644 src/email/message/attachment/command/mod.rs create mode 100644 src/email/message/attachment/mod.rs delete mode 100644 src/email/message/handlers.rs diff --git a/src/cli.rs b/src/cli.rs index 1f0826d..a1c8b7a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,7 +10,7 @@ use crate::{ flag::command::FlagSubcommand, folder::command::FolderSubcommand, manual::command::ManualGenerateCommand, - message::command::MessageSubcommand, + message::{attachment::command::AttachmentSubcommand, command::MessageSubcommand}, output::{ColorFmt, OutputFmt}, printer::Printer, }; @@ -105,6 +105,10 @@ pub enum HimalayaCommand { #[command(alias = "messages", alias = "msgs", alias = "msg")] Message(MessageSubcommand), + /// Manage attachments + #[command(subcommand)] + Attachment(AttachmentSubcommand), + /// Generate manual pages to a directory #[command(arg_required_else_help = true)] #[command(alias = "manuals", alias = "mans")] @@ -124,6 +128,7 @@ impl HimalayaCommand { Self::Envelope(cmd) => cmd.execute(printer, config).await, Self::Flag(cmd) => cmd.execute(printer, config).await, Self::Message(cmd) => cmd.execute(printer, config).await, + Self::Attachment(cmd) => cmd.execute(printer, config).await, Self::Manual(cmd) => cmd.execute(printer).await, Self::Completion(cmd) => cmd.execute(printer).await, } diff --git a/src/email/message/args.rs b/src/email/message/args.rs deleted file mode 100644 index 80dbb43..0000000 --- a/src/email/message/args.rs +++ /dev/null @@ -1,356 +0,0 @@ -//! Email CLI module. -//! -//! This module contains the command matcher, the subcommands and the -//! arguments related to the email domain. - -use anyhow::Result; -use clap::{Arg, ArgAction, ArgMatches, Command}; - -use crate::template; - -const ARG_CRITERIA: &str = "criterion"; -const ARG_HEADERS: &str = "headers"; -const ARG_ID: &str = "id"; -const ARG_IDS: &str = "ids"; -const ARG_MIME_TYPE: &str = "mime-type"; -const ARG_QUERY: &str = "query"; -const ARG_RAW: &str = "raw"; -const ARG_REPLY_ALL: &str = "reply-all"; -const CMD_ATTACHMENTS: &str = "attachments"; -const CMD_COPY: &str = "copy"; -const CMD_DELETE: &str = "delete"; -const CMD_FORWARD: &str = "forward"; -const CMD_MESSAGE: &str = "message"; -const CMD_MOVE: &str = "move"; -const CMD_READ: &str = "read"; -const CMD_REPLY: &str = "reply"; -const CMD_SAVE: &str = "save"; -const CMD_SEND: &str = "send"; -const CMD_WRITE: &str = "write"; - -pub type All = bool; -pub type Criteria = String; -pub type Folder<'a> = &'a str; -pub type Headers<'a> = Vec<&'a str>; -pub type Id<'a> = &'a str; -pub type Ids<'a> = Vec<&'a str>; -pub type Query = String; -pub type Raw = bool; -pub type RawEmail = String; -pub type TextMime<'a> = &'a str; - -/// Represents the email commands. -#[derive(Debug, PartialEq, Eq)] -pub enum Cmd<'a> { - Attachments(Ids<'a>), - Copy(Ids<'a>, Folder<'a>), - Delete(Ids<'a>), - Forward( - Id<'a>, - template::args::Headers<'a>, - template::args::Body<'a>, - ), - Move(Ids<'a>, Folder<'a>), - Read(Ids<'a>, TextMime<'a>, Raw, Headers<'a>), - Reply( - Id<'a>, - All, - template::args::Headers<'a>, - template::args::Body<'a>, - ), - Save(RawEmail), - Send(RawEmail), - Write(template::args::Headers<'a>, template::args::Body<'a>), -} - -/// Email command matcher. -pub fn matches(m: &ArgMatches) -> Result> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_MESSAGE) { - if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) { - let ids = parse_ids_arg(m); - Some(Cmd::Attachments(ids)) - } else if let Some(m) = m.subcommand_matches(CMD_COPY) { - let ids = parse_ids_arg(m); - let folder = "INBOX"; - Some(Cmd::Copy(ids, folder)) - } else if let Some(m) = m.subcommand_matches(CMD_DELETE) { - let ids = parse_ids_arg(m); - Some(Cmd::Delete(ids)) - } else if let Some(m) = m.subcommand_matches(CMD_FORWARD) { - let id = parse_id_arg(m); - let headers = template::args::parse_headers_arg(m); - let body = template::args::parse_body_arg(m); - Some(Cmd::Forward(id, headers, body)) - } else if let Some(m) = m.subcommand_matches(CMD_MOVE) { - let ids = parse_ids_arg(m); - let folder = "INBOX"; - Some(Cmd::Move(ids, folder)) - } else if let Some(m) = m.subcommand_matches(CMD_READ) { - let ids = parse_ids_arg(m); - let mime = parse_mime_type_arg(m); - let raw = parse_raw_flag(m); - let headers = parse_headers_arg(m); - Some(Cmd::Read(ids, mime, raw, headers)) - } else if let Some(m) = m.subcommand_matches(CMD_REPLY) { - let id = parse_id_arg(m); - let all = parse_reply_all_flag(m); - let headers = template::args::parse_headers_arg(m); - let body = template::args::parse_body_arg(m); - Some(Cmd::Reply(id, all, headers, body)) - } else if let Some(m) = m.subcommand_matches(CMD_SAVE) { - let email = parse_raw_arg(m); - Some(Cmd::Save(email)) - } else if let Some(m) = m.subcommand_matches(CMD_SEND) { - let email = parse_raw_arg(m); - Some(Cmd::Send(email)) - } else if let Some(m) = m.subcommand_matches(CMD_WRITE) { - let headers = template::args::parse_headers_arg(m); - let body = template::args::parse_body_arg(m); - Some(Cmd::Write(headers, body)) - } else { - None - } - } else { - None - }; - - Ok(cmd) -} - -/// Represents the email subcommands. -pub fn subcmd() -> Command { - Command::new(CMD_MESSAGE) - .about("Subcommand to manage messages") - .long_about("Subcommand to manage messages like read, write, reply or send") - .aliases(["msg"]) - .subcommand_required(true) - .arg_required_else_help(true) - .subcommands([ - Command::new(CMD_ATTACHMENTS) - .about("Downloads all emails attachments") - .arg(ids_arg()), - Command::new(CMD_WRITE) - .about("Write a new email") - .aliases(["new", "n"]) - .args(template::args::args()), - Command::new(CMD_SEND) - .about("Send a raw email") - .arg(raw_arg()), - Command::new(CMD_SAVE) - .about("Save a raw email") - .arg(raw_arg()), - Command::new(CMD_READ) - .about("Read text bodies of emails") - .arg(mime_type_arg()) - .arg(raw_flag()) - .arg(headers_arg()) - .arg(ids_arg()), - Command::new(CMD_REPLY) - .about("Answer to an email") - .arg(reply_all_flag()) - .args(template::args::args()) - .arg(id_arg()), - Command::new(CMD_FORWARD) - .aliases(["fwd", "f"]) - .about("Forward an email") - .args(template::args::args()) - .arg(id_arg()), - Command::new(CMD_COPY) - .alias("cp") - .about("Copy emails to the given folder") - // .arg(folder::args::target_arg()) - .arg(ids_arg()), - Command::new(CMD_MOVE) - .alias("mv") - .about("Move emails to the given folder") - // .arg(folder::args::target_arg()) - .arg(ids_arg()), - Command::new(CMD_DELETE) - .aliases(["remove", "rm"]) - .about("Delete emails") - .arg(ids_arg()), - ]) -} - -/// Represents the email id argument. -pub fn id_arg() -> Arg { - Arg::new(ARG_ID) - .help("Specifies the target email") - .value_name("ID") - .required(true) -} - -/// Represents the email id argument parser. -pub fn parse_id_arg(matches: &ArgMatches) -> &str { - matches.get_one::(ARG_ID).unwrap() -} - -/// Represents the email ids argument. -pub fn ids_arg() -> Arg { - Arg::new(ARG_IDS) - .help("Email ids") - .value_name("IDS") - .num_args(1..) - .required(true) -} - -/// Represents the email ids argument parser. -pub fn parse_ids_arg(matches: &ArgMatches) -> Vec<&str> { - matches - .get_many::(ARG_IDS) - .unwrap() - .map(String::as_str) - .collect() -} - -/// Represents the email sort criteria argument. -pub fn criteria_arg<'a>() -> Arg { - Arg::new(ARG_CRITERIA) - .help("Email sorting preferences") - .long("criterion") - .short('c') - .value_name("CRITERION:ORDER") - .action(ArgAction::Append) - .value_parser([ - "arrival", - "arrival:asc", - "arrival:desc", - "cc", - "cc:asc", - "cc:desc", - "date", - "date:asc", - "date:desc", - "from", - "from:asc", - "from:desc", - "size", - "size:asc", - "size:desc", - "subject", - "subject:asc", - "subject:desc", - "to", - "to:asc", - "to:desc", - ]) -} - -/// Represents the email sort criteria argument parser. -pub fn parse_criteria_arg(matches: &ArgMatches) -> String { - matches - .get_many::(ARG_CRITERIA) - .unwrap_or_default() - .map(ToOwned::to_owned) - .collect::>() - .join(" ") -} - -/// Represents the email reply all argument. -pub fn reply_all_flag() -> Arg { - Arg::new(ARG_REPLY_ALL) - .help("Include all recipients") - .long("all") - .short('A') - .action(ArgAction::SetTrue) -} - -/// Represents the email reply all argument parser. -pub fn parse_reply_all_flag(matches: &ArgMatches) -> bool { - matches.get_flag(ARG_REPLY_ALL) -} - -/// Represents the email headers argument. -pub fn headers_arg() -> Arg { - Arg::new(ARG_HEADERS) - .help("Shows additional headers with the email") - .long("header") - .short('H') - .value_name("STRING") - .action(ArgAction::Append) -} - -/// Represents the email headers argument parser. -pub fn parse_headers_arg(m: &ArgMatches) -> Vec<&str> { - m.get_many::(ARG_HEADERS) - .unwrap_or_default() - .map(String::as_str) - .collect::>() -} - -/// Represents the raw flag. -pub fn raw_flag() -> Arg { - Arg::new(ARG_RAW) - .help("Returns raw version of email") - .long("raw") - .short('r') - .action(ArgAction::SetTrue) -} - -/// Represents the raw flag parser. -pub fn parse_raw_flag(m: &ArgMatches) -> bool { - m.get_flag(ARG_RAW) -} - -/// Represents the email raw argument. -pub fn raw_arg() -> Arg { - Arg::new(ARG_RAW).raw(true) -} - -/// Represents the email raw argument parser. -pub fn parse_raw_arg(m: &ArgMatches) -> String { - m.get_one::(ARG_RAW).cloned().unwrap_or_default() -} - -/// Represents the email MIME type argument. -pub fn mime_type_arg() -> Arg { - Arg::new(ARG_MIME_TYPE) - .help("MIME type to use") - .short('t') - .long("mime-type") - .value_name("MIME") - .value_parser(["plain", "html"]) - .default_value("plain") -} - -/// Represents the email MIME type argument parser. -pub fn parse_mime_type_arg(matches: &ArgMatches) -> &str { - matches.get_one::(ARG_MIME_TYPE).unwrap() -} - -/// Represents the email query argument. -pub fn query_arg() -> Arg { - Arg::new(ARG_QUERY) - .long_help("The query system depends on the backend, see the wiki for more details") - .value_name("QUERY") - .num_args(1..) - .required(true) -} - -/// Represents the email query argument parser. -pub fn parse_query_arg(matches: &ArgMatches) -> String { - matches - .get_many::(ARG_QUERY) - .unwrap_or_default() - .fold((false, vec![]), |(escape, mut cmds), cmd| { - match (cmd.as_str(), escape) { - // Next command is an arg and needs to be escaped - ("subject", _) | ("body", _) | ("text", _) => { - cmds.push(cmd.to_string()); - (true, cmds) - } - // Escaped arg commands - (_, true) => { - cmds.push(format!("\"{}\"", cmd)); - (false, cmds) - } - // Regular commands - (_, false) => { - cmds.push(cmd.to_string()); - (false, cmds) - } - } - }) - .1 - .join(" ") -} diff --git a/src/email/message/attachment/command/download.rs b/src/email/message/attachment/command/download.rs new file mode 100644 index 0000000..f491e0a --- /dev/null +++ b/src/email/message/attachment/command/download.rs @@ -0,0 +1,85 @@ +use anyhow::{Context, Result}; +use clap::Parser; +use log::info; +use std::fs; +use uuid::Uuid; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameArg, + printer::Printer, +}; + +/// Download attachments of a message +#[derive(Debug, Parser)] +pub struct AttachmentDownloadCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub envelopes: EnvelopeIdsArgs, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl AttachmentDownloadCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing attachment download command"); + + let folder = &self.folder.name; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + + let ids = &self.envelopes.ids; + let emails = backend.get_messages(&folder, ids).await?; + + let mut emails_count = 0; + let mut attachments_count = 0; + + let mut ids = ids.iter(); + for email in emails.to_vec() { + let id = ids.next().unwrap(); + let attachments = email.attachments()?; + + if attachments.is_empty() { + printer.print_log(format!("No attachment found for message {id}!"))?; + continue; + } else { + emails_count += 1; + } + + printer.print_log(format!( + "{} attachment(s) found for message {id}!", + attachments.len() + ))?; + + for attachment in attachments { + let filename = attachment + .filename + .unwrap_or_else(|| Uuid::new_v4().to_string()); + let filepath = account_config.download_fpath(&filename)?; + printer.print_log(format!("Downloading {:?}…", filepath))?; + fs::write(&filepath, &attachment.body) + .with_context(|| format!("cannot save attachment at {filepath:?}"))?; + attachments_count += 1; + } + } + + match attachments_count { + 0 => printer.print("No attachment found!"), + 1 => printer.print("Downloaded 1 attachment!"), + n => printer.print(format!( + "Downloaded {} attachment(s) from {} messages(s)!", + n, emails_count, + )), + } + } +} diff --git a/src/email/message/attachment/command/mod.rs b/src/email/message/attachment/command/mod.rs new file mode 100644 index 0000000..3770ae0 --- /dev/null +++ b/src/email/message/attachment/command/mod.rs @@ -0,0 +1,24 @@ +pub mod download; + +use anyhow::Result; +use clap::Subcommand; + +use crate::{config::TomlConfig, printer::Printer}; + +use self::download::AttachmentDownloadCommand; + +/// Subcommand dedicated to attachments +#[derive(Debug, Subcommand)] +pub enum AttachmentSubcommand { + /// Download all attachments of one or more messages + #[command(arg_required_else_help = true)] + Download(AttachmentDownloadCommand), +} + +impl AttachmentSubcommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + match self { + Self::Download(cmd) => cmd.execute(printer, config).await, + } + } +} diff --git a/src/email/message/attachment/mod.rs b/src/email/message/attachment/mod.rs new file mode 100644 index 0000000..9fe7961 --- /dev/null +++ b/src/email/message/attachment/mod.rs @@ -0,0 +1 @@ +pub mod command; diff --git a/src/email/message/handlers.rs b/src/email/message/handlers.rs deleted file mode 100644 index 72a99f9..0000000 --- a/src/email/message/handlers.rs +++ /dev/null @@ -1,101 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use atty::Stream; -use email::{ - account::config::AccountConfig, envelope::Id, flag::Flag, message::Message, - template::FilterParts, -}; -use log::trace; -use mail_builder::MessageBuilder; -use std::{ - fs, - io::{self, BufRead}, -}; -use url::Url; -use uuid::Uuid; - -use crate::{backend::Backend, printer::Printer, ui::editor}; - -pub async fn attachments( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - folder: &str, - ids: Vec<&str>, -) -> Result<()> { - let emails = backend.get_messages(&folder, &ids).await?; - let mut index = 0; - - let mut emails_count = 0; - let mut attachments_count = 0; - - let mut ids = ids.iter(); - for email in emails.to_vec() { - let id = ids.next().unwrap(); - let attachments = email.attachments()?; - - index = index + 1; - - if attachments.is_empty() { - printer.print_log(format!("No attachment found for email #{}", id))?; - continue; - } else { - emails_count = emails_count + 1; - } - - printer.print_log(format!( - "{} attachment(s) found for email #{}…", - attachments.len(), - id - ))?; - - for attachment in attachments { - let filename = attachment - .filename - .unwrap_or_else(|| Uuid::new_v4().to_string()); - let filepath = config.download_fpath(&filename)?; - printer.print_log(format!("Downloading {:?}…", filepath))?; - fs::write(&filepath, &attachment.body).context("cannot download attachment")?; - attachments_count = attachments_count + 1; - } - } - - match attachments_count { - 0 => printer.print("No attachment found!"), - 1 => printer.print("Downloaded 1 attachment!"), - n => printer.print(format!( - "Downloaded {} attachment(s) from {} email(s)!", - n, emails_count, - )), - } -} - -/// Parses and edits a message from a [mailto] URL string. -/// -/// [mailto]: https://en.wikipedia.org/wiki/Mailto -pub async fn mailto( - config: &AccountConfig, - backend: &Backend, - printer: &mut P, - url: &Url, -) -> Result<()> { - let mut builder = MessageBuilder::new().to(url.path()); - - for (key, val) in url.query_pairs() { - match key.to_lowercase().as_bytes() { - b"cc" => builder = builder.cc(val.to_string()), - b"bcc" => builder = builder.bcc(val.to_string()), - b"subject" => builder = builder.subject(val), - b"body" => builder = builder.text_body(val), - _ => (), - } - } - - let tpl = config - .generate_tpl_interpreter() - .with_show_only_headers(config.email_writing_headers()) - .build() - .from_msg_builder(builder) - .await?; - - editor::edit_tpl_with_editor(config, printer, backend, tpl).await -} diff --git a/src/email/message/mod.rs b/src/email/message/mod.rs index 754da7b..12c67d2 100644 --- a/src/email/message/mod.rs +++ b/src/email/message/mod.rs @@ -1,6 +1,5 @@ -// pub mod args; pub mod arg; +pub mod attachment; pub mod command; pub mod config; -// pub mod handlers; // pub mod template; From fff11fbe201c982f70120414c1da9ffc2ddfab3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Thu, 7 Dec 2023 22:37:28 +0100 Subject: [PATCH 23/29] refactor template with clap derive api --- src/cli.rs | 11 +- src/email/message/arg/body.rs | 3 +- src/email/message/arg/mod.rs | 1 + src/email/message/arg/reply.rs | 9 + .../message/attachment/command/download.rs | 2 +- src/email/message/command/reply.rs | 11 +- src/email/message/mod.rs | 2 +- src/email/message/template/args.rs | 154 ------------------ src/email/message/template/command/forward.rs | 65 ++++++++ src/email/message/template/command/mod.rs | 52 ++++++ src/email/message/template/command/reply.rs | 69 ++++++++ src/email/message/template/command/save.rs | 66 ++++++++ src/email/message/template/command/send.rs | 73 +++++++++ src/email/message/template/command/write.rs | 48 ++++++ src/email/message/template/handlers.rs | 143 ---------------- src/email/message/template/mod.rs | 3 +- src/email/mod.rs | 6 +- src/main.rs | 93 ----------- 18 files changed, 406 insertions(+), 405 deletions(-) create mode 100644 src/email/message/arg/reply.rs delete mode 100644 src/email/message/template/args.rs create mode 100644 src/email/message/template/command/forward.rs create mode 100644 src/email/message/template/command/mod.rs create mode 100644 src/email/message/template/command/reply.rs create mode 100644 src/email/message/template/command/save.rs create mode 100644 src/email/message/template/command/send.rs create mode 100644 src/email/message/template/command/write.rs delete mode 100644 src/email/message/template/handlers.rs diff --git a/src/cli.rs b/src/cli.rs index a1c8b7a..a66fc68 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,7 +10,10 @@ use crate::{ flag::command::FlagSubcommand, folder::command::FolderSubcommand, manual::command::ManualGenerateCommand, - message::{attachment::command::AttachmentSubcommand, command::MessageSubcommand}, + message::{ + attachment::command::AttachmentSubcommand, command::MessageSubcommand, + template::command::TemplateSubcommand, + }, output::{ColorFmt, OutputFmt}, printer::Printer, }; @@ -105,6 +108,11 @@ pub enum HimalayaCommand { #[command(alias = "messages", alias = "msgs", alias = "msg")] Message(MessageSubcommand), + /// Manage templates + #[command(subcommand)] + #[command(alias = "templates", alias = "tpls", alias = "tpl")] + Template(TemplateSubcommand), + /// Manage attachments #[command(subcommand)] Attachment(AttachmentSubcommand), @@ -128,6 +136,7 @@ impl HimalayaCommand { Self::Envelope(cmd) => cmd.execute(printer, config).await, Self::Flag(cmd) => cmd.execute(printer, config).await, Self::Message(cmd) => cmd.execute(printer, config).await, + Self::Template(cmd) => cmd.execute(printer, config).await, Self::Attachment(cmd) => cmd.execute(printer, config).await, Self::Manual(cmd) => cmd.execute(printer).await, Self::Completion(cmd) => cmd.execute(printer).await, diff --git a/src/email/message/arg/body.rs b/src/email/message/arg/body.rs index b06446d..36ca6e0 100644 --- a/src/email/message/arg/body.rs +++ b/src/email/message/arg/body.rs @@ -1,6 +1,5 @@ -use std::ops::Deref; - use clap::Parser; +use std::ops::Deref; /// The raw message body argument parser #[derive(Debug, Parser)] diff --git a/src/email/message/arg/mod.rs b/src/email/message/arg/mod.rs index 53bf2e6..509753e 100644 --- a/src/email/message/arg/mod.rs +++ b/src/email/message/arg/mod.rs @@ -1,2 +1,3 @@ pub mod body; pub mod header; +pub mod reply; diff --git a/src/email/message/arg/reply.rs b/src/email/message/arg/reply.rs new file mode 100644 index 0000000..5855001 --- /dev/null +++ b/src/email/message/arg/reply.rs @@ -0,0 +1,9 @@ +use clap::Parser; + +/// The reply to all argument parser +#[derive(Debug, Parser)] +pub struct MessageReplyAllArg { + /// Reply to all recipients + #[arg(long, short = 'A')] + pub all: bool, +} diff --git a/src/email/message/attachment/command/download.rs b/src/email/message/attachment/command/download.rs index f491e0a..e0b7a1d 100644 --- a/src/email/message/attachment/command/download.rs +++ b/src/email/message/attachment/command/download.rs @@ -36,7 +36,7 @@ impl AttachmentDownloadCommand { let (toml_account_config, account_config) = config.clone().into_account_configs(account, cache)?; - let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; let ids = &self.envelopes.ids; let emails = backend.get_messages(&folder, ids).await?; diff --git a/src/email/message/command/reply.rs b/src/email/message/command/reply.rs index d9cff8b..c95c537 100644 --- a/src/email/message/command/reply.rs +++ b/src/email/message/command/reply.rs @@ -12,7 +12,7 @@ use crate::{ config::TomlConfig, envelope::arg::ids::EnvelopeIdArg, folder::arg::name::FolderNameArg, - message::arg::{body::BodyRawArg, header::HeaderRawArgs}, + message::arg::{body::BodyRawArg, header::HeaderRawArgs, reply::MessageReplyAllArg}, printer::Printer, ui::editor, }; @@ -26,9 +26,8 @@ pub struct MessageReplyCommand { #[command(flatten)] pub envelope: EnvelopeIdArg, - /// Reply to all recipients - #[arg(long, short = 'A')] - pub all: bool, + #[command(flatten)] + pub reply: MessageReplyAllArg, #[command(flatten)] pub headers: HeaderRawArgs, @@ -73,11 +72,11 @@ impl MessageReplyCommand { .get_messages(folder, &[id]) .await? .first() - .ok_or(anyhow!("cannot find message"))? + .ok_or(anyhow!("cannot find message {id}"))? .to_reply_tpl_builder(&account_config) .with_headers(self.headers.raw) .with_body(body) - .with_reply_all(self.all) + .with_reply_all(self.reply.all) .build() .await?; editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await?; diff --git a/src/email/message/mod.rs b/src/email/message/mod.rs index 12c67d2..b442bc0 100644 --- a/src/email/message/mod.rs +++ b/src/email/message/mod.rs @@ -2,4 +2,4 @@ pub mod arg; pub mod attachment; pub mod command; pub mod config; -// pub mod template; +pub mod template; diff --git a/src/email/message/template/args.rs b/src/email/message/template/args.rs deleted file mode 100644 index a64e7b7..0000000 --- a/src/email/message/template/args.rs +++ /dev/null @@ -1,154 +0,0 @@ -//! Module related to email template CLI. -//! -//! This module provides subcommands, arguments and a command matcher -//! related to email templating. - -use anyhow::Result; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use log::warn; - -use crate::message; - -const ARG_BODY: &str = "body"; -const ARG_HEADERS: &str = "headers"; -const ARG_TPL: &str = "template"; -const CMD_FORWARD: &str = "forward"; -const CMD_REPLY: &str = "reply"; -const CMD_SAVE: &str = "save"; -const CMD_SEND: &str = "send"; -const CMD_WRITE: &str = "write"; - -pub const CMD_TPL: &str = "template"; - -pub type RawTpl = String; -pub type Headers<'a> = Option>; -pub type Body<'a> = Option<&'a str>; - -/// Represents the template commands. -#[derive(Debug, PartialEq, Eq)] -pub enum Cmd<'a> { - Forward(message::args::Id<'a>, Headers<'a>, Body<'a>), - Write(Headers<'a>, Body<'a>), - Reply( - message::args::Id<'a>, - message::args::All, - Headers<'a>, - Body<'a>, - ), - Save(RawTpl), - Send(RawTpl), -} - -/// Represents the template command matcher. -pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_FORWARD) { - let id = message::args::parse_id_arg(m); - let headers = parse_headers_arg(m); - let body = parse_body_arg(m); - Some(Cmd::Forward(id, headers, body)) - } else if let Some(m) = m.subcommand_matches(CMD_REPLY) { - let id = message::args::parse_id_arg(m); - let all = message::args::parse_reply_all_flag(m); - let headers = parse_headers_arg(m); - let body = parse_body_arg(m); - Some(Cmd::Reply(id, all, headers, body)) - } else if let Some(m) = m.subcommand_matches(CMD_SAVE) { - let raw_tpl = parse_raw_arg(m); - Some(Cmd::Save(raw_tpl)) - } else if let Some(m) = m.subcommand_matches(CMD_SEND) { - let raw_tpl = parse_raw_arg(m); - Some(Cmd::Send(raw_tpl)) - } else if let Some(m) = m.subcommand_matches(CMD_WRITE) { - let headers = parse_headers_arg(m); - let body = parse_body_arg(m); - Some(Cmd::Write(headers, body)) - } else { - None - }; - - Ok(cmd) -} - -/// Represents the template subcommands. -pub fn subcmd() -> Command { - Command::new(CMD_TPL) - .alias("tpl") - .about("Subcommand to manage templates") - .long_about("Subcommand to manage templates like write, reply, send or save") - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand( - Command::new(CMD_FORWARD) - .alias("fwd") - .about("Generate a template for forwarding an email") - .arg(message::args::id_arg()) - .args(&args()), - ) - .subcommand( - Command::new(CMD_REPLY) - .about("Generate a template for replying to an email") - .arg(message::args::id_arg()) - .arg(message::args::reply_all_flag()) - .args(&args()), - ) - .subcommand( - Command::new(CMD_SAVE) - .about("Compile the template into a valid email then saves it") - .arg(Arg::new(ARG_TPL).raw(true)), - ) - .subcommand( - Command::new(CMD_SEND) - .about("Compile the template into a valid email then sends it") - .arg(Arg::new(ARG_TPL).raw(true)), - ) - .subcommand( - Command::new(CMD_WRITE) - .aliases(["new", "n"]) - .about("Generate a template for writing a new email") - .args(&args()), - ) -} - -/// Represents the template arguments. -pub fn args() -> Vec { - vec![ - Arg::new(ARG_HEADERS) - .help("Override a specific header") - .short('H') - .long("header") - .value_name("KEY:VAL") - .action(ArgAction::Append), - Arg::new(ARG_BODY) - .help("Override the body") - .short('B') - .long("body") - .value_name("STRING"), - ] -} - -/// Represents the template headers argument parser. -pub fn parse_headers_arg(m: &ArgMatches) -> Headers<'_> { - m.get_many::(ARG_HEADERS).map(|h| { - h.filter_map(|h| match h.split_once(':') { - Some((key, val)) => Some((key, val.trim())), - None => { - warn!("invalid raw header {h:?}, skipping it"); - None - } - }) - .collect() - }) -} - -/// Represents the template body argument parser. -pub fn parse_body_arg(matches: &ArgMatches) -> Body<'_> { - matches.get_one::(ARG_BODY).map(String::as_str) -} - -/// Represents the raw template argument parser. -pub fn parse_raw_arg(matches: &ArgMatches) -> RawTpl { - matches - .get_one::(ARG_TPL) - .cloned() - .unwrap_or_default() -} diff --git a/src/email/message/template/command/forward.rs b/src/email/message/template/command/forward.rs new file mode 100644 index 0000000..b5950d1 --- /dev/null +++ b/src/email/message/template/command/forward.rs @@ -0,0 +1,65 @@ +use anyhow::{anyhow, Result}; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + envelope::arg::ids::EnvelopeIdArg, + folder::arg::name::FolderNameArg, + message::arg::{body::BodyRawArg, header::HeaderRawArgs}, + printer::Printer, +}; + +/// Generate a forward message template +#[derive(Debug, Parser)] +pub struct TemplateForwardCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub envelope: EnvelopeIdArg, + + #[command(flatten)] + pub headers: HeaderRawArgs, + + #[command(flatten)] + pub body: BodyRawArg, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl TemplateForwardCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing template forward command"); + + let folder = &self.folder.name; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + let id = self.envelope.id; + let tpl: String = backend + .get_messages(folder, &[id]) + .await? + .first() + .ok_or(anyhow!("cannot find message {id}"))? + .to_forward_tpl_builder(&account_config) + .with_headers(self.headers.raw) + .with_body(self.body.raw()) + .build() + .await? + .into(); + + printer.print(tpl) + } +} diff --git a/src/email/message/template/command/mod.rs b/src/email/message/template/command/mod.rs new file mode 100644 index 0000000..202fdf5 --- /dev/null +++ b/src/email/message/template/command/mod.rs @@ -0,0 +1,52 @@ +pub mod forward; +pub mod reply; +pub mod save; +pub mod send; +pub mod write; + +use anyhow::Result; +use clap::Subcommand; + +use crate::{config::TomlConfig, printer::Printer}; + +use self::{ + forward::TemplateForwardCommand, reply::TemplateReplyCommand, save::TemplateSaveCommand, + send::TemplateSendCommand, write::TemplateWriteCommand, +}; + +/// Subcommand to manage templates +#[derive(Debug, Subcommand)] +pub enum TemplateSubcommand { + /// Write a new template + #[command(alias = "new", alias = "compose")] + Write(TemplateWriteCommand), + + /// Reply to a template + #[command()] + Reply(TemplateReplyCommand), + + /// Generate a template for forwarding an email + #[command(alias = "fwd")] + Forward(TemplateForwardCommand), + + /// Save a template to a folder + #[command(arg_required_else_help = true)] + #[command(alias = "add", alias = "create")] + Save(TemplateSaveCommand), + + /// Send a template + #[command(arg_required_else_help = true)] + Send(TemplateSendCommand), +} + +impl TemplateSubcommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + match self { + Self::Write(cmd) => cmd.execute(printer, config).await, + Self::Reply(cmd) => cmd.execute(printer, config).await, + Self::Forward(cmd) => cmd.execute(printer, config).await, + Self::Save(cmd) => cmd.execute(printer, config).await, + Self::Send(cmd) => cmd.execute(printer, config).await, + } + } +} diff --git a/src/email/message/template/command/reply.rs b/src/email/message/template/command/reply.rs new file mode 100644 index 0000000..80f75b4 --- /dev/null +++ b/src/email/message/template/command/reply.rs @@ -0,0 +1,69 @@ +use anyhow::{anyhow, Result}; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + envelope::arg::ids::EnvelopeIdArg, + folder::arg::name::FolderNameArg, + message::arg::{body::BodyRawArg, header::HeaderRawArgs, reply::MessageReplyAllArg}, + printer::Printer, +}; + +/// Generate a reply message template +#[derive(Debug, Parser)] +pub struct TemplateReplyCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub envelope: EnvelopeIdArg, + + #[command(flatten)] + pub reply: MessageReplyAllArg, + + #[command(flatten)] + pub headers: HeaderRawArgs, + + #[command(flatten)] + pub body: BodyRawArg, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl TemplateReplyCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing template reply command"); + + let folder = &self.folder.name; + let id = self.envelope.id; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + let tpl: String = backend + .get_messages(folder, &[id]) + .await? + .first() + .ok_or(anyhow!("cannot find message {id}"))? + .to_reply_tpl_builder(&account_config) + .with_headers(self.headers.raw) + .with_body(self.body.raw()) + .with_reply_all(self.reply.all) + .build() + .await? + .into(); + + printer.print(tpl) + } +} diff --git a/src/email/message/template/command/save.rs b/src/email/message/template/command/save.rs new file mode 100644 index 0000000..c99fc52 --- /dev/null +++ b/src/email/message/template/command/save.rs @@ -0,0 +1,66 @@ +use anyhow::Result; +use atty::Stream; +use clap::Parser; +use log::info; +use mml::MmlCompilerBuilder; +use std::io::{self, BufRead}; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer, +}; + +/// Save a template to a folder +#[derive(Debug, Parser)] +pub struct TemplateSaveCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + /// The raw template to save + #[arg(raw = true, value_delimiter = ' ')] + pub raw: Vec, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl TemplateSaveCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing template save command"); + + let folder = &self.folder.name; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + let is_tty = atty::is(Stream::Stdin); + let is_json = printer.is_json(); + let tpl = if is_tty || is_json { + self.raw.join(" ").replace("\r", "") + } else { + io::stdin() + .lock() + .lines() + .filter_map(Result::ok) + .collect::>() + .join("\n") + }; + + #[allow(unused_mut)] + let mut compiler = MmlCompilerBuilder::new(); + + #[cfg(feature = "pgp")] + compiler.set_some_pgp(config.pgp.clone()); + + let msg = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; + backend.add_raw_message(folder, &msg).await?; + + printer.print(format!("Template successfully saved to {folder}!")) + } +} diff --git a/src/email/message/template/command/send.rs b/src/email/message/template/command/send.rs new file mode 100644 index 0000000..ff36745 --- /dev/null +++ b/src/email/message/template/command/send.rs @@ -0,0 +1,73 @@ +use anyhow::Result; +use atty::Stream; +use clap::Parser; +use email::flag::Flag; +use log::info; +use mml::MmlCompilerBuilder; +use std::io::{self, BufRead}; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, printer::Printer, +}; + +/// Send a template +#[derive(Debug, Parser)] +pub struct TemplateSendCommand { + /// The raw template to save + #[arg(raw = true, value_delimiter = ' ')] + pub raw: Vec, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl TemplateSendCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing template send command"); + + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (toml_account_config, account_config) = + config.clone().into_account_configs(account, cache)?; + let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; + let folder = account_config.sent_folder_alias()?; + + let is_tty = atty::is(Stream::Stdin); + let is_json = printer.is_json(); + let tpl = if is_tty || is_json { + self.raw.join(" ").replace("\r", "") + } else { + io::stdin() + .lock() + .lines() + .filter_map(Result::ok) + .collect::>() + .join("\r\n") + }; + + #[allow(unused_mut)] + let mut compiler = MmlCompilerBuilder::new(); + + #[cfg(feature = "pgp")] + compiler.set_some_pgp(config.pgp.clone()); + + let msg = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; + + backend.send_raw_message(&msg).await?; + + if account_config.email_sending_save_copy.unwrap_or_default() { + backend + .add_raw_message_with_flag(&folder, &msg, Flag::Seen) + .await?; + + printer.print(format!("Template successfully sent and saved to {folder}!")) + } else { + printer.print("Template successfully sent!") + } + } +} diff --git a/src/email/message/template/command/write.rs b/src/email/message/template/command/write.rs new file mode 100644 index 0000000..72b3168 --- /dev/null +++ b/src/email/message/template/command/write.rs @@ -0,0 +1,48 @@ +use anyhow::Result; +use clap::Parser; +use email::message::Message; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + message::arg::{body::BodyRawArg, header::HeaderRawArgs}, + printer::Printer, +}; + +/// Write a new template +#[derive(Debug, Parser)] +pub struct TemplateWriteCommand { + #[command(flatten)] + pub headers: HeaderRawArgs, + + #[command(flatten)] + pub body: BodyRawArg, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl TemplateWriteCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing template write command"); + + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + + let (_, account_config) = config.clone().into_account_configs(account, cache)?; + + let tpl: String = Message::new_tpl_builder(&account_config) + .with_headers(self.headers.raw) + .with_body(self.body.raw()) + .build() + .await? + .into(); + + printer.print(tpl) + } +} diff --git a/src/email/message/template/handlers.rs b/src/email/message/template/handlers.rs deleted file mode 100644 index 188bb74..0000000 --- a/src/email/message/template/handlers.rs +++ /dev/null @@ -1,143 +0,0 @@ -use anyhow::{anyhow, Result}; -use atty::Stream; -use email::{account::config::AccountConfig, flag::Flag, message::Message}; -use mml::MmlCompilerBuilder; -use std::io::{stdin, BufRead}; - -use crate::{backend::Backend, printer::Printer}; - -pub async fn forward( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - folder: &str, - id: &str, - headers: Option>, - body: Option<&str>, -) -> Result<()> { - let tpl: String = backend - .get_messages(folder, &[id]) - .await? - .first() - .ok_or_else(|| anyhow!("cannot find email {}", id))? - .to_forward_tpl_builder(config) - .with_some_headers(headers) - .with_some_body(body) - .build() - .await? - .into(); - - printer.print(tpl) -} - -pub async fn reply( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - folder: &str, - id: &str, - all: bool, - headers: Option>, - body: Option<&str>, -) -> Result<()> { - let tpl: String = backend - .get_messages(folder, &[id]) - .await? - .first() - .ok_or_else(|| anyhow!("cannot find email {}", id))? - .to_reply_tpl_builder(config) - .with_some_headers(headers) - .with_some_body(body) - .with_reply_all(all) - .build() - .await? - .into(); - - printer.print(tpl) -} - -pub async fn save( - #[allow(unused_variables)] config: &AccountConfig, - printer: &mut P, - backend: &Backend, - folder: &str, - tpl: String, -) -> Result<()> { - let tpl = if atty::is(Stream::Stdin) || printer.is_json() { - tpl.replace("\r", "") - } else { - stdin() - .lock() - .lines() - .filter_map(Result::ok) - .collect::>() - .join("\n") - }; - - #[allow(unused_mut)] - let mut compiler = MmlCompilerBuilder::new(); - - #[cfg(feature = "pgp")] - compiler.set_some_pgp(config.pgp.clone()); - - let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; - - backend.add_raw_message(folder, &email).await?; - - printer.print("Template successfully saved!") -} - -pub async fn send( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - tpl: String, -) -> Result<()> { - let folder = config.sent_folder_alias()?; - - let tpl = if atty::is(Stream::Stdin) || printer.is_json() { - tpl.replace("\r", "") - } else { - stdin() - .lock() - .lines() - .filter_map(Result::ok) - .collect::>() - .join("\n") - }; - - #[allow(unused_mut)] - let mut compiler = MmlCompilerBuilder::new(); - - #[cfg(feature = "pgp")] - compiler.set_some_pgp(config.pgp.clone()); - - let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; - - backend.send_raw_message(&email).await?; - - if config.email_sending_save_copy.unwrap_or_default() { - backend - .add_raw_message_with_flag(&folder, &email, Flag::Seen) - .await?; - } - - printer.print("Template successfully sent!")?; - Ok(()) -} - -pub async fn write( - config: &AccountConfig, - printer: &mut P, - headers: Option>, - body: Option<&str>, -) -> Result<()> { - let tpl: String = Message::new_tpl_builder(config) - .with_some_headers(headers) - .with_some_body(body) - .build() - .await? - .into(); - - printer.print(tpl) -} diff --git a/src/email/message/template/mod.rs b/src/email/message/template/mod.rs index b0b957b..9fe7961 100644 --- a/src/email/message/template/mod.rs +++ b/src/email/message/template/mod.rs @@ -1,2 +1 @@ -pub mod args; -pub mod handlers; +pub mod command; diff --git a/src/email/mod.rs b/src/email/mod.rs index 0e144c4..550c6b0 100644 --- a/src/email/mod.rs +++ b/src/email/mod.rs @@ -2,5 +2,7 @@ pub mod envelope; pub mod message; #[doc(inline)] -// pub use self::{envelope::flag, message::template}; -pub use self::envelope::flag; +pub use self::{ + envelope::flag, + message::{attachment, template}, +}; diff --git a/src/main.rs b/src/main.rs index 611770d..ea150eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,96 +36,3 @@ async fn main() -> Result<()> { cli.command.execute(&mut printer, &config).await } - -// fn create_app() -> clap::Command { -// clap::Command::new(env!("CARGO_PKG_NAME")) -// .subcommand(message::args::subcmd()) -// .subcommand(template::args::subcmd()) -// } - -// #[tokio::main] -// async fn main() -> Result<()> { -// // check mailto command before app initialization -// let raw_args: Vec = env::args().collect(); -// if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { -// let url = Url::parse(&raw_args[1])?; -// let (toml_account_config, account_config) = TomlConfig::from_default_paths() -// .await? -// .into_account_configs(None, false)?; -// let backend_builder = -// BackendBuilder::new(toml_account_config, account_config.clone(), true).await?; -// let backend = backend_builder.build().await?; -// let mut printer = StdoutPrinter::default(); - -// return message::handlers::mailto(&account_config, &backend, &mut printer, &url).await; -// } - -// match message::args::matches(&m)? { -// Some(message::args::Cmd::Attachments(ids)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return message::handlers::attachments( -// &account_config, -// &mut printer, -// &backend, -// &folder, -// ids, -// ) -// .await; -// } -// _ => (), -// } - -// match template::args::matches(&m)? { -// Some(template::args::Cmd::Forward(id, headers, body)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return template::handlers::forward( -// &account_config, -// &mut printer, -// &backend, -// &folder, -// id, -// headers, -// body, -// ) -// .await; -// } -// Some(template::args::Cmd::Write(headers, body)) => { -// return template::handlers::write(&account_config, &mut printer, headers, body).await; -// } -// Some(template::args::Cmd::Reply(id, all, headers, body)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return template::handlers::reply( -// &account_config, -// &mut printer, -// &backend, -// &folder, -// id, -// all, -// headers, -// body, -// ) -// .await; -// } -// Some(template::args::Cmd::Save(template)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return template::handlers::save( -// &account_config, -// &mut printer, -// &backend, -// &folder, -// template, -// ) -// .await; -// } -// Some(template::args::Cmd::Send(template)) => { -// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; -// return template::handlers::send(&account_config, &mut printer, &backend, template) -// .await; -// } -// _ => (), -// } -// } From ef3214f36fa833fd72552193cbc7603d18cd2556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 8 Dec 2023 12:18:18 +0100 Subject: [PATCH 24/29] clean doc --- src/account/arg/name.rs | 26 ++++++------- src/account/command/configure.rs | 29 ++++++++------- src/account/command/list.rs | 13 ++++--- src/account/command/mod.rs | 9 +++-- src/account/command/sync.rs | 37 ++++++++++++++----- src/cache/arg/disable.rs | 18 ++++----- src/cli.rs | 28 ++++---------- src/completion/command.rs | 8 +++- src/config/mod.rs | 5 +-- src/email/envelope/arg/ids.rs | 8 ++-- src/email/envelope/command/list.rs | 26 ++++++++----- src/email/envelope/command/mod.rs | 8 +++- src/email/envelope/flag/arg/ids_and_flags.rs | 4 +- src/email/envelope/flag/command/add.rs | 11 ++++-- src/email/envelope/flag/command/mod.rs | 10 +++-- src/email/envelope/flag/command/remove.rs | 11 ++++-- src/email/envelope/flag/command/set.rs | 11 ++++-- src/email/message/arg/body.rs | 14 +++---- src/email/message/arg/header.rs | 4 +- src/email/message/arg/reply.rs | 7 +++- .../message/attachment/command/download.rs | 9 +++-- src/email/message/attachment/command/mod.rs | 7 +++- src/email/message/command/copy.rs | 6 +-- src/email/message/command/delete.rs | 11 ++++-- src/email/message/command/forward.rs | 19 +++++----- src/email/message/command/mailto.rs | 28 ++++++++++++-- src/email/message/command/mod.rs | 19 ++++------ src/email/message/command/move_.rs | 6 +-- src/email/message/command/read.rs | 22 ++++++----- src/email/message/command/reply.rs | 15 +++++--- src/email/message/command/save.rs | 25 ++++++------- src/email/message/command/send.rs | 25 +++++++------ src/email/message/command/write.rs | 15 +++++--- src/email/message/template/arg/body.rs | 25 +++++++++++++ src/email/message/template/arg/mod.rs | 1 + src/email/message/template/command/forward.rs | 14 ++++--- src/email/message/template/command/mod.rs | 25 +++++++------ src/email/message/template/command/reply.rs | 15 +++++--- src/email/message/template/command/save.rs | 21 +++++++---- src/email/message/template/command/send.rs | 20 ++++++---- src/email/message/template/command/write.rs | 15 ++++---- src/email/message/template/mod.rs | 1 + src/folder/arg/name.rs | 20 +++++----- src/folder/command/create.rs | 11 ++++-- src/folder/command/delete.rs | 12 +++--- src/folder/command/expunge.rs | 10 ++--- src/folder/command/list.rs | 16 ++++---- src/folder/command/mod.rs | 12 +++--- src/folder/command/purge.rs | 12 +++--- src/main.rs | 25 +++++++------ src/manual/command.rs | 9 ++++- src/ui/table/arg/max_width.rs | 12 ++++-- 52 files changed, 452 insertions(+), 318 deletions(-) create mode 100644 src/email/message/template/arg/body.rs create mode 100644 src/email/message/template/arg/mod.rs diff --git a/src/account/arg/name.rs b/src/account/arg/name.rs index 4456e14..189d5ba 100644 --- a/src/account/arg/name.rs +++ b/src/account/arg/name.rs @@ -1,26 +1,24 @@ use clap::Parser; -/// The account name argument parser +/// The account name argument parser. #[derive(Debug, Parser)] pub struct AccountNameArg { - /// The name of the account + /// The name of the account. /// - /// The account names are taken from the table at the root level - /// of your TOML configuration file. - #[arg(value_name = "ACCOUNT")] + /// An account name corresponds to an entry in the table at the + /// root level of your TOML configuration file. + #[arg(name = "account_name", value_name = "ACCOUNT")] pub name: String, } -/// The account name flag parser +/// The account name flag parser. #[derive(Debug, Parser)] pub struct AccountNameFlag { - /// Override the default account - #[arg( - long = "account", - short = 'a', - name = "account-name", - value_name = "NAME", - global = true - )] + /// Override the default account. + /// + /// An account name corresponds to an entry in the table at the + /// root level of your TOML configuration file. + #[arg(long = "account", short = 'a', global = true)] + #[arg(name = "account_name", value_name = "NAME")] pub name: Option, } diff --git a/src/account/command/configure.rs b/src/account/command/configure.rs index bcfbda1..616e97c 100644 --- a/src/account/command/configure.rs +++ b/src/account/command/configure.rs @@ -15,26 +15,32 @@ use crate::{ printer::Printer, }; -/// Configure the given account +/// Configure an account. +/// +/// This command is mostly used to define or reset passwords managed +/// by your global keyring. If you do not use the keyring system, you +/// can skip this command. #[derive(Debug, Parser)] pub struct AccountConfigureCommand { #[command(flatten)] pub account: AccountNameArg, - /// Force the account to reconfigure, even if it has already been - /// configured + /// Reset keyring passwords. + /// + /// This argument will force passwords to be prompted again, then + /// saved to your global keyring. #[arg(long, short)] - pub force: bool, + pub reset: bool, } impl AccountConfigureCommand { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { info!("executing account configure command"); - let (_, account_config) = - config.into_toml_account_config(Some(self.account.name.as_str()))?; + let account = &self.account.name; + let (_, account_config) = config.into_toml_account_config(Some(account))?; - if self.force { + if self.reset { #[cfg(feature = "imap")] if let Some(ref config) = account_config.imap { let reset = match &config.auth { @@ -102,11 +108,8 @@ impl AccountConfigureCommand { } printer.print(format!( - "Account {} successfully {}configured!", - self.account.name, - if self.force { "re" } else { "" } - ))?; - - Ok(()) + "Account {account} successfully {}configured!", + if self.reset { "re" } else { "" } + )) } } diff --git a/src/account/command/list.rs b/src/account/command/list.rs index ee70b04..5b6a72a 100644 --- a/src/account/command/list.rs +++ b/src/account/command/list.rs @@ -6,14 +6,17 @@ use crate::{ account::Accounts, config::TomlConfig, printer::{PrintTableOpts, Printer}, - ui::arg::max_width::MaxTableWidthFlag, + ui::arg::max_width::TableMaxWidthFlag, }; -/// List all accounts +/// List all accounts. +/// +/// This command lists all accounts defined in your TOML configuration +/// file. #[derive(Debug, Parser)] pub struct AccountListCommand { #[command(flatten)] - pub table: MaxTableWidthFlag, + pub table: TableMaxWidthFlag, } impl AccountListCommand { @@ -31,9 +34,7 @@ impl AccountListCommand { .unwrap_or(&Default::default()), max_width: self.table.max_width, }, - )?; - - Ok(()) + ) } } diff --git a/src/account/command/mod.rs b/src/account/command/mod.rs index 2677935..76c8147 100644 --- a/src/account/command/mod.rs +++ b/src/account/command/mod.rs @@ -11,18 +11,19 @@ use self::{ configure::AccountConfigureCommand, list::AccountListCommand, sync::AccountSyncCommand, }; -/// Subcommand to manage accounts +/// Manage accounts. +/// +/// An account is a set of settings, identified by an account +/// name. Settings are directly taken from your TOML configuration +/// file. #[derive(Debug, Subcommand)] pub enum AccountSubcommand { - /// Configure an account #[command(alias = "cfg")] Configure(AccountConfigureCommand), - /// List all accounts #[command(alias = "lst")] List(AccountListCommand), - /// Synchronize an account locally #[command()] Sync(AccountSyncCommand), } diff --git a/src/account/command/sync.rs b/src/account/command/sync.rs index 286d05f..7db35bc 100644 --- a/src/account/command/sync.rs +++ b/src/account/command/sync.rs @@ -32,30 +32,49 @@ const SUB_PROGRESS_DONE_STYLE: Lazy = Lazy::new(|| { ProgressStyle::with_template(" {prefix:.bold} \n {wide_bar:.green} {percent}% ").unwrap() }); +/// Synchronize an account. +/// +/// This command allows you to synchronize all folders and emails +/// (including envelopes and messages) of an account into a local +/// Maildir folder. #[derive(Debug, Parser)] pub struct AccountSyncCommand { #[command(flatten)] pub account: AccountNameArg, - /// Run the synchronization without applying changes + /// Run the synchronization without applying any changes. /// /// Instead, a report will be printed to stdout containing all the /// changes the synchronization plan to do. #[arg(long, short)] pub dry_run: bool, - #[arg(long, short = 'f', value_name = "FOLDER", action = ArgAction::Append, conflicts_with = "exclude_folder", conflicts_with = "all_folders")] + /// Synchronize only specific folders. + /// + /// Only the given folders will be synchronized (including + /// associated envelopes and messages). Useful when you need to + /// speed up the synchronization process. A good usecase is to + /// synchronize only the INBOX in order to quickly check for new + /// messages. + #[arg(long, short = 'f')] + #[arg(value_name = "FOLDER", action = ArgAction::Append)] + #[arg(conflicts_with = "exclude_folder", conflicts_with = "all_folders")] pub include_folder: Vec, - #[arg(long, short = 'x', value_name = "FOLDER", action = ArgAction::Append, conflicts_with = "include_folder", conflicts_with = "all_folders")] + /// Omit specific folders from the synchronization. + /// + /// The given folders will be excluded from the synchronization + /// (including associated envelopes and messages). Useful when you + /// have heavy folders that you do not want to take care of, or to + /// speed up the synchronization process. + #[arg(long, short = 'x')] + #[arg(value_name = "FOLDER", action = ArgAction::Append)] + #[arg(conflicts_with = "include_folder", conflicts_with = "all_folders")] pub exclude_folder: Vec, - #[arg( - long, - short = 'A', - conflicts_with = "include_folder", - conflicts_with = "exclude_folder" - )] + /// Synchronize all exsting folders. + #[arg(long, short = 'A')] + #[arg(conflicts_with = "include_folder", conflicts_with = "exclude_folder")] pub all_folders: bool, } diff --git a/src/cache/arg/disable.rs b/src/cache/arg/disable.rs index 54d5b8e..b1049f2 100644 --- a/src/cache/arg/disable.rs +++ b/src/cache/arg/disable.rs @@ -1,19 +1,15 @@ use clap::Parser; -/// The disable cache flag parser +/// The disable cache flag parser. #[derive(Debug, Parser)] -pub struct DisableCacheFlag { - /// Disable any sort of cache +pub struct CacheDisableFlag { + /// Disable any sort of cache. /// /// The action depends on commands it apply on. For example, when /// listing envelopes using the IMAP backend, this flag will - /// ensure that envelopes are fetched from the IMAP server and not - /// from the synchronized local Maildir. - #[arg( - long = "disable-cache", - alias = "no-cache", - name = "disable-cache", - global = true - )] + /// ensure that envelopes are fetched from the IMAP server rather + /// than the synchronized local Maildir. + #[arg(long = "disable-cache", alias = "no-cache", global = true)] + #[arg(name = "cache_disable")] pub disable: bool, } diff --git a/src/cli.rs b/src/cli.rs index a66fc68..d8f9025 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -19,14 +19,8 @@ use crate::{ }; #[derive(Parser, Debug)] -#[command( - name = "himalaya", - author, - version, - about, - propagate_version = true, - infer_subcommands = true -)] +#[command(name = "himalaya", author, version, about)] +#[command(propagate_version = true, infer_subcommands = true)] pub struct Cli { #[command(subcommand)] pub command: HimalayaCommand, @@ -83,46 +77,38 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum HimalayaCommand { - /// Manage accounts #[command(subcommand)] #[command(alias = "accounts")] Account(AccountSubcommand), - /// Manage folders #[command(subcommand)] #[command(alias = "folders")] Folder(FolderSubcommand), - /// Manage envelopes #[command(subcommand)] #[command(alias = "envelopes")] Envelope(EnvelopeSubcommand), - /// Manage flags #[command(subcommand)] #[command(alias = "flags")] Flag(FlagSubcommand), - /// Manage messages #[command(subcommand)] #[command(alias = "messages", alias = "msgs", alias = "msg")] Message(MessageSubcommand), - /// Manage templates + #[command(subcommand)] + #[command(alias = "attachments")] + Attachment(AttachmentSubcommand), + #[command(subcommand)] #[command(alias = "templates", alias = "tpls", alias = "tpl")] Template(TemplateSubcommand), - /// Manage attachments - #[command(subcommand)] - Attachment(AttachmentSubcommand), - - /// Generate manual pages to a directory #[command(arg_required_else_help = true)] #[command(alias = "manuals", alias = "mans")] Manual(ManualGenerateCommand), - /// Print completion script for a shell to stdout #[command(arg_required_else_help = true)] #[command(alias = "completions")] Completion(CompletionGenerateCommand), @@ -136,8 +122,8 @@ impl HimalayaCommand { Self::Envelope(cmd) => cmd.execute(printer, config).await, Self::Flag(cmd) => cmd.execute(printer, config).await, Self::Message(cmd) => cmd.execute(printer, config).await, - Self::Template(cmd) => cmd.execute(printer, config).await, Self::Attachment(cmd) => cmd.execute(printer, config).await, + Self::Template(cmd) => cmd.execute(printer, config).await, Self::Manual(cmd) => cmd.execute(printer).await, Self::Completion(cmd) => cmd.execute(printer).await, } diff --git a/src/completion/command.rs b/src/completion/command.rs index 67a3a7f..32b45b0 100644 --- a/src/completion/command.rs +++ b/src/completion/command.rs @@ -6,10 +6,14 @@ use std::io; use crate::{cli::Cli, printer::Printer}; -/// Print completion script for a shell to stdout +/// Print completion script for a shell to stdout. +/// +/// This command allows you to generate completion script for a given +/// shell. The script is printed to the standard output. If you want +/// to write it to a file, just use unix redirection. #[derive(Debug, Parser)] pub struct CompletionGenerateCommand { - /// Shell for which completion script should be generated for + /// Shell for which completion script should be generated for. #[arg(value_parser = value_parser!(Shell))] pub shell: Shell, } diff --git a/src/config/mod.rs b/src/config/mod.rs index 25559c1..3d96fb1 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -121,10 +121,7 @@ impl TomlConfig { match path.map(Into::into) { Some(ref path) if path.exists() => Self::from_path(path), Some(path) => Self::from_wizard(path).await, - None => match Self::first_valid_default_path() { - Some(path) => Self::from_path(&path), - None => Self::from_wizard(Self::default_path()?).await, - }, + None => Self::from_default_paths().await, } } diff --git a/src/email/envelope/arg/ids.rs b/src/email/envelope/arg/ids.rs index 6a29438..2ed73d9 100644 --- a/src/email/envelope/arg/ids.rs +++ b/src/email/envelope/arg/ids.rs @@ -1,17 +1,17 @@ use clap::Parser; -/// The envelope id argument parser +/// The envelope id argument parser. #[derive(Debug, Parser)] pub struct EnvelopeIdArg { - /// The envelope id + /// The envelope id. #[arg(value_name = "ID", required = true)] pub id: usize, } -/// The envelopes ids arguments parser +/// The envelopes ids arguments parser. #[derive(Debug, Parser)] pub struct EnvelopeIdsArgs { - /// The list of envelopes ids + /// The list of envelopes ids. #[arg(value_name = "ID", required = true)] pub ids: Vec, } diff --git a/src/email/envelope/command/list.rs b/src/email/envelope/command/list.rs index 6e9ba1b..5528e00 100644 --- a/src/email/envelope/command/list.rs +++ b/src/email/envelope/command/list.rs @@ -5,35 +5,43 @@ use log::info; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, - cache::arg::disable::DisableCacheFlag, + cache::arg::disable::CacheDisableFlag, config::TomlConfig, folder::arg::name::FolderNameOptionalArg, printer::{PrintTableOpts, Printer}, - ui::arg::max_width::MaxTableWidthFlag, + ui::arg::max_width::TableMaxWidthFlag, }; -/// List all envelopes from a folder +/// List all envelopes. +/// +/// This command allows you to list all envelopes included in the +/// given folder. #[derive(Debug, Parser)] pub struct EnvelopeListCommand { #[command(flatten)] pub folder: FolderNameOptionalArg, - /// The page number + /// The page number. + /// + /// The page number starts from 1 (which is the default). Giving a + /// page number to big will result in a out of bound error. #[arg(long, short, value_name = "NUMBER", default_value = "1")] pub page: usize, - /// The page size + /// The page size. + /// + /// Determine the amount of envelopes a page should contain. #[arg(long, short = 's', value_name = "NUMBER")] pub page_size: Option, #[command(flatten)] - pub table: MaxTableWidthFlag, + pub table: TableMaxWidthFlag, + + #[command(flatten)] + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, - - #[command(flatten)] - pub cache: DisableCacheFlag, } impl EnvelopeListCommand { diff --git a/src/email/envelope/command/mod.rs b/src/email/envelope/command/mod.rs index 8bda915..1ebf707 100644 --- a/src/email/envelope/command/mod.rs +++ b/src/email/envelope/command/mod.rs @@ -7,10 +7,14 @@ use crate::{config::TomlConfig, printer::Printer}; use self::list::EnvelopeListCommand; -/// Subcommand to manage envelopes +/// Manage envelopes. +/// +/// An envelope is a small representation of a message. It contains an +/// identifier (given by the backend), some flags as well as few +/// headers from the message itself. This subcommand allows you to +/// manage them. #[derive(Debug, Subcommand)] pub enum EnvelopeSubcommand { - /// List all envelopes from a folder #[command(alias = "lst")] List(EnvelopeListCommand), } diff --git a/src/email/envelope/flag/arg/ids_and_flags.rs b/src/email/envelope/flag/arg/ids_and_flags.rs index fe0e146..a8f9e70 100644 --- a/src/email/envelope/flag/arg/ids_and_flags.rs +++ b/src/email/envelope/flag/arg/ids_and_flags.rs @@ -2,10 +2,10 @@ use clap::Parser; use email::flag::{Flag, Flags}; use log::debug; -/// The ids and/or flags arguments parser +/// The ids and/or flags arguments parser. #[derive(Debug, Parser)] pub struct IdsAndFlagsArgs { - /// The list of ids and/or flags + /// The list of ids and/or flags. /// /// Every argument that can be parsed as an integer is considered /// an id, otherwise it is considered as a flag. diff --git a/src/email/envelope/flag/command/add.rs b/src/email/envelope/flag/command/add.rs index 63d7dd3..c979993 100644 --- a/src/email/envelope/flag/command/add.rs +++ b/src/email/envelope/flag/command/add.rs @@ -5,14 +5,17 @@ use log::info; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, - cache::arg::disable::DisableCacheFlag, + cache::arg::disable::CacheDisableFlag, config::TomlConfig, flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, folder::arg::name::FolderNameArg, printer::Printer, }; -/// Add flag(s) to an envelope +/// Add flag(s) to an envelope. +/// +/// This command allows you to attach the given flag(s) to the given +/// envelope(s). #[derive(Debug, Parser)] pub struct FlagAddCommand { #[command(flatten)] @@ -22,10 +25,10 @@ pub struct FlagAddCommand { pub args: IdsAndFlagsArgs, #[command(flatten)] - pub account: AccountNameFlag, + pub cache: CacheDisableFlag, #[command(flatten)] - pub cache: DisableCacheFlag, + pub account: AccountNameFlag, } impl FlagAddCommand { diff --git a/src/email/envelope/flag/command/mod.rs b/src/email/envelope/flag/command/mod.rs index 05c7a2f..87d1633 100644 --- a/src/email/envelope/flag/command/mod.rs +++ b/src/email/envelope/flag/command/mod.rs @@ -9,20 +9,22 @@ use crate::{config::TomlConfig, printer::Printer}; use self::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand}; -/// Subcommand to manage flags +/// Manage flags. +/// +/// A flag is a tag associated to an envelope. Existing flags are +/// seen, answered, flagged, deleted, draft. Other flags are +/// considered custom, which are not always supported (the +/// synchronization does not take care of them yet). #[derive(Debug, Subcommand)] pub enum FlagSubcommand { - /// Add flag(s) to an envelope #[command(arg_required_else_help = true)] #[command(alias = "create")] Add(FlagAddCommand), - /// Replace flag(s) of an envelope #[command(arg_required_else_help = true)] #[command(aliases = ["update", "change", "replace"])] Set(FlagSetCommand), - /// Remove flag(s) from an envelope #[command(arg_required_else_help = true)] #[command(aliases = ["rm", "delete", "del"])] Remove(FlagRemoveCommand), diff --git a/src/email/envelope/flag/command/remove.rs b/src/email/envelope/flag/command/remove.rs index 59583f5..807811d 100644 --- a/src/email/envelope/flag/command/remove.rs +++ b/src/email/envelope/flag/command/remove.rs @@ -5,14 +5,17 @@ use log::info; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, - cache::arg::disable::DisableCacheFlag, + cache::arg::disable::CacheDisableFlag, config::TomlConfig, flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, folder::arg::name::FolderNameArg, printer::Printer, }; -/// Remove flag(s) from an envelope +/// Remove flag(s) from an envelope. +/// +/// This command allows you to remove the given flag(s) from the given +/// envelope(s). #[derive(Debug, Parser)] pub struct FlagRemoveCommand { #[command(flatten)] @@ -22,10 +25,10 @@ pub struct FlagRemoveCommand { pub args: IdsAndFlagsArgs, #[command(flatten)] - pub account: AccountNameFlag, + pub cache: CacheDisableFlag, #[command(flatten)] - pub cache: DisableCacheFlag, + pub account: AccountNameFlag, } impl FlagRemoveCommand { diff --git a/src/email/envelope/flag/command/set.rs b/src/email/envelope/flag/command/set.rs index cb04482..8c7343a 100644 --- a/src/email/envelope/flag/command/set.rs +++ b/src/email/envelope/flag/command/set.rs @@ -5,14 +5,17 @@ use log::info; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, - cache::arg::disable::DisableCacheFlag, + cache::arg::disable::CacheDisableFlag, config::TomlConfig, flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, folder::arg::name::FolderNameArg, printer::Printer, }; -/// Replace flag(s) of an envelope +/// Replace flag(s) of an envelope. +/// +/// This command allows you to replace existing flags of the given +/// envelope(s) with the given flag(s). #[derive(Debug, Parser)] pub struct FlagSetCommand { #[command(flatten)] @@ -22,10 +25,10 @@ pub struct FlagSetCommand { pub args: IdsAndFlagsArgs, #[command(flatten)] - pub account: AccountNameFlag, + pub cache: CacheDisableFlag, #[command(flatten)] - pub cache: DisableCacheFlag, + pub account: AccountNameFlag, } impl FlagSetCommand { diff --git a/src/email/message/arg/body.rs b/src/email/message/arg/body.rs index 36ca6e0..f7ec4a1 100644 --- a/src/email/message/arg/body.rs +++ b/src/email/message/arg/body.rs @@ -1,22 +1,22 @@ use clap::Parser; use std::ops::Deref; -/// The raw message body argument parser +/// The raw message body argument parser. #[derive(Debug, Parser)] -pub struct BodyRawArg { - /// Prefill the template with a custom body - #[arg(raw = true, required = false)] - #[arg(name = "body-raw", value_delimiter = ' ')] +pub struct MessageRawBodyArg { + /// Prefill the template with a custom body. + #[arg(trailing_var_arg = true)] + #[arg(name = "body-raw")] pub raw: Vec, } -impl BodyRawArg { +impl MessageRawBodyArg { pub fn raw(self) -> String { self.raw.join(" ").replace("\r", "").replace("\n", "\r\n") } } -impl Deref for BodyRawArg { +impl Deref for MessageRawBodyArg { type Target = Vec; fn deref(&self) -> &Self::Target { diff --git a/src/email/message/arg/header.rs b/src/email/message/arg/header.rs index 0f4b34d..29e4336 100644 --- a/src/email/message/arg/header.rs +++ b/src/email/message/arg/header.rs @@ -1,9 +1,9 @@ use clap::Parser; -/// The envelope id argument parser +/// The envelope id argument parser. #[derive(Debug, Parser)] pub struct HeaderRawArgs { - /// Prefill the template with custom headers + /// Prefill the template with custom headers. /// /// A raw header should follow the pattern KEY:VAL. #[arg(long = "header", short = 'H', required = false)] diff --git a/src/email/message/arg/reply.rs b/src/email/message/arg/reply.rs index 5855001..20491dc 100644 --- a/src/email/message/arg/reply.rs +++ b/src/email/message/arg/reply.rs @@ -1,9 +1,12 @@ use clap::Parser; -/// The reply to all argument parser +/// The reply to all argument parser. #[derive(Debug, Parser)] pub struct MessageReplyAllArg { - /// Reply to all recipients + /// Reply to all recipients. + /// + /// This argument will add all recipients for the To and Cc + /// headers. #[arg(long, short = 'A')] pub all: bool, } diff --git a/src/email/message/attachment/command/download.rs b/src/email/message/attachment/command/download.rs index e0b7a1d..c2dad39 100644 --- a/src/email/message/attachment/command/download.rs +++ b/src/email/message/attachment/command/download.rs @@ -5,12 +5,15 @@ use std::fs; use uuid::Uuid; use crate::{ - account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameArg, printer::Printer, }; -/// Download attachments of a message +/// Download all attachments for the given message. +/// +/// This command allows you to download all attachments found for the +/// given message to your downloads directory. #[derive(Debug, Parser)] pub struct AttachmentDownloadCommand { #[command(flatten)] @@ -20,7 +23,7 @@ pub struct AttachmentDownloadCommand { pub envelopes: EnvelopeIdsArgs, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, diff --git a/src/email/message/attachment/command/mod.rs b/src/email/message/attachment/command/mod.rs index 3770ae0..5ae2c61 100644 --- a/src/email/message/attachment/command/mod.rs +++ b/src/email/message/attachment/command/mod.rs @@ -7,10 +7,13 @@ use crate::{config::TomlConfig, printer::Printer}; use self::download::AttachmentDownloadCommand; -/// Subcommand dedicated to attachments +/// Manage attachments. +/// +/// A message body can be composed of multiple MIME parts. An +/// attachment is the representation of a binary part of a message +/// body. #[derive(Debug, Subcommand)] pub enum AttachmentSubcommand { - /// Download all attachments of one or more messages #[command(arg_required_else_help = true)] Download(AttachmentDownloadCommand), } diff --git a/src/email/message/command/copy.rs b/src/email/message/command/copy.rs index 07d4b00..6ffb2ae 100644 --- a/src/email/message/command/copy.rs +++ b/src/email/message/command/copy.rs @@ -5,14 +5,14 @@ use log::info; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, - cache::arg::disable::DisableCacheFlag, + cache::arg::disable::CacheDisableFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::{SourceFolderNameArg, TargetFolderNameArg}, printer::Printer, }; -/// Copy a message from a source folder to a target folder +/// Copy a message from a source folder to a target folder. #[derive(Debug, Parser)] pub struct MessageCopyCommand { #[command(flatten)] @@ -25,7 +25,7 @@ pub struct MessageCopyCommand { pub envelopes: EnvelopeIdsArgs, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, diff --git a/src/email/message/command/delete.rs b/src/email/message/command/delete.rs index 55dece0..8b67b91 100644 --- a/src/email/message/command/delete.rs +++ b/src/email/message/command/delete.rs @@ -3,12 +3,17 @@ use clap::Parser; use log::info; use crate::{ - account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameArg, printer::Printer, }; -/// Delete a message from a folder +/// Mark as deleted a message from a folder. +/// +/// This command does not really delete the message: if the given +/// folder points to the trash folder, it adds the "deleted" flag to +/// its envelope, otherwise it moves it to the trash folder. Only the +/// expunge folder command truly deletes messages. #[derive(Debug, Parser)] pub struct MessageDeleteCommand { #[command(flatten)] @@ -18,7 +23,7 @@ pub struct MessageDeleteCommand { pub envelopes: EnvelopeIdsArgs, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, diff --git a/src/email/message/command/forward.rs b/src/email/message/command/forward.rs index f6199cc..4dbf61f 100644 --- a/src/email/message/command/forward.rs +++ b/src/email/message/command/forward.rs @@ -7,16 +7,21 @@ use std::io::{self, BufRead}; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, - cache::arg::disable::DisableCacheFlag, + cache::arg::disable::CacheDisableFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg, folder::arg::name::FolderNameArg, - message::arg::{body::BodyRawArg, header::HeaderRawArgs}, + message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs}, printer::Printer, ui::editor, }; -/// Forward a new message +/// Forward a message. +/// +/// This command allows you to forward the given message using the +/// editor defined in your environment variable $EDITOR. When the +/// edition process finishes, you can choose between saving or sending +/// the final message. #[derive(Debug, Parser)] pub struct MessageForwardCommand { #[command(flatten)] @@ -25,18 +30,14 @@ pub struct MessageForwardCommand { #[command(flatten)] pub envelope: EnvelopeIdArg, - /// Forward to all recipients - #[arg(long, short = 'A')] - pub all: bool, - #[command(flatten)] pub headers: HeaderRawArgs, #[command(flatten)] - pub body: BodyRawArg, + pub body: MessageRawBodyArg, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, diff --git a/src/email/message/command/mailto.rs b/src/email/message/command/mailto.rs index 4041317..c2daed4 100644 --- a/src/email/message/command/mailto.rs +++ b/src/email/message/command/mailto.rs @@ -1,20 +1,30 @@ use anyhow::Result; use clap::Parser; -use log::info; +use log::{debug, info}; use mail_builder::MessageBuilder; use url::Url; use crate::{backend::Backend, config::TomlConfig, printer::Printer, ui::editor}; -/// Parse and edit a message from a mailto URL string +/// Parse and edit a message from a mailto URL string. +/// +/// This command allows you to edit a message from the mailto format +/// using the editor defined in your environment variable +/// $EDITOR. When the edition process finishes, you can choose between +/// saving or sending the final message. #[derive(Debug, Parser)] pub struct MessageMailtoCommand { - /// The mailto url + /// The mailto url. #[arg()] pub url: Url, } impl MessageMailtoCommand { + pub fn new(url: &str) -> Result { + let url = Url::parse(url)?; + Ok(Self { url }) + } + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { info!("executing message mailto command"); @@ -23,17 +33,27 @@ impl MessageMailtoCommand { let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; let mut builder = MessageBuilder::new().to(self.url.path()); + let mut body = String::new(); for (key, val) in self.url.query_pairs() { match key.to_lowercase().as_bytes() { b"cc" => builder = builder.cc(val.to_string()), b"bcc" => builder = builder.bcc(val.to_string()), b"subject" => builder = builder.subject(val), - b"body" => builder = builder.text_body(val), + b"body" => body += &val, _ => (), } } + match account_config.signature() { + Ok(Some(ref signature)) => builder = builder.text_body(body + "\n\n" + signature), + Ok(None) => builder = builder.text_body(body), + Err(err) => { + debug!("cannot add signature to mailto message, skipping it: {err}"); + debug!("{err:?}"); + } + } + let tpl = account_config .generate_tpl_interpreter() .with_show_only_headers(account_config.email_writing_headers()) diff --git a/src/email/message/command/mod.rs b/src/email/message/command/mod.rs index dc2c5d5..53606b2 100644 --- a/src/email/message/command/mod.rs +++ b/src/email/message/command/mod.rs @@ -21,47 +21,42 @@ use self::{ write::MessageWriteCommand, }; -/// Subcommand to manage messages +/// Manage messages. +/// +/// A message is the content of an email. It is composed of headers +/// (located at the top of the message) and a body (located at the +/// bottom of the message). Both are separated by two new lines. This +/// subcommand allows you to manage them. #[derive(Debug, Subcommand)] pub enum MessageSubcommand { - /// Read a message #[command(arg_required_else_help = true)] Read(MessageReadCommand), - /// Write a new message - #[command(alias = "new", alias = "compose")] + #[command(alias = "add", alias = "create", alias = "new", alias = "compose")] Write(MessageWriteCommand), - /// Reply to a message #[command()] Reply(MessageReplyCommand), - /// Forward a message #[command(alias = "fwd")] Forward(MessageForwardCommand), - /// Parse and edit a message from a mailto URL string #[command()] Mailto(MessageMailtoCommand), - /// Save a message to a folder #[command(arg_required_else_help = true)] #[command(alias = "add", alias = "create")] Save(MessageSaveCommand), - /// Send a message #[command(arg_required_else_help = true)] Send(MessageSendCommand), - /// Copy a message from a source folder to a target folder #[command(arg_required_else_help = true)] Copy(MessageCopyCommand), - /// Move a message from a source folder to a target folder #[command(arg_required_else_help = true)] Move(MessageMoveCommand), - /// Delete a message from a folder #[command(arg_required_else_help = true)] Delete(MessageDeleteCommand), } diff --git a/src/email/message/command/move_.rs b/src/email/message/command/move_.rs index 76d2744..728116a 100644 --- a/src/email/message/command/move_.rs +++ b/src/email/message/command/move_.rs @@ -5,14 +5,14 @@ use log::info; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, - cache::arg::disable::DisableCacheFlag, + cache::arg::disable::CacheDisableFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::{SourceFolderNameArg, TargetFolderNameArg}, printer::Printer, }; -/// Move a message from a source folder to a target folder +/// Move a message from a source folder to a target folder. #[derive(Debug, Parser)] pub struct MessageMoveCommand { #[command(flatten)] @@ -25,7 +25,7 @@ pub struct MessageMoveCommand { pub envelopes: EnvelopeIdsArgs, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, diff --git a/src/email/message/command/read.rs b/src/email/message/command/read.rs index 373547a..53d1515 100644 --- a/src/email/message/command/read.rs +++ b/src/email/message/command/read.rs @@ -4,12 +4,14 @@ use log::info; use mml::message::FilterParts; use crate::{ - account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameArg, printer::Printer, }; -/// Read a message from a folder +/// Read a message. +/// +/// This command allows you to read a message. #[derive(Debug, Parser)] pub struct MessageReadCommand { #[command(flatten)] @@ -18,18 +20,18 @@ pub struct MessageReadCommand { #[command(flatten)] pub envelopes: EnvelopeIdsArgs, - /// Read the raw version of the message + /// Read the raw version of the given message. /// - /// The raw message represents the message as it is on the - /// backend, unedited: not decoded nor decrypted. This is useful - /// for debugging faulty messages, but also for + /// The raw message represents the headers and the body as it is + /// on the backend, unedited: not decoded nor decrypted. This is + /// useful for debugging faulty messages, but also for /// saving/sending/transfering messages. #[arg(long, short)] #[arg(conflicts_with = "no_headers")] #[arg(conflicts_with = "headers")] pub raw: bool, - /// Read only body of text/html parts + /// Read only body of text/html parts. /// /// This argument is useful when you need to read the HTML version /// of a message. Combined with --no-headers, you can write it to @@ -38,7 +40,7 @@ pub struct MessageReadCommand { #[arg(conflicts_with = "raw")] pub html: bool, - /// Read only the body of the message + /// Read only the body of the message. /// /// All headers will be removed from the message. #[arg(long)] @@ -47,7 +49,7 @@ pub struct MessageReadCommand { pub no_headers: bool, /// List of headers that should be visible at the top of the - /// message + /// message. /// /// If a given header is not found in the message, it will not be /// visible. If no header is given, defaults to the one set up in @@ -58,7 +60,7 @@ pub struct MessageReadCommand { pub headers: Vec, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, diff --git a/src/email/message/command/reply.rs b/src/email/message/command/reply.rs index c95c537..dfcbbbe 100644 --- a/src/email/message/command/reply.rs +++ b/src/email/message/command/reply.rs @@ -8,16 +8,21 @@ use std::io::{self, BufRead}; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, - cache::arg::disable::DisableCacheFlag, + cache::arg::disable::CacheDisableFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg, folder::arg::name::FolderNameArg, - message::arg::{body::BodyRawArg, header::HeaderRawArgs, reply::MessageReplyAllArg}, + message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg}, printer::Printer, ui::editor, }; -/// Reply a new message +/// Reply to a message. +/// +/// This command allows you to reply to the given message using the +/// editor defined in your environment variable $EDITOR. When the +/// edition process finishes, you can choose between saving or sending +/// the final message. #[derive(Debug, Parser)] pub struct MessageReplyCommand { #[command(flatten)] @@ -33,10 +38,10 @@ pub struct MessageReplyCommand { pub headers: HeaderRawArgs, #[command(flatten)] - pub body: BodyRawArg, + pub body: MessageRawBodyArg, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, diff --git a/src/email/message/command/save.rs b/src/email/message/command/save.rs index afd3813..637ddeb 100644 --- a/src/email/message/command/save.rs +++ b/src/email/message/command/save.rs @@ -5,22 +5,24 @@ use log::info; use std::io::{self, BufRead}; use crate::{ - account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, - config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer, + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, + config::TomlConfig, folder::arg::name::FolderNameArg, message::arg::body::MessageRawBodyArg, + printer::Printer, }; -/// Save a message to a folder +/// Save a message to a folder. +/// +/// This command allows you to add a raw message to the given folder. #[derive(Debug, Parser)] pub struct MessageSaveCommand { #[command(flatten)] pub folder: FolderNameArg, - /// The raw message to save - #[arg(value_name = "MESSAGE", raw = true)] - pub raw: String, + #[command(flatten)] + pub body: MessageRawBodyArg, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, @@ -33,7 +35,6 @@ impl MessageSaveCommand { let folder = &self.folder.name; let account = self.account.name.as_ref().map(String::as_str); let cache = self.cache.disable; - let raw_msg = &self.raw; let (toml_account_config, account_config) = config.clone().into_account_configs(account, cache)?; @@ -41,8 +42,8 @@ impl MessageSaveCommand { let is_tty = atty::is(Stream::Stdin); let is_json = printer.is_json(); - let raw_email = if is_tty || is_json { - raw_msg.replace("\r", "").replace("\n", "\r\n") + let msg = if is_tty || is_json { + self.body.raw() } else { io::stdin() .lock() @@ -52,9 +53,7 @@ impl MessageSaveCommand { .join("\r\n") }; - backend - .add_raw_message(folder, raw_email.as_bytes()) - .await?; + backend.add_raw_message(folder, msg.as_bytes()).await?; printer.print(format!("Message successfully saved to {folder}!")) } diff --git a/src/email/message/command/send.rs b/src/email/message/command/send.rs index d83e6e5..9f176b1 100644 --- a/src/email/message/command/send.rs +++ b/src/email/message/command/send.rs @@ -6,19 +6,21 @@ use log::info; use std::io::{self, BufRead}; use crate::{ - account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, - config::TomlConfig, printer::Printer, + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, + config::TomlConfig, message::arg::body::MessageRawBodyArg, printer::Printer, }; -/// Send a message from a folder +/// Send a message. +/// +/// This command allows you to send a raw message and to save a copy +/// to your send folder. #[derive(Debug, Parser)] pub struct MessageSendCommand { - /// The raw message to send - #[arg(value_name = "MESSAGE", raw = true)] - pub raw: String, + #[command(flatten)] + pub body: MessageRawBodyArg, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, @@ -30,7 +32,6 @@ impl MessageSendCommand { let account = self.account.name.as_ref().map(String::as_str); let cache = self.cache.disable; - let raw_msg = &self.raw; let (toml_account_config, account_config) = config.clone().into_account_configs(account, cache)?; @@ -39,8 +40,8 @@ impl MessageSendCommand { let is_tty = atty::is(Stream::Stdin); let is_json = printer.is_json(); - let raw_email = if is_tty || is_json { - raw_msg.replace("\r", "").replace("\n", "\r\n") + let msg = if is_tty || is_json { + self.body.raw() } else { io::stdin() .lock() @@ -50,11 +51,11 @@ impl MessageSendCommand { .join("\r\n") }; - backend.send_raw_message(raw_email.as_bytes()).await?; + backend.send_raw_message(msg.as_bytes()).await?; if account_config.email_sending_save_copy.unwrap_or_default() { backend - .add_raw_message_with_flag(&folder, raw_email.as_bytes(), Flag::Seen) + .add_raw_message_with_flag(&folder, msg.as_bytes(), Flag::Seen) .await?; printer.print(format!("Message successfully sent and saved to {folder}!")) diff --git a/src/email/message/command/write.rs b/src/email/message/command/write.rs index c0d5aca..38b1c28 100644 --- a/src/email/message/command/write.rs +++ b/src/email/message/command/write.rs @@ -8,24 +8,29 @@ use std::io::{self, BufRead}; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, - cache::arg::disable::DisableCacheFlag, + cache::arg::disable::CacheDisableFlag, config::TomlConfig, - message::arg::{body::BodyRawArg, header::HeaderRawArgs}, + message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs}, printer::Printer, ui::editor, }; -/// Write a new message +/// Write a new message. +/// +/// This command allows you to write a new message using the editor +/// defined in your environment variable $EDITOR. When the edition +/// process finishes, you can choose between saving or sending the +/// final message. #[derive(Debug, Parser)] pub struct MessageWriteCommand { #[command(flatten)] pub headers: HeaderRawArgs, #[command(flatten)] - pub body: BodyRawArg, + pub body: MessageRawBodyArg, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, diff --git a/src/email/message/template/arg/body.rs b/src/email/message/template/arg/body.rs new file mode 100644 index 0000000..fafd455 --- /dev/null +++ b/src/email/message/template/arg/body.rs @@ -0,0 +1,25 @@ +use clap::Parser; +use std::ops::Deref; + +/// The raw template body argument parser. +#[derive(Debug, Parser)] +pub struct TemplateRawBodyArg { + /// Prefill the template with a custom body. + #[arg(trailing_var_arg = true)] + #[arg(name = "body-raw")] + pub raw: Vec, +} + +impl TemplateRawBodyArg { + pub fn raw(self) -> String { + self.raw.join(" ").replace("\r", "") + } +} + +impl Deref for TemplateRawBodyArg { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.raw + } +} diff --git a/src/email/message/template/arg/mod.rs b/src/email/message/template/arg/mod.rs new file mode 100644 index 0000000..81f6efd --- /dev/null +++ b/src/email/message/template/arg/mod.rs @@ -0,0 +1 @@ +pub mod body; diff --git a/src/email/message/template/command/forward.rs b/src/email/message/template/command/forward.rs index b5950d1..c09529d 100644 --- a/src/email/message/template/command/forward.rs +++ b/src/email/message/template/command/forward.rs @@ -5,15 +5,19 @@ use log::info; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, - cache::arg::disable::DisableCacheFlag, + cache::arg::disable::CacheDisableFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg, folder::arg::name::FolderNameArg, - message::arg::{body::BodyRawArg, header::HeaderRawArgs}, + message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs}, printer::Printer, }; -/// Generate a forward message template +/// Generate a template for forwarding a message. +/// +/// The generated template is prefilled with your email in a From +/// header as well as your signature. The forwarded message is also +/// prefilled in the body of the template, prefixed by a separator. #[derive(Debug, Parser)] pub struct TemplateForwardCommand { #[command(flatten)] @@ -26,10 +30,10 @@ pub struct TemplateForwardCommand { pub headers: HeaderRawArgs, #[command(flatten)] - pub body: BodyRawArg, + pub body: MessageRawBodyArg, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, diff --git a/src/email/message/template/command/mod.rs b/src/email/message/template/command/mod.rs index 202fdf5..eed6ca7 100644 --- a/src/email/message/template/command/mod.rs +++ b/src/email/message/template/command/mod.rs @@ -14,28 +14,31 @@ use self::{ send::TemplateSendCommand, write::TemplateWriteCommand, }; -/// Subcommand to manage templates +/// Manage templates. +/// +/// A template is an editable version of a message (headers + +/// body). It uses a specific language called MML that allows you to +/// attach file or encrypt content. This subcommand allows you manage +/// them. +/// +/// You can learn more about MML at +/// . #[derive(Debug, Subcommand)] pub enum TemplateSubcommand { - /// Write a new template - #[command(alias = "new", alias = "compose")] + #[command(alias = "create", alias = "new", alias = "compose")] Write(TemplateWriteCommand), - /// Reply to a template - #[command()] + #[command(arg_required_else_help = true)] Reply(TemplateReplyCommand), - /// Generate a template for forwarding an email + #[command(arg_required_else_help = true)] #[command(alias = "fwd")] Forward(TemplateForwardCommand), - /// Save a template to a folder - #[command(arg_required_else_help = true)] - #[command(alias = "add", alias = "create")] + #[command(alias = "add")] Save(TemplateSaveCommand), - /// Send a template - #[command(arg_required_else_help = true)] + #[command()] Send(TemplateSendCommand), } diff --git a/src/email/message/template/command/reply.rs b/src/email/message/template/command/reply.rs index 80f75b4..fe42af5 100644 --- a/src/email/message/template/command/reply.rs +++ b/src/email/message/template/command/reply.rs @@ -5,15 +5,20 @@ use log::info; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, - cache::arg::disable::DisableCacheFlag, + cache::arg::disable::CacheDisableFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg, folder::arg::name::FolderNameArg, - message::arg::{body::BodyRawArg, header::HeaderRawArgs, reply::MessageReplyAllArg}, + message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg}, printer::Printer, }; -/// Generate a reply message template +/// Generate a template for replying to a message. +/// +/// The generated template is prefilled with your email in a From +/// header as well as your signature. The replied message is also +/// prefilled in the body of the template, with all lines prefixed by +/// the symbol greater than ">". #[derive(Debug, Parser)] pub struct TemplateReplyCommand { #[command(flatten)] @@ -29,10 +34,10 @@ pub struct TemplateReplyCommand { pub headers: HeaderRawArgs, #[command(flatten)] - pub body: BodyRawArg, + pub body: MessageRawBodyArg, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, diff --git a/src/email/message/template/command/save.rs b/src/email/message/template/command/save.rs index c99fc52..4253468 100644 --- a/src/email/message/template/command/save.rs +++ b/src/email/message/template/command/save.rs @@ -6,22 +6,27 @@ use mml::MmlCompilerBuilder; use std::io::{self, BufRead}; use crate::{ - account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, - config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer, + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, + config::TomlConfig, email::template::arg::body::TemplateRawBodyArg, + folder::arg::name::FolderNameArg, printer::Printer, }; -/// Save a template to a folder +/// Save a template to a folder. +/// +/// This command allows you to save a template to the given +/// folder. The template is compiled into a MIME message before being +/// saved to the folder. If you want to save a raw message, use the +/// message save command instead. #[derive(Debug, Parser)] pub struct TemplateSaveCommand { #[command(flatten)] pub folder: FolderNameArg, - /// The raw template to save - #[arg(raw = true, value_delimiter = ' ')] - pub raw: Vec, + #[command(flatten)] + pub body: TemplateRawBodyArg, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, @@ -42,7 +47,7 @@ impl TemplateSaveCommand { let is_tty = atty::is(Stream::Stdin); let is_json = printer.is_json(); let tpl = if is_tty || is_json { - self.raw.join(" ").replace("\r", "") + self.body.raw() } else { io::stdin() .lock() diff --git a/src/email/message/template/command/send.rs b/src/email/message/template/command/send.rs index ff36745..4b74b95 100644 --- a/src/email/message/template/command/send.rs +++ b/src/email/message/template/command/send.rs @@ -7,19 +7,23 @@ use mml::MmlCompilerBuilder; use std::io::{self, BufRead}; use crate::{ - account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, - config::TomlConfig, printer::Printer, + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, + config::TomlConfig, email::template::arg::body::TemplateRawBodyArg, printer::Printer, }; -/// Send a template +/// Send a template. +/// +/// This command allows you to send a template and save a copy to the +/// sent folder. The template is compiled into a MIME message before +/// being sent. If you want to send a raw message, use the message +/// send command instead. #[derive(Debug, Parser)] pub struct TemplateSendCommand { - /// The raw template to save - #[arg(raw = true, value_delimiter = ' ')] - pub raw: Vec, + #[command(flatten)] + pub body: TemplateRawBodyArg, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, @@ -40,7 +44,7 @@ impl TemplateSendCommand { let is_tty = atty::is(Stream::Stdin); let is_json = printer.is_json(); let tpl = if is_tty || is_json { - self.raw.join(" ").replace("\r", "") + self.body.raw() } else { io::stdin() .lock() diff --git a/src/email/message/template/command/write.rs b/src/email/message/template/command/write.rs index 72b3168..cf22bd4 100644 --- a/src/email/message/template/command/write.rs +++ b/src/email/message/template/command/write.rs @@ -4,24 +4,25 @@ use email::message::Message; use log::info; use crate::{ - account::arg::name::AccountNameFlag, - cache::arg::disable::DisableCacheFlag, - config::TomlConfig, - message::arg::{body::BodyRawArg, header::HeaderRawArgs}, + account::arg::name::AccountNameFlag, cache::arg::disable::CacheDisableFlag, config::TomlConfig, + email::template::arg::body::TemplateRawBodyArg, message::arg::header::HeaderRawArgs, printer::Printer, }; -/// Write a new template +/// Generate a template for writing a new message from scratch. +/// +/// The generated template is prefilled with your email in a From +/// header as well as your signature. #[derive(Debug, Parser)] pub struct TemplateWriteCommand { #[command(flatten)] pub headers: HeaderRawArgs, #[command(flatten)] - pub body: BodyRawArg, + pub body: TemplateRawBodyArg, #[command(flatten)] - pub cache: DisableCacheFlag, + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, diff --git a/src/email/message/template/mod.rs b/src/email/message/template/mod.rs index 9fe7961..1a3a73a 100644 --- a/src/email/message/template/mod.rs +++ b/src/email/message/template/mod.rs @@ -1 +1,2 @@ +pub mod arg; pub mod command; diff --git a/src/folder/arg/name.rs b/src/folder/arg/name.rs index 730c1a3..15307b8 100644 --- a/src/folder/arg/name.rs +++ b/src/folder/arg/name.rs @@ -1,34 +1,34 @@ use clap::Parser; use email::account::config::DEFAULT_INBOX_FOLDER; -/// The folder name argument parser +/// The folder name argument parser. #[derive(Debug, Parser)] pub struct FolderNameArg { - /// The name of the folder - #[arg(name = "folder-name", value_name = "FOLDER")] + /// The name of the folder. + #[arg(name = "folder_name", value_name = "FOLDER")] pub name: String, } -/// The optional folder name argument parser +/// The optional folder name argument parser. #[derive(Debug, Parser)] pub struct FolderNameOptionalArg { - /// The name of the folder - #[arg(name = "folder-name", value_name = "FOLDER", default_value = DEFAULT_INBOX_FOLDER)] + /// The name of the folder. + #[arg(name = "folder_name", value_name = "FOLDER", default_value = DEFAULT_INBOX_FOLDER)] pub name: String, } -/// The source folder name argument parser +/// The source folder name argument parser. #[derive(Debug, Parser)] pub struct SourceFolderNameArg { - /// The name of the source folder + /// The name of the source folder. #[arg(name = "from-folder-name", value_name = "FROM")] pub name: String, } -/// The target folder name argument parser +/// The target folder name argument parser. #[derive(Debug, Parser)] pub struct TargetFolderNameArg { - /// The name of the target folder + /// The name of the target folder. #[arg(name = "to-folder-name", value_name = "TO")] pub name: String, } diff --git a/src/folder/command/create.rs b/src/folder/command/create.rs index 02c409e..3e27138 100644 --- a/src/folder/command/create.rs +++ b/src/folder/command/create.rs @@ -3,21 +3,24 @@ use clap::Parser; use log::info; use crate::{ - account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer, }; -/// Create a new folder +/// Create a new folder. +/// +/// This command allows you to create a new folder using the given +/// name. #[derive(Debug, Parser)] pub struct FolderCreateCommand { #[command(flatten)] pub folder: FolderNameArg, #[command(flatten)] - pub account: AccountNameFlag, + pub cache: CacheDisableFlag, #[command(flatten)] - pub cache: DisableCacheFlag, + pub account: AccountNameFlag, } impl FolderCreateCommand { diff --git a/src/folder/command/delete.rs b/src/folder/command/delete.rs index e0786db..14a500a 100644 --- a/src/folder/command/delete.rs +++ b/src/folder/command/delete.rs @@ -5,24 +5,24 @@ use log::info; use std::process; use crate::{ - account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer, }; -/// Delete a folder +/// Delete a folder. /// -/// All emails from a given folder are definitely deleted. The folder -/// is also deleted after execution of the command. +/// All emails from the given folder are definitely deleted. The +/// folder is also deleted after execution of the command. #[derive(Debug, Parser)] pub struct FolderDeleteCommand { #[command(flatten)] pub folder: FolderNameArg, #[command(flatten)] - pub account: AccountNameFlag, + pub cache: CacheDisableFlag, #[command(flatten)] - pub cache: DisableCacheFlag, + pub account: AccountNameFlag, } impl FolderDeleteCommand { diff --git a/src/folder/command/expunge.rs b/src/folder/command/expunge.rs index ac2c0b3..a6df0ca 100644 --- a/src/folder/command/expunge.rs +++ b/src/folder/command/expunge.rs @@ -3,14 +3,14 @@ use clap::Parser; use log::info; use crate::{ - account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer, }; -/// Expunge a folder +/// Expunge a folder. /// /// The concept of expunging is similar to the IMAP one: it definitely -/// deletes emails from a given folder that contain the "deleted" +/// deletes emails from the given folder that contain the "deleted" /// flag. #[derive(Debug, Parser)] pub struct FolderExpungeCommand { @@ -18,10 +18,10 @@ pub struct FolderExpungeCommand { pub folder: FolderNameArg, #[command(flatten)] - pub account: AccountNameFlag, + pub cache: CacheDisableFlag, #[command(flatten)] - pub cache: DisableCacheFlag, + pub account: AccountNameFlag, } impl FolderExpungeCommand { diff --git a/src/folder/command/list.rs b/src/folder/command/list.rs index 03e0a65..8809b92 100644 --- a/src/folder/command/list.rs +++ b/src/folder/command/list.rs @@ -5,24 +5,26 @@ use log::info; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, - cache::arg::disable::DisableCacheFlag, + cache::arg::disable::CacheDisableFlag, config::TomlConfig, folder::Folders, printer::{PrintTableOpts, Printer}, - ui::arg::max_width::MaxTableWidthFlag, + ui::arg::max_width::TableMaxWidthFlag, }; -/// List all folders +/// List all folders. +/// +/// This command allows you to list all exsting folders. #[derive(Debug, Parser)] pub struct FolderListCommand { #[command(flatten)] - pub table: MaxTableWidthFlag, + pub table: TableMaxWidthFlag, + + #[command(flatten)] + pub cache: CacheDisableFlag, #[command(flatten)] pub account: AccountNameFlag, - - #[command(flatten)] - pub cache: DisableCacheFlag, } impl FolderListCommand { diff --git a/src/folder/command/mod.rs b/src/folder/command/mod.rs index 54868ae..29959aa 100644 --- a/src/folder/command/mod.rs +++ b/src/folder/command/mod.rs @@ -14,26 +14,24 @@ use self::{ list::FolderListCommand, purge::FolderPurgeCommand, }; -/// Subcommand to manage accounts +/// Manage folders. +/// +/// A folder (AKA mailbox, or directory) contains envelopes and +/// messages. This subcommand allows you to manage them. #[derive(Debug, Subcommand)] pub enum FolderSubcommand { - /// Create a new folder - #[command(alias = "add")] + #[command(alias = "add", alias = "new")] Create(FolderCreateCommand), - /// List all folders #[command(alias = "lst")] List(FolderListCommand), - /// Expunge a folder #[command()] Expunge(FolderExpungeCommand), - /// Purge a folder #[command()] Purge(FolderPurgeCommand), - /// Delete a folder #[command(alias = "remove", alias = "rm")] Delete(FolderDeleteCommand), } diff --git a/src/folder/command/purge.rs b/src/folder/command/purge.rs index 3d870f7..9f97648 100644 --- a/src/folder/command/purge.rs +++ b/src/folder/command/purge.rs @@ -5,24 +5,24 @@ use log::info; use std::process; use crate::{ - account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer, }; -/// Purge a folder +/// Purge a folder. /// -/// All emails from a given folder are definitely deleted. The purged -/// folder will remain empty after executing of the command. +/// All emails from the given folder are definitely deleted. The +/// purged folder will remain empty after execution of the command. #[derive(Debug, Parser)] pub struct FolderPurgeCommand { #[command(flatten)] pub folder: FolderNameArg, #[command(flatten)] - pub account: AccountNameFlag, + pub cache: CacheDisableFlag, #[command(flatten)] - pub cache: DisableCacheFlag, + pub account: AccountNameFlag, } impl FolderPurgeCommand { diff --git a/src/main.rs b/src/main.rs index ea150eb..71291b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,10 @@ use anyhow::Result; use clap::Parser; use env_logger::{Builder as LoggerBuilder, Env, DEFAULT_FILTER_ENV}; -use himalaya::{cli::Cli, config::TomlConfig, printer::StdoutPrinter}; +use himalaya::{ + cli::Cli, config::TomlConfig, message::command::mailto::MessageMailtoCommand, + printer::StdoutPrinter, +}; use log::{debug, warn}; use std::env; @@ -9,7 +12,7 @@ use std::env; async fn main() -> Result<()> { #[cfg(not(target_os = "windows"))] if let Err((_, err)) = coredump::register_panic_handler() { - warn!("cannot register custom panic handler: {err}"); + warn!("cannot register coredump panic handler: {err}"); debug!("{err:?}"); } @@ -18,15 +21,15 @@ async fn main() -> Result<()> { .format_timestamp(None) .init(); - let raw_args: Vec = env::args().collect(); - if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { - // TODO - // let cmd = MessageMailtoCommand::command() - // .no_binary_name(true) - // .try_get_matches_from([&raw_args[1]]); - // match cmd { - // Ok(m) => m.exec - // } + // if the first argument starts by "mailto:", execute straight the + // mailto message command + if let Some(ref url) = env::args().nth(1).filter(|arg| arg.starts_with("mailto:")) { + let mut printer = StdoutPrinter::default(); + let config = TomlConfig::from_default_paths().await?; + + return MessageMailtoCommand::new(url)? + .execute(&mut printer, &config) + .await; } let cli = Cli::parse(); diff --git a/src/manual/command.rs b/src/manual/command.rs index f55ca93..383bb2c 100644 --- a/src/manual/command.rs +++ b/src/manual/command.rs @@ -7,10 +7,15 @@ use std::{fs, path::PathBuf}; use crate::{cli::Cli, printer::Printer}; -/// Generate manual pages to a directory +/// Generate manual pages to a directory. +/// +/// This command allows you to generate manual pages (following the +/// man page format) to the given directory. If the directory does not +/// exist, it will be created. Any existing man pages will be +/// overriden. #[derive(Debug, Parser)] pub struct ManualGenerateCommand { - /// Directory where man files should be generated in + /// Directory where man files should be generated in. #[arg(value_parser = dir_parser)] pub dir: PathBuf, } diff --git a/src/ui/table/arg/max_width.rs b/src/ui/table/arg/max_width.rs index bbcd977..db394ff 100644 --- a/src/ui/table/arg/max_width.rs +++ b/src/ui/table/arg/max_width.rs @@ -1,9 +1,13 @@ use clap::Parser; -/// The table max width argument parser +/// The table max width argument parser. #[derive(Debug, Parser)] -pub struct MaxTableWidthFlag { - /// The maximum width the table should not exceed - #[arg(long, short = 'w', value_name = "PIXELS")] +pub struct TableMaxWidthFlag { + /// The maximum width the table should not exceed. + /// + /// This argument will force the table not to exceed the given + /// width in pixels. Columns may shrink with ellipsis in order to + /// fit the width. + #[arg(long, short = 'w', name = "table_max_width", value_name = "PIXELS")] pub max_width: Option, } From 04e721d591ed0c5fe91e60eeb9b18ab76d087a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 9 Dec 2023 09:36:26 +0100 Subject: [PATCH 25/29] adjust api, test commands with a greenmail instance --- config.sample.toml | 23 +++++++++++++++++-- src/account/arg/name.rs | 4 ++-- src/account/command/mod.rs | 4 ++-- src/account/command/sync.rs | 2 +- src/account/mod.rs | 5 +++- src/cache/arg/disable.rs | 2 +- src/cli.rs | 1 + src/email/envelope/flag/command/add.rs | 4 ++-- src/email/envelope/flag/command/remove.rs | 4 ++-- src/email/envelope/flag/command/set.rs | 4 ++-- src/email/message/arg/body.rs | 2 +- src/email/message/arg/mod.rs | 17 ++++++++++++++ .../message/attachment/command/download.rs | 6 ++--- src/email/message/command/delete.rs | 6 ++--- src/email/message/command/forward.rs | 21 +++-------------- src/email/message/command/mailto.rs | 23 +++++++++++++++---- src/email/message/command/mod.rs | 8 ++++--- src/email/message/command/read.rs | 6 ++--- src/email/message/command/reply.rs | 21 +++-------------- src/email/message/command/save.rs | 8 +++---- src/email/message/command/send.rs | 6 ++--- src/email/message/command/write.rs | 17 +------------- src/email/message/template/arg/body.rs | 4 ++-- src/email/message/template/arg/mod.rs | 17 ++++++++++++++ src/email/message/template/command/forward.rs | 4 ++-- src/email/message/template/command/mod.rs | 4 ++-- src/email/message/template/command/reply.rs | 4 ++-- src/email/message/template/command/save.rs | 10 ++++---- src/email/message/template/command/send.rs | 6 ++--- src/folder/arg/name.rs | 15 +++++++++--- src/folder/command/mod.rs | 4 ++-- 31 files changed, 150 insertions(+), 112 deletions(-) diff --git a/config.sample.toml b/config.sample.toml index b59b07e..ca17825 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -7,7 +7,7 @@ display-name = "My example account" email = "example@localhost" sync = true -sync-dir = "./.sync" +sync-dir = "/tmp/himalaya-sync-example" # The default backend used for all the features like adding folders, # listing envelopes or copying messages. @@ -21,7 +21,7 @@ imap.ssl = false imap.starttls = false imap.insecure = true imap.auth = "passwd" -imap.passwd.raw = "example" +imap.passwd.keyring = "example-passwd" # Override the backend used for sending messages. message.send.backend = "smtp" @@ -35,3 +35,22 @@ smtp.starttls = false smtp.insecure = true smtp.auth = "passwd" smtp.passwd.raw = "example" + +[example-2] +display-name = "My example account 2" +email = "example2@localhost" + +backend = "imap" + +imap.host = "localhost" +imap.port = 3143 +imap.login = "example2@localhost" +imap.ssl = false +imap.starttls = false +imap.insecure = true +imap.auth = "passwd" +imap.passwd.raw = "example" + +message.send.backend = "sendmail" + +sendmail.cmd = "sendmail" diff --git a/src/account/arg/name.rs b/src/account/arg/name.rs index 189d5ba..b1fe3a2 100644 --- a/src/account/arg/name.rs +++ b/src/account/arg/name.rs @@ -12,13 +12,13 @@ pub struct AccountNameArg { } /// The account name flag parser. -#[derive(Debug, Parser)] +#[derive(Debug, Default, Parser)] pub struct AccountNameFlag { /// Override the default account. /// /// An account name corresponds to an entry in the table at the /// root level of your TOML configuration file. - #[arg(long = "account", short = 'a', global = true)] + #[arg(long = "account", short = 'a')] #[arg(name = "account_name", value_name = "NAME")] pub name: Option, } diff --git a/src/account/command/mod.rs b/src/account/command/mod.rs index 76c8147..0161821 100644 --- a/src/account/command/mod.rs +++ b/src/account/command/mod.rs @@ -15,7 +15,7 @@ use self::{ /// /// An account is a set of settings, identified by an account /// name. Settings are directly taken from your TOML configuration -/// file. +/// file. This subcommand allows you to manage them. #[derive(Debug, Subcommand)] pub enum AccountSubcommand { #[command(alias = "cfg")] @@ -24,7 +24,7 @@ pub enum AccountSubcommand { #[command(alias = "lst")] List(AccountListCommand), - #[command()] + #[command(alias = "synchronize", alias = "synchronise")] Sync(AccountSyncCommand), } diff --git a/src/account/command/sync.rs b/src/account/command/sync.rs index 7db35bc..cf136ac 100644 --- a/src/account/command/sync.rs +++ b/src/account/command/sync.rs @@ -35,7 +35,7 @@ const SUB_PROGRESS_DONE_STYLE: Lazy = Lazy::new(|| { /// Synchronize an account. /// /// This command allows you to synchronize all folders and emails -/// (including envelopes and messages) of an account into a local +/// (including envelopes and messages) of a given account into a local /// Maildir folder. #[derive(Debug, Parser)] pub struct AccountSyncCommand { diff --git a/src/account/mod.rs b/src/account/mod.rs index 1910174..683cdd9 100644 --- a/src/account/mod.rs +++ b/src/account/mod.rs @@ -123,7 +123,10 @@ impl From> for Accounts { Account::new(name, &backends, account.default.unwrap_or_default()) }) .collect(); - accounts.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap()); + + // sort accounts by name + accounts.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap()); + Self(accounts) } } diff --git a/src/cache/arg/disable.rs b/src/cache/arg/disable.rs index b1049f2..04f32a1 100644 --- a/src/cache/arg/disable.rs +++ b/src/cache/arg/disable.rs @@ -1,7 +1,7 @@ use clap::Parser; /// The disable cache flag parser. -#[derive(Debug, Parser)] +#[derive(Debug, Default, Parser)] pub struct CacheDisableFlag { /// Disable any sort of cache. /// diff --git a/src/cli.rs b/src/cli.rs index d8f9025..06a04ec 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -82,6 +82,7 @@ pub enum HimalayaCommand { Account(AccountSubcommand), #[command(subcommand)] + #[command(visible_alias = "mailbox", aliases = ["mailboxes", "mboxes", "mbox"])] #[command(alias = "folders")] Folder(FolderSubcommand), diff --git a/src/email/envelope/flag/command/add.rs b/src/email/envelope/flag/command/add.rs index c979993..5150516 100644 --- a/src/email/envelope/flag/command/add.rs +++ b/src/email/envelope/flag/command/add.rs @@ -8,7 +8,7 @@ use crate::{ cache::arg::disable::CacheDisableFlag, config::TomlConfig, flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, - folder::arg::name::FolderNameArg, + folder::arg::name::FolderNameOptionalFlag, printer::Printer, }; @@ -19,7 +19,7 @@ use crate::{ #[derive(Debug, Parser)] pub struct FlagAddCommand { #[command(flatten)] - pub folder: FolderNameArg, + pub folder: FolderNameOptionalFlag, #[command(flatten)] pub args: IdsAndFlagsArgs, diff --git a/src/email/envelope/flag/command/remove.rs b/src/email/envelope/flag/command/remove.rs index 807811d..e2d3a14 100644 --- a/src/email/envelope/flag/command/remove.rs +++ b/src/email/envelope/flag/command/remove.rs @@ -8,7 +8,7 @@ use crate::{ cache::arg::disable::CacheDisableFlag, config::TomlConfig, flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, - folder::arg::name::FolderNameArg, + folder::arg::name::FolderNameOptionalFlag, printer::Printer, }; @@ -19,7 +19,7 @@ use crate::{ #[derive(Debug, Parser)] pub struct FlagRemoveCommand { #[command(flatten)] - pub folder: FolderNameArg, + pub folder: FolderNameOptionalFlag, #[command(flatten)] pub args: IdsAndFlagsArgs, diff --git a/src/email/envelope/flag/command/set.rs b/src/email/envelope/flag/command/set.rs index 8c7343a..5cbe7f1 100644 --- a/src/email/envelope/flag/command/set.rs +++ b/src/email/envelope/flag/command/set.rs @@ -8,7 +8,7 @@ use crate::{ cache::arg::disable::CacheDisableFlag, config::TomlConfig, flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, - folder::arg::name::FolderNameArg, + folder::arg::name::FolderNameOptionalFlag, printer::Printer, }; @@ -19,7 +19,7 @@ use crate::{ #[derive(Debug, Parser)] pub struct FlagSetCommand { #[command(flatten)] - pub folder: FolderNameArg, + pub folder: FolderNameOptionalFlag, #[command(flatten)] pub args: IdsAndFlagsArgs, diff --git a/src/email/message/arg/body.rs b/src/email/message/arg/body.rs index f7ec4a1..e5eabd7 100644 --- a/src/email/message/arg/body.rs +++ b/src/email/message/arg/body.rs @@ -6,7 +6,7 @@ use std::ops::Deref; pub struct MessageRawBodyArg { /// Prefill the template with a custom body. #[arg(trailing_var_arg = true)] - #[arg(name = "body-raw")] + #[arg(name = "body_raw", value_name = "BODY")] pub raw: Vec, } diff --git a/src/email/message/arg/mod.rs b/src/email/message/arg/mod.rs index 509753e..216276b 100644 --- a/src/email/message/arg/mod.rs +++ b/src/email/message/arg/mod.rs @@ -1,3 +1,20 @@ +use clap::Parser; + pub mod body; pub mod header; pub mod reply; + +/// The raw message argument parser. +#[derive(Debug, Parser)] +pub struct MessageRawArg { + /// The raw message, including headers and body. + #[arg(trailing_var_arg = true)] + #[arg(name = "message_raw", value_name = "MESSAGE")] + pub raw: Vec, +} + +impl MessageRawArg { + pub fn raw(self) -> String { + self.raw.join(" ").replace("\r", "").replace("\n", "\r\n") + } +} diff --git a/src/email/message/attachment/command/download.rs b/src/email/message/attachment/command/download.rs index c2dad39..13be174 100644 --- a/src/email/message/attachment/command/download.rs +++ b/src/email/message/attachment/command/download.rs @@ -6,8 +6,8 @@ use uuid::Uuid; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, - config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameArg, - printer::Printer, + config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, + folder::arg::name::FolderNameOptionalFlag, printer::Printer, }; /// Download all attachments for the given message. @@ -17,7 +17,7 @@ use crate::{ #[derive(Debug, Parser)] pub struct AttachmentDownloadCommand { #[command(flatten)] - pub folder: FolderNameArg, + pub folder: FolderNameOptionalFlag, #[command(flatten)] pub envelopes: EnvelopeIdsArgs, diff --git a/src/email/message/command/delete.rs b/src/email/message/command/delete.rs index 8b67b91..a5145e7 100644 --- a/src/email/message/command/delete.rs +++ b/src/email/message/command/delete.rs @@ -4,8 +4,8 @@ use log::info; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, - config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameArg, - printer::Printer, + config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, + folder::arg::name::FolderNameOptionalFlag, printer::Printer, }; /// Mark as deleted a message from a folder. @@ -17,7 +17,7 @@ use crate::{ #[derive(Debug, Parser)] pub struct MessageDeleteCommand { #[command(flatten)] - pub folder: FolderNameArg, + pub folder: FolderNameOptionalFlag, #[command(flatten)] pub envelopes: EnvelopeIdsArgs, diff --git a/src/email/message/command/forward.rs b/src/email/message/command/forward.rs index 4dbf61f..506e396 100644 --- a/src/email/message/command/forward.rs +++ b/src/email/message/command/forward.rs @@ -1,8 +1,6 @@ use anyhow::{anyhow, Result}; -use atty::Stream; use clap::Parser; use log::info; -use std::io::{self, BufRead}; use crate::{ account::arg::name::AccountNameFlag, @@ -10,7 +8,7 @@ use crate::{ cache::arg::disable::CacheDisableFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg, - folder::arg::name::FolderNameArg, + folder::arg::name::FolderNameOptionalFlag, message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs}, printer::Printer, ui::editor, @@ -25,7 +23,7 @@ use crate::{ #[derive(Debug, Parser)] pub struct MessageForwardCommand { #[command(flatten)] - pub folder: FolderNameArg, + pub folder: FolderNameOptionalFlag, #[command(flatten)] pub envelope: EnvelopeIdArg, @@ -55,19 +53,6 @@ impl MessageForwardCommand { config.clone().into_account_configs(account, cache)?; let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - let is_tty = atty::is(Stream::Stdin); - let is_json = printer.is_json(); - let body = if !self.body.is_empty() && (is_tty || is_json) { - self.body.raw() - } else { - io::stdin() - .lock() - .lines() - .filter_map(Result::ok) - .collect::>() - .join("\r\n") - }; - let id = self.envelope.id; let tpl = backend .get_messages(folder, &[id]) @@ -76,7 +61,7 @@ impl MessageForwardCommand { .ok_or(anyhow!("cannot find message"))? .to_forward_tpl_builder(&account_config) .with_headers(self.headers.raw) - .with_body(body) + .with_body(self.body.raw()) .build() .await?; editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await diff --git a/src/email/message/command/mailto.rs b/src/email/message/command/mailto.rs index c2daed4..a58f0ca 100644 --- a/src/email/message/command/mailto.rs +++ b/src/email/message/command/mailto.rs @@ -4,7 +4,10 @@ use log::{debug, info}; use mail_builder::MessageBuilder; use url::Url; -use crate::{backend::Backend, config::TomlConfig, printer::Printer, ui::editor}; +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, + config::TomlConfig, printer::Printer, ui::editor, +}; /// Parse and edit a message from a mailto URL string. /// @@ -17,19 +20,31 @@ pub struct MessageMailtoCommand { /// The mailto url. #[arg()] pub url: Url, + + #[command(flatten)] + pub cache: CacheDisableFlag, + + #[command(flatten)] + pub account: AccountNameFlag, } impl MessageMailtoCommand { pub fn new(url: &str) -> Result { - let url = Url::parse(url)?; - Ok(Self { url }) + Ok(Self { + url: Url::parse(url)?, + cache: Default::default(), + account: Default::default(), + }) } pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { info!("executing message mailto command"); + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + let (toml_account_config, account_config) = - config.clone().into_account_configs(None, false)?; + config.clone().into_account_configs(account, cache)?; let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; let mut builder = MessageBuilder::new().to(self.url.path()); diff --git a/src/email/message/command/mod.rs b/src/email/message/command/mod.rs index 53606b2..9036b46 100644 --- a/src/email/message/command/mod.rs +++ b/src/email/message/command/mod.rs @@ -32,32 +32,34 @@ pub enum MessageSubcommand { #[command(arg_required_else_help = true)] Read(MessageReadCommand), - #[command(alias = "add", alias = "create", alias = "new", alias = "compose")] + #[command(aliases = ["add", "create", "new", "compose"])] Write(MessageWriteCommand), #[command()] Reply(MessageReplyCommand), - #[command(alias = "fwd")] + #[command(aliases = ["fwd", "fd"])] Forward(MessageForwardCommand), #[command()] Mailto(MessageMailtoCommand), #[command(arg_required_else_help = true)] - #[command(alias = "add", alias = "create")] Save(MessageSaveCommand), #[command(arg_required_else_help = true)] Send(MessageSendCommand), #[command(arg_required_else_help = true)] + #[command(aliases = ["cpy", "cp"])] Copy(MessageCopyCommand), #[command(arg_required_else_help = true)] + #[command(alias = "mv")] Move(MessageMoveCommand), #[command(arg_required_else_help = true)] + #[command(aliases = ["remove", "rm"])] Delete(MessageDeleteCommand), } diff --git a/src/email/message/command/read.rs b/src/email/message/command/read.rs index 53d1515..8acb3c1 100644 --- a/src/email/message/command/read.rs +++ b/src/email/message/command/read.rs @@ -5,8 +5,8 @@ use mml::message::FilterParts; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, - config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameArg, - printer::Printer, + config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, + folder::arg::name::FolderNameOptionalFlag, printer::Printer, }; /// Read a message. @@ -15,7 +15,7 @@ use crate::{ #[derive(Debug, Parser)] pub struct MessageReadCommand { #[command(flatten)] - pub folder: FolderNameArg, + pub folder: FolderNameOptionalFlag, #[command(flatten)] pub envelopes: EnvelopeIdsArgs, diff --git a/src/email/message/command/reply.rs b/src/email/message/command/reply.rs index dfcbbbe..03437cc 100644 --- a/src/email/message/command/reply.rs +++ b/src/email/message/command/reply.rs @@ -1,9 +1,7 @@ use anyhow::{anyhow, Result}; -use atty::Stream; use clap::Parser; use email::flag::Flag; use log::info; -use std::io::{self, BufRead}; use crate::{ account::arg::name::AccountNameFlag, @@ -11,7 +9,7 @@ use crate::{ cache::arg::disable::CacheDisableFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg, - folder::arg::name::FolderNameArg, + folder::arg::name::FolderNameOptionalFlag, message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg}, printer::Printer, ui::editor, @@ -26,7 +24,7 @@ use crate::{ #[derive(Debug, Parser)] pub struct MessageReplyCommand { #[command(flatten)] - pub folder: FolderNameArg, + pub folder: FolderNameOptionalFlag, #[command(flatten)] pub envelope: EnvelopeIdArg, @@ -59,19 +57,6 @@ impl MessageReplyCommand { config.clone().into_account_configs(account, cache)?; let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - let is_tty = atty::is(Stream::Stdin); - let is_json = printer.is_json(); - let body = if !self.body.is_empty() && (is_tty || is_json) { - self.body.raw() - } else { - io::stdin() - .lock() - .lines() - .filter_map(Result::ok) - .collect::>() - .join("\r\n") - }; - let id = self.envelope.id; let tpl = backend .get_messages(folder, &[id]) @@ -80,7 +65,7 @@ impl MessageReplyCommand { .ok_or(anyhow!("cannot find message {id}"))? .to_reply_tpl_builder(&account_config) .with_headers(self.headers.raw) - .with_body(body) + .with_body(self.body.raw()) .with_reply_all(self.reply.all) .build() .await?; diff --git a/src/email/message/command/save.rs b/src/email/message/command/save.rs index 637ddeb..b20f919 100644 --- a/src/email/message/command/save.rs +++ b/src/email/message/command/save.rs @@ -6,7 +6,7 @@ use std::io::{self, BufRead}; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, - config::TomlConfig, folder::arg::name::FolderNameArg, message::arg::body::MessageRawBodyArg, + config::TomlConfig, folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg, printer::Printer, }; @@ -16,10 +16,10 @@ use crate::{ #[derive(Debug, Parser)] pub struct MessageSaveCommand { #[command(flatten)] - pub folder: FolderNameArg, + pub folder: FolderNameOptionalFlag, #[command(flatten)] - pub body: MessageRawBodyArg, + pub message: MessageRawArg, #[command(flatten)] pub cache: CacheDisableFlag, @@ -43,7 +43,7 @@ impl MessageSaveCommand { let is_tty = atty::is(Stream::Stdin); let is_json = printer.is_json(); let msg = if is_tty || is_json { - self.body.raw() + self.message.raw() } else { io::stdin() .lock() diff --git a/src/email/message/command/send.rs b/src/email/message/command/send.rs index 9f176b1..d8e1930 100644 --- a/src/email/message/command/send.rs +++ b/src/email/message/command/send.rs @@ -7,7 +7,7 @@ use std::io::{self, BufRead}; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, - config::TomlConfig, message::arg::body::MessageRawBodyArg, printer::Printer, + config::TomlConfig, message::arg::MessageRawArg, printer::Printer, }; /// Send a message. @@ -17,7 +17,7 @@ use crate::{ #[derive(Debug, Parser)] pub struct MessageSendCommand { #[command(flatten)] - pub body: MessageRawBodyArg, + pub message: MessageRawArg, #[command(flatten)] pub cache: CacheDisableFlag, @@ -41,7 +41,7 @@ impl MessageSendCommand { let is_tty = atty::is(Stream::Stdin); let is_json = printer.is_json(); let msg = if is_tty || is_json { - self.body.raw() + self.message.raw() } else { io::stdin() .lock() diff --git a/src/email/message/command/write.rs b/src/email/message/command/write.rs index 38b1c28..16eb455 100644 --- a/src/email/message/command/write.rs +++ b/src/email/message/command/write.rs @@ -1,9 +1,7 @@ use anyhow::Result; -use atty::Stream; use clap::Parser; use email::message::Message; use log::info; -use std::io::{self, BufRead}; use crate::{ account::arg::name::AccountNameFlag, @@ -47,22 +45,9 @@ impl MessageWriteCommand { config.clone().into_account_configs(account, cache)?; let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - let is_tty = atty::is(Stream::Stdin); - let is_json = printer.is_json(); - let body = if !self.body.is_empty() && (is_tty || is_json) { - self.body.raw() - } else { - io::stdin() - .lock() - .lines() - .filter_map(Result::ok) - .collect::>() - .join("\r\n") - }; - let tpl = Message::new_tpl_builder(&account_config) .with_headers(self.headers.raw) - .with_body(body) + .with_body(self.body.raw()) .build() .await?; diff --git a/src/email/message/template/arg/body.rs b/src/email/message/template/arg/body.rs index fafd455..f577d19 100644 --- a/src/email/message/template/arg/body.rs +++ b/src/email/message/template/arg/body.rs @@ -4,9 +4,9 @@ use std::ops::Deref; /// The raw template body argument parser. #[derive(Debug, Parser)] pub struct TemplateRawBodyArg { - /// Prefill the template with a custom body. + /// Prefill the template with a custom MML body. #[arg(trailing_var_arg = true)] - #[arg(name = "body-raw")] + #[arg(name = "body_raw", value_name = "BODY")] pub raw: Vec, } diff --git a/src/email/message/template/arg/mod.rs b/src/email/message/template/arg/mod.rs index 81f6efd..8798e08 100644 --- a/src/email/message/template/arg/mod.rs +++ b/src/email/message/template/arg/mod.rs @@ -1 +1,18 @@ pub mod body; + +use clap::Parser; + +/// The raw template argument parser. +#[derive(Debug, Parser)] +pub struct TemplateRawArg { + /// The raw template, including headers and MML body. + #[arg(trailing_var_arg = true)] + #[arg(name = "template_raw", value_name = "TEMPLATE")] + pub raw: Vec, +} + +impl TemplateRawArg { + pub fn raw(self) -> String { + self.raw.join(" ").replace("\r", "") + } +} diff --git a/src/email/message/template/command/forward.rs b/src/email/message/template/command/forward.rs index c09529d..f1b510c 100644 --- a/src/email/message/template/command/forward.rs +++ b/src/email/message/template/command/forward.rs @@ -8,7 +8,7 @@ use crate::{ cache::arg::disable::CacheDisableFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg, - folder::arg::name::FolderNameArg, + folder::arg::name::FolderNameOptionalFlag, message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs}, printer::Printer, }; @@ -21,7 +21,7 @@ use crate::{ #[derive(Debug, Parser)] pub struct TemplateForwardCommand { #[command(flatten)] - pub folder: FolderNameArg, + pub folder: FolderNameOptionalFlag, #[command(flatten)] pub envelope: EnvelopeIdArg, diff --git a/src/email/message/template/command/mod.rs b/src/email/message/template/command/mod.rs index eed6ca7..fa4bab7 100644 --- a/src/email/message/template/command/mod.rs +++ b/src/email/message/template/command/mod.rs @@ -25,7 +25,7 @@ use self::{ /// . #[derive(Debug, Subcommand)] pub enum TemplateSubcommand { - #[command(alias = "create", alias = "new", alias = "compose")] + #[command(aliases = ["add", "create", "new", "compose"])] Write(TemplateWriteCommand), #[command(arg_required_else_help = true)] @@ -35,7 +35,7 @@ pub enum TemplateSubcommand { #[command(alias = "fwd")] Forward(TemplateForwardCommand), - #[command(alias = "add")] + #[command()] Save(TemplateSaveCommand), #[command()] diff --git a/src/email/message/template/command/reply.rs b/src/email/message/template/command/reply.rs index fe42af5..4d5cd6a 100644 --- a/src/email/message/template/command/reply.rs +++ b/src/email/message/template/command/reply.rs @@ -8,7 +8,7 @@ use crate::{ cache::arg::disable::CacheDisableFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg, - folder::arg::name::FolderNameArg, + folder::arg::name::FolderNameOptionalFlag, message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg}, printer::Printer, }; @@ -22,7 +22,7 @@ use crate::{ #[derive(Debug, Parser)] pub struct TemplateReplyCommand { #[command(flatten)] - pub folder: FolderNameArg, + pub folder: FolderNameOptionalFlag, #[command(flatten)] pub envelope: EnvelopeIdArg, diff --git a/src/email/message/template/command/save.rs b/src/email/message/template/command/save.rs index 4253468..5e1b1c6 100644 --- a/src/email/message/template/command/save.rs +++ b/src/email/message/template/command/save.rs @@ -7,8 +7,8 @@ use std::io::{self, BufRead}; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, - config::TomlConfig, email::template::arg::body::TemplateRawBodyArg, - folder::arg::name::FolderNameArg, printer::Printer, + config::TomlConfig, email::template::arg::TemplateRawArg, + folder::arg::name::FolderNameOptionalFlag, printer::Printer, }; /// Save a template to a folder. @@ -20,10 +20,10 @@ use crate::{ #[derive(Debug, Parser)] pub struct TemplateSaveCommand { #[command(flatten)] - pub folder: FolderNameArg, + pub folder: FolderNameOptionalFlag, #[command(flatten)] - pub body: TemplateRawBodyArg, + pub template: TemplateRawArg, #[command(flatten)] pub cache: CacheDisableFlag, @@ -47,7 +47,7 @@ impl TemplateSaveCommand { let is_tty = atty::is(Stream::Stdin); let is_json = printer.is_json(); let tpl = if is_tty || is_json { - self.body.raw() + self.template.raw() } else { io::stdin() .lock() diff --git a/src/email/message/template/command/send.rs b/src/email/message/template/command/send.rs index 4b74b95..c62d642 100644 --- a/src/email/message/template/command/send.rs +++ b/src/email/message/template/command/send.rs @@ -8,7 +8,7 @@ use std::io::{self, BufRead}; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, - config::TomlConfig, email::template::arg::body::TemplateRawBodyArg, printer::Printer, + config::TomlConfig, email::template::arg::TemplateRawArg, printer::Printer, }; /// Send a template. @@ -20,7 +20,7 @@ use crate::{ #[derive(Debug, Parser)] pub struct TemplateSendCommand { #[command(flatten)] - pub body: TemplateRawBodyArg, + pub template: TemplateRawArg, #[command(flatten)] pub cache: CacheDisableFlag, @@ -44,7 +44,7 @@ impl TemplateSendCommand { let is_tty = atty::is(Stream::Stdin); let is_json = printer.is_json(); let tpl = if is_tty || is_json { - self.body.raw() + self.template.raw() } else { io::stdin() .lock() diff --git a/src/folder/arg/name.rs b/src/folder/arg/name.rs index 15307b8..8b6c515 100644 --- a/src/folder/arg/name.rs +++ b/src/folder/arg/name.rs @@ -1,11 +1,12 @@ use clap::Parser; use email::account::config::DEFAULT_INBOX_FOLDER; -/// The folder name argument parser. +/// The optional folder name flag parser. #[derive(Debug, Parser)] -pub struct FolderNameArg { +pub struct FolderNameOptionalFlag { /// The name of the folder. - #[arg(name = "folder_name", value_name = "FOLDER")] + #[arg(long = "folder", short = 'f')] + #[arg(name = "folder_name", value_name = "NAME", default_value = DEFAULT_INBOX_FOLDER)] pub name: String, } @@ -17,6 +18,14 @@ pub struct FolderNameOptionalArg { pub name: String, } +/// The required folder name argument parser. +#[derive(Debug, Parser)] +pub struct FolderNameArg { + /// The name of the folder. + #[arg(name = "folder_name", value_name = "FOLDER")] + pub name: String, +} + /// The source folder name argument parser. #[derive(Debug, Parser)] pub struct SourceFolderNameArg { diff --git a/src/folder/command/mod.rs b/src/folder/command/mod.rs index 29959aa..5de0450 100644 --- a/src/folder/command/mod.rs +++ b/src/folder/command/mod.rs @@ -16,8 +16,8 @@ use self::{ /// Manage folders. /// -/// A folder (AKA mailbox, or directory) contains envelopes and -/// messages. This subcommand allows you to manage them. +/// A folder (as known as mailbox, or directory) contains one or more +/// emails. This subcommand allows you to manage them. #[derive(Debug, Subcommand)] pub enum FolderSubcommand { #[command(alias = "add", alias = "new")] From 203ed2f917598b5729e27e1ef57a71ed2456055d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 9 Dec 2023 22:06:08 +0100 Subject: [PATCH 26/29] fix editor command hanging, add --preview flag for msg read cmd --- Cargo.lock | 9 +++------ Cargo.toml | 9 ++++++--- src/email/message/command/read.rs | 15 +++++++++++++-- src/ui/editor.rs | 5 +++-- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 099bdf7..2201059 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2445,8 +2445,6 @@ dependencies = [ [[package]] name = "keyring-lib" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dcc9433b6eaf33f2f6a8d3a53b598a5d0b8be224c41bd98d1ec936ef4d02d69" dependencies = [ "keyring", "log", @@ -3233,10 +3231,10 @@ dependencies = [ [[package]] name = "process-lib" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe824234b824573ff3a80ddf3a6b19e6ffba966798d071f280723ee02a7273ce" dependencies = [ + "anyhow", "log", + "once_cell", "thiserror", "tokio", ] @@ -3707,9 +3705,8 @@ dependencies = [ [[package]] name = "secret-lib" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d46e99ae858a1978ec3e6e887966514900229fc0df99935a2c61102854f9195e" dependencies = [ + "anyhow", "keyring-lib", "log", "process-lib", diff --git a/Cargo.toml b/Cargo.toml index 21a4b20..b0c6de5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,8 @@ default-features = false path = "/home/soywod/sourcehut/pimalaya/email" [dependencies.keyring-lib] -version = "=0.1.0" +# version = "=0.1.0" +path = "/home/soywod/sourcehut/pimalaya/keyring" [dependencies.mail-builder] version = "0.3" @@ -114,7 +115,8 @@ version = "0.3" path = "/home/soywod/sourcehut/pimalaya/oauth" [dependencies.process-lib] -version = "=0.1.0" +# version = "=0.1.0" +path = "/home/soywod/sourcehut/pimalaya/process" [dependencies.mml-lib] # version = "=1.0.1" @@ -123,7 +125,8 @@ features = ["compiler", "interpreter"] path = "/home/soywod/sourcehut/pimalaya/mml" [dependencies.secret-lib] -version = "=0.1.0" +# version = "=0.1.0" +path = "/home/soywod/sourcehut/pimalaya/secret" [dependencies.serde] version = "1.0" diff --git a/src/email/message/command/read.rs b/src/email/message/command/read.rs index 8acb3c1..49cab78 100644 --- a/src/email/message/command/read.rs +++ b/src/email/message/command/read.rs @@ -11,7 +11,9 @@ use crate::{ /// Read a message. /// -/// This command allows you to read a message. +/// This command allows you to read a message. When reading a message, +/// the "seen" flag is automatically applied to the corresponding +/// envelope. To prevent this behaviour, use the --preview flag. #[derive(Debug, Parser)] pub struct MessageReadCommand { #[command(flatten)] @@ -20,6 +22,11 @@ pub struct MessageReadCommand { #[command(flatten)] pub envelopes: EnvelopeIdsArgs, + /// Read the message without applying the "seen" flag to its + /// corresponding envelope. + #[arg(long, short)] + pub preview: bool, + /// Read the raw version of the given message. /// /// The raw message represents the headers and the body as it is @@ -79,7 +86,11 @@ impl MessageReadCommand { let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; let ids = &self.envelopes.ids; - let emails = backend.get_messages(&folder, &ids).await?; + let emails = if self.preview { + backend.peek_messages(&folder, &ids).await + } else { + backend.get_messages(&folder, &ids).await + }?; let mut glue = ""; let mut bodies = String::default(); diff --git a/src/ui/editor.rs b/src/ui/editor.rs index 34eef3c..e7caeb7 100644 --- a/src/ui/editor.rs +++ b/src/ui/editor.rs @@ -6,7 +6,7 @@ use email::{ }; use log::debug; use mml::MmlCompilerBuilder; -use process::Cmd; +use process::SingleCmd; use std::{env, fs}; use crate::{ @@ -23,7 +23,8 @@ pub async fn open_with_tpl(tpl: String) -> Result { debug!("open editor"); let editor = env::var("EDITOR").context("cannot get editor from env var")?; - Cmd::from(format!("{editor} {}", &path.to_string_lossy())) + SingleCmd::from(format!("{editor} {}", &path.to_string_lossy())) + .with_output_piped(false) .run() .await .context("cannot launch editor")?; From 8e05be7f7734616fc1d634bda00f2c412e16947b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 10 Dec 2023 22:01:49 +0100 Subject: [PATCH 27/29] apply pr #461 due to conflicts, bump pimalaya crates --- Cargo.lock | 1129 +++++++++++++---- Cargo.toml | 172 +-- src/account/wizard.rs | 21 +- src/config/wizard.rs | 4 +- .../message/attachment/command/download.rs | 4 +- src/email/message/command/save.rs | 5 +- src/email/message/command/send.rs | 5 +- src/email/message/template/command/save.rs | 5 +- src/email/message/template/command/send.rs | 5 +- src/output/output.rs | 9 +- 10 files changed, 980 insertions(+), 379 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2201059..663cb49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures", + "opaque-debug", +] + [[package]] name = "aes" version = "0.8.3" @@ -40,7 +52,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures", ] @@ -95,9 +107,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" dependencies = [ "anstyle", "anstyle-parse", @@ -115,30 +127,30 @@ checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -153,6 +165,105 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" +dependencies = [ + "concurrent-queue", + "event-listener 4.0.0", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6d3b15875ba253d1110c740755e246537483f152fa334f91abd7fe84c88b3ff" +dependencies = [ + "async-lock 3.2.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.1.0", + "parking", + "polling 3.3.1", + "rustix 0.38.28", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" +dependencies = [ + "event-listener 4.0.0", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +dependencies = [ + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", + "blocking", + "cfg-if", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.28", + "windows-sys 0.48.0", +] + [[package]] name = "async-recursion" version = "1.0.5" @@ -164,6 +275,30 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "async-signal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" +dependencies = [ + "async-io 2.2.1", + "async-lock 2.8.0", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.28", + "signal-hook-registry", + "slab", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-task" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" + [[package]] name = "async-trait" version = "0.1.74" @@ -175,6 +310,12 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" @@ -219,12 +360,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" -[[package]] -name = "base64" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" - [[package]] name = "base64" version = "0.21.5" @@ -264,6 +399,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-modes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" +dependencies = [ + "block-padding 0.2.1", + "cipher 0.3.0", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + [[package]] name = "block-padding" version = "0.3.3" @@ -273,6 +424,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +dependencies = [ + "async-channel", + "async-lock 3.2.0", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite 2.1.0", + "piper", + "tracing", +] + [[package]] name = "blowfish" version = "0.9.1" @@ -280,7 +447,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" dependencies = [ "byteorder", - "cipher", + "cipher 0.4.4", ] [[package]] @@ -377,7 +544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3264e2574e9ef2b53ce6f536dea83a69ac0bc600b762d1523ff83fe07230ce30" dependencies = [ "byteorder", - "cipher", + "cipher 0.4.4", ] [[package]] @@ -386,7 +553,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b07d673db1ccf000e90f54b819db9e75a8348d6eb056e9b8ab53231b7a9911" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -405,7 +572,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -445,7 +612,7 @@ version = "1.0.0-alpha.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8edacfd1c8ea3db7e11640b5444a1703baee4c3b8c21ffd0726e09a92ab9abe5" dependencies = [ - "hashbrown 0.14.2", + "hashbrown 0.14.3", "regex-automata 0.3.9", "serde", "stacker", @@ -453,6 +620,15 @@ dependencies = [ "vergen", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "cipher" version = "0.4.4" @@ -465,9 +641,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.8" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" +checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" dependencies = [ "clap_builder", "clap_derive", @@ -475,15 +651,14 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.8" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" +checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim 0.10.0", - "terminal_size 0.3.0", ] [[package]] @@ -535,6 +710,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "concurrent-queue" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.7" @@ -577,9 +761,9 @@ checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -587,9 +771,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "coredump" @@ -812,13 +996,24 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" dependencies = [ "powerfmt", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_builder" version = "0.12.0" @@ -856,7 +1051,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -997,7 +1192,9 @@ dependencies = [ [[package]] name = "email-lib" -version = "0.15.3" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e3f51881639d657795ea95cd2fb065b8d9249912100b297730133ffbf38e0a5" dependencies = [ "advisory-lock", "anyhow", @@ -1017,7 +1214,7 @@ dependencies = [ "md5", "mml-lib", "notmuch", - "oauth-lib 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "oauth-lib", "once_cell", "ouroboros", "pgp-lib", @@ -1025,18 +1222,18 @@ dependencies = [ "rayon", "regex", "rusqlite", - "rustls 0.21.9", + "rustls 0.22.1", "rustls-native-certs", "secret-lib", - "shellexpand-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "shellexpand-utils", "thiserror", "tokio", - "tokio-rustls", + "tokio-rustls 0.25.0", "tree_magic_mini", "urlencoding", "utf7-imap", "uuid", - "webpki-roots", + "webpki-roots 0.25.3", ] [[package]] @@ -1076,13 +1273,34 @@ dependencies = [ ] [[package]] -name = "env_logger" -version = "0.10.1" +name = "enumflags2" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +checksum = "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939" dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "atty", "humantime", - "is-terminal", "log", "regex", "termcolor", @@ -1105,12 +1323,50 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.0", + "pin-project-lite", ] [[package]] @@ -1125,6 +1381,15 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fastrand" version = "2.0.1" @@ -1149,14 +1414,14 @@ checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" [[package]] name = "filetime" -version = "0.2.22" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", - "windows-sys 0.48.0", + "redox_syscall", + "windows-sys 0.52.0", ] [[package]] @@ -1259,6 +1524,31 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.29" @@ -1413,18 +1703,18 @@ dependencies = [ [[package]] name = "gix-bitmap" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ccab4bc576844ddb51b78d81b4a42d73e6229660fa614dfc3d3999c874d1959" +checksum = "d49e1a13a30d3f88be4bceae184dd13a2d3fb9ffa7515f7ed7ae771b857f4916" dependencies = [ "thiserror", ] [[package]] name = "gix-chunk" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b42ea64420f7994000130328f3c7a2038f639120518870436d31b8bde704493" +checksum = "d411ecd9b558b0c20b3252b7e409eec48eabc41d18324954fe526bac6e2db55f" dependencies = [ "thiserror", ] @@ -1733,9 +2023,9 @@ dependencies = [ [[package]] name = "gix-quote" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "475c86a97dd0127ba4465fbb239abac9ea10e68301470c9791a6dd5351cdc905" +checksum = "4f84845efa535468bc79c5a87b9d29219f1da0313c8ecf0365a5daa7e72786f2" dependencies = [ "bstr", "btoi", @@ -1845,11 +2135,11 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b85d89dc728613e26e0ed952a19583744e7f5240fcd4aa30d6c824ffd8b52f0f" +checksum = "9f82c41937f00e15a1f6cb0b55307f0ca1f77f4407ff2bf440be35aa688c6a3e" dependencies = [ - "fastrand", + "fastrand 2.0.1", ] [[package]] @@ -1965,9 +2255,9 @@ checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ "ahash", "allocator-api2", @@ -1979,7 +2269,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.2", + "hashbrown 0.14.3", ] [[package]] @@ -2027,12 +2317,12 @@ dependencies = [ "once_cell", "rand", "ring 0.16.20", - "rustls 0.21.9", + "rustls 0.21.10", "rustls-pemfile", "thiserror", "tinyvec", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tracing", "url", ] @@ -2052,21 +2342,20 @@ dependencies = [ "parking_lot", "rand", "resolv-conf", - "rustls 0.21.9", + "rustls 0.21.10", "smallvec", "thiserror", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tracing", ] [[package]] name = "himalaya" -version = "0.9.0" +version = "1.0.0-beta" dependencies = [ "anyhow", "async-trait", - "atty", "chrono", "clap", "clap_complete", @@ -2085,17 +2374,17 @@ dependencies = [ "mail-builder", "md5", "mml-lib", - "oauth-lib 0.1.0", + "oauth-lib", "once_cell", "process-lib", "rusqlite", "secret-lib", "serde", "serde_json", - "shellexpand-utils 0.1.0", + "shellexpand-utils", "tempfile", "termcolor", - "terminal_size 0.1.17", + "terminal_size", "tokio", "toml 0.7.8", "toml_edit 0.19.15", @@ -2155,9 +2444,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -2216,10 +2505,10 @@ dependencies = [ "http", "hyper", "log", - "rustls 0.21.9", + "rustls 0.21.10", "rustls-native-certs", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", ] [[package]] @@ -2251,7 +2540,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "075557004419d7f2031b8bb7f44bb43e55a83ca7b63076a8fb8fe75753836477" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -2323,7 +2612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown 0.14.3", ] [[package]] @@ -2367,6 +2656,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.3", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "ipconfig" version = "0.3.2" @@ -2385,22 +2685,11 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi 0.3.3", - "rustix", - "windows-sys 0.48.0", -] - [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jobserver" @@ -2413,9 +2702,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] @@ -2431,23 +2720,28 @@ dependencies = [ [[package]] name = "keyring" -version = "2.0.5" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9549a129bd08149e0a71b2d1ce2729780d47127991bfd0a78cc1df697ec72492" +checksum = "ec6488afbd1d8202dbd6e2dd38c0753d8c0adba9ac9985fc6f732a0d551f75e1" dependencies = [ "byteorder", "lazy_static", "linux-keyutils", + "secret-service", "security-framework", "winapi", ] [[package]] name = "keyring-lib" -version = "0.1.0" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2271300047bf0f2f4c4b6c51cbf7f848621aa373a3e80742bbdc0148110578" dependencies = [ "keyring", "log", + "once_cell", + "secret-service", "thiserror", ] @@ -2462,9 +2756,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.150" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libgpg-error-sys" @@ -2491,7 +2785,7 @@ checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ "bitflags 2.4.1", "libc", - "redox_syscall 0.4.1", + "redox_syscall", ] [[package]] @@ -2523,9 +2817,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "lock_api" @@ -2566,7 +2866,7 @@ dependencies = [ "mail-parser", "parking_lot", "quick-xml", - "ring 0.17.5", + "ring 0.17.7", "rustls-pemfile", "serde", "serde_json", @@ -2584,30 +2884,31 @@ dependencies = [ [[package]] name = "mail-parser" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e51d275d90a41584d12ea25fc4a7d7d00236a4718308b994ed13a7ff90a6985" +checksum = "7af1b812c3c16317ccadacb0db823f04c2872dfc5a1125f171f4a22d1705e9a7" dependencies = [ "encoding_rs", ] [[package]] name = "mail-send" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbc58a799366b3b2956a2c5ae7e2892ea34b3016343cbbdc5deb844aa6c0973" +checksum = "e373c9ec9178397af6ea2964568e49e1d486b2d80d17b06aa19da75566416422" dependencies = [ - "base64 0.20.0", + "base64 0.21.5", "gethostname", "mail-auth", "mail-builder", "md5", "rand", - "rustls 0.21.9", + "rustls 0.22.1", + "rustls-pki-types", "smtp-proto", "tokio", - "tokio-rustls", - "webpki-roots", + "tokio-rustls 0.25.0", + "webpki-roots 0.26.0", ] [[package]] @@ -2699,9 +3000,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", @@ -2710,7 +3011,9 @@ dependencies = [ [[package]] name = "mml-lib" -version = "1.0.1" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bda7bfafd584f65bc7bb85a60be2a6ccf6f384f1e1d27b697d8b6524869213" dependencies = [ "async-recursion", "chumsky", @@ -2723,7 +3026,7 @@ dependencies = [ "pgp-lib", "process-lib", "secret-lib", - "shellexpand-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "shellexpand-utils", "thiserror", "tree_magic_mini", ] @@ -2743,6 +3046,7 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", + "memoffset 0.7.1", ] [[package]] @@ -2765,6 +3069,31 @@ dependencies = [ "libc", ] +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -2783,6 +3112,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.4.1" @@ -2815,6 +3153,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -2850,18 +3200,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" -[[package]] -name = "oauth-lib" -version = "0.1.0" -dependencies = [ - "log", - "oauth2", - "reqwest", - "thiserror", - "tokio", - "url", -] - [[package]] name = "oauth-lib" version = "0.1.0" @@ -2907,9 +3245,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl-probe" @@ -2923,6 +3267,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_str_bytes" version = "6.6.1" @@ -2979,6 +3333,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + [[package]] name = "parking_lot" version = "0.12.1" @@ -2997,7 +3357,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall", "smallvec", "windows-targets 0.48.5", ] @@ -3056,10 +3416,10 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27e1f8e085bfa9b85763fe3ddaacbe90a09cd847b3833129153a6cb063bbe132" dependencies = [ - "aes", + "aes 0.8.3", "base64 0.21.5", "bitfield", - "block-padding", + "block-padding 0.3.3", "blowfish", "bstr", "buffer-redux", @@ -3068,7 +3428,7 @@ dependencies = [ "cast5", "cfb-mode", "chrono", - "cipher", + "cipher 0.4.4", "crc24", "curve25519-dalek", "derive_builder", @@ -3135,6 +3495,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -3169,10 +3540,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" [[package]] -name = "portable-atomic" -version = "1.5.1" +name = "polling" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bccab0e7fd7cc19f820a1c8c91720af652d0c88dc9664dd72aef2614f04af3b" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf63fa624ab313c11656b4cda960bfc46c410187ad493c41f6ba2d8c1e991c9e" +dependencies = [ + "cfg-if", + "concurrent-queue", + "pin-project-lite", + "rustix 0.38.28", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" [[package]] name = "powerfmt" @@ -3195,6 +3596,16 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -3221,18 +3632,19 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] [[package]] name = "process-lib" -version = "0.1.0" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "651bb0e9f091cf2ff67fbc6151d92c97bb2af864bfdbb85def0442250e10562b" dependencies = [ - "anyhow", "log", "once_cell", "thiserror", @@ -3328,15 +3740,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -3426,20 +3829,20 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.9", + "rustls 0.21.10", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "system-configuration", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 0.25.3", "winreg 0.50.0", ] @@ -3480,9 +3883,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.5" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", "getrandom", @@ -3509,9 +3912,9 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" [[package]] name = "rsa" -version = "0.9.4" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3211b01eea83d80687da9eef70e39d65144a3894866a5153a2723e425a157f" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" dependencies = [ "const-oid", "digest", @@ -3558,15 +3961,29 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.25" +version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ "bitflags 2.4.1", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.48.0", + "linux-raw-sys 0.4.12", + "windows-sys 0.52.0", ] [[package]] @@ -3583,16 +4000,30 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.9" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring 0.17.5", - "rustls-webpki", + "ring 0.17.7", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6b63262c9fcac8659abfaa96cac103d28166d3ff3eaf8f412e19f3ae9e5a48" +dependencies = [ + "log", + "ring 0.17.7", + "rustls-pki-types", + "rustls-webpki 0.102.0", + "subtle", + "zeroize", +] + [[package]] name = "rustls-connector" version = "0.16.1" @@ -3626,13 +4057,30 @@ dependencies = [ "base64 0.21.5", ] +[[package]] +name = "rustls-pki-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7673e0aa20ee4937c6aacfc12bb8341cfbf054cdd21df6bec5fd0629fe9339b" + [[package]] name = "rustls-webpki" version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.5", + "ring 0.17.7", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2635c8bc2b88d367767c5de8ea1d8db9af3f6219eba28442242d9ab81d1b89" +dependencies = [ + "ring 0.17.7", + "rustls-pki-types", "untrusted 0.9.0", ] @@ -3644,9 +4092,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "safemem" @@ -3684,7 +4132,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.5", + "ring 0.17.7", "untrusted 0.9.0", ] @@ -3704,15 +4152,34 @@ dependencies = [ [[package]] name = "secret-lib" -version = "0.1.0" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5064a00687ed7f19a36e30a8b2695739d60f3d423f229097c8d110f9f76a8a96" dependencies = [ - "anyhow", "keyring-lib", - "log", "process-lib", "thiserror", ] +[[package]] +name = "secret-service" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da1a5ad4d28c03536f82f77d9f36603f5e37d8869ac98f0a750d5b5686d8d95" +dependencies = [ + "aes 0.7.5", + "block-modes", + "futures-util", + "generic-array", + "hkdf", + "num", + "once_cell", + "rand", + "serde", + "sha2", + "zbus", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -3783,6 +4250,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "serde_spanned" version = "0.6.4" @@ -3861,18 +4339,9 @@ dependencies = [ [[package]] name = "shellexpand-utils" -version = "0.1.0" -dependencies = [ - "log", - "shellexpand", - "thiserror", -] - -[[package]] -name = "shellexpand-utils" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b44114d15a72740d3049e739929d0f4af335ba2eb855ea0f1fa2bfb4d7066215" +checksum = "ab3789fb50c0dc67a1171515bc5709c30a8938ee210e19c02be1e8c1f6f881bc" dependencies = [ "log", "shellexpand", @@ -3963,9 +4432,9 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "spki" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -4077,9 +4546,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", - "fastrand", - "redox_syscall 0.4.1", - "rustix", + "fastrand 2.0.1", + "redox_syscall", + "rustix 0.38.28", "windows-sys 0.48.0", ] @@ -4102,16 +4571,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "terminal_size" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" -dependencies = [ - "rustix", - "windows-sys 0.48.0", -] - [[package]] name = "thiserror" version = "1.0.50" @@ -4180,9 +4639,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.34.0" +version = "1.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" dependencies = [ "backtrace", "bytes", @@ -4193,6 +4652,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.5", "tokio-macros", + "tracing", "windows-sys 0.48.0", ] @@ -4213,7 +4673,18 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.9", + "rustls 0.21.10", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.1", + "rustls-pki-types", "tokio", ] @@ -4343,9 +4814,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "twofish" @@ -4353,7 +4824,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78e83a30223c757c3947cd144a31014ff04298d8719ae10d03c31c0448c8013" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -4363,10 +4834,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] -name = "unicode-bidi" -version = "0.3.13" +name = "uds_windows" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-bom" @@ -4481,6 +4962,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + [[package]] name = "walkdir" version = "2.4.0" @@ -4508,9 +4995,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -4518,9 +5005,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", @@ -4533,9 +5020,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" dependencies = [ "cfg-if", "js-sys", @@ -4545,9 +5032,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4555,9 +5042,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", @@ -4568,15 +5055,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "web-sys" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" dependencies = [ "js-sys", "wasm-bindgen", @@ -4588,7 +5075,7 @@ version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring 0.17.5", + "ring 0.17.7", "untrusted 0.9.0", ] @@ -4598,6 +5085,15 @@ version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" +[[package]] +name = "webpki-roots" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2cfda980f21be5a7ed2eadb3e6fe074d56022bea2cdeb1a62eb220fc04188" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "widestring" version = "1.0.2" @@ -4677,6 +5173,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -4707,6 +5212,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -4719,6 +5239,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -4731,6 +5257,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -4743,6 +5275,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -4755,6 +5293,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -4767,6 +5311,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -4779,6 +5329,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -4792,10 +5348,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "winnow" -version = "0.5.19" +name = "windows_x86_64_msvc" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67b5f0a4e7a27a64c651977932b9dc5667ca7fc31ac44b03ed37a0cf42fdfff" dependencies = [ "memchr", ] @@ -4831,6 +5393,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xdg-home" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" +dependencies = [ + "nix", + "winapi", +] + [[package]] name = "z-base-32" version = "0.1.3" @@ -4838,19 +5410,80 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80a0d98613370af88e15bd2047702d7c78c8c6aba44403eb227c8ad706871f92" [[package]] -name = "zerocopy" -version = "0.7.26" +name = "zbus" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" +checksum = "31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "byteorder", + "derivative", + "enumflags2", + "event-listener 2.5.3", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "winapi", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.7.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "306dca4455518f1f31635ec308b6b3e4eb1b11758cefafc782827d0aa7acb5c7" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.26" +version = "0.7.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" +checksum = "be912bf68235a88fbefd1b73415cb218405958d1655b2ece9035a19920bdf6ba" dependencies = [ "proc-macro2", "quote", @@ -4883,7 +5516,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ - "aes", + "aes 0.8.3", "byteorder", "bzip2", "constant_time_eq", @@ -4925,3 +5558,41 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zvariant" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/Cargo.toml b/Cargo.toml index b0c6de5..7133954 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "himalaya" -description = "CLI to manage emails." -version = "0.9.0" +description = "CLI to manage emails" +version = "1.0.0-beta" authors = ["soywod "] edition = "2021" license = "MIT" @@ -36,135 +36,45 @@ pgp-commands = ["pgp", "mml-lib/pgp-commands", "email-lib/pgp-commands"] pgp-gpg = ["pgp", "mml-lib/pgp-gpg", "email-lib/pgp-gpg"] pgp-native = ["pgp", "mml-lib/pgp-native", "email-lib/pgp-native"] -# dev dependencies +[dev-dependencies] +async-trait = "0.1" +tempfile = "3.3" -[dev-dependencies.async-trait] -version = "0.1" - -[dev-dependencies.tempfile] -version = "3.3" - -# dependencies - -[dependencies.anyhow] -version = "1.0" - -[dependencies.async-trait] -version = "0.1" - -[dependencies.atty] -version = "0.2" - -[dependencies.chrono] -version = "0.4.24" - -[dependencies.clap] -version = "4.4" -features = ["derive", "wrap_help"] - -[dependencies.clap_complete] -version = "4.4" - -[dependencies.clap_mangen] -version = "0.2" - -[dependencies.console] -version = "0.15.2" - -[dependencies.dialoguer] -version = "0.10.2" - -[dependencies.dirs] -version = "4.0.0" - -[dependencies.email_address] -version = "0.2.4" - -[dependencies.env_logger] -version = "0.10" - -[dependencies.erased-serde] -version = "0.3" - -[dependencies.indicatif] -version = "0.17" - -[dependencies.log] -version = "0.4" - -[dependencies.md5] -version = "0.7.0" - -[dependencies.once_cell] -version = "1.16.0" - -[dependencies.email-lib] -# version = "=0.15.3" -default-features = false -path = "/home/soywod/sourcehut/pimalaya/email" - -[dependencies.keyring-lib] -# version = "=0.1.0" -path = "/home/soywod/sourcehut/pimalaya/keyring" - -[dependencies.mail-builder] -version = "0.3" - -[dependencies.oauth-lib] -# version = "=0.1.0" -path = "/home/soywod/sourcehut/pimalaya/oauth" - -[dependencies.process-lib] -# version = "=0.1.0" -path = "/home/soywod/sourcehut/pimalaya/process" - -[dependencies.mml-lib] -# version = "=1.0.1" -default-features = false -features = ["compiler", "interpreter"] -path = "/home/soywod/sourcehut/pimalaya/mml" - -[dependencies.secret-lib] -# version = "=0.1.0" -path = "/home/soywod/sourcehut/pimalaya/secret" - -[dependencies.serde] -version = "1.0" -features = ["derive"] - -[dependencies.serde_json] -version = "1.0" - -[dependencies.shellexpand-utils] -# version = "=0.1.0" -path = "/home/soywod/sourcehut/pimalaya/shellexpand-utils" - -[dependencies.termcolor] -version = "1.1" - -[dependencies.terminal_size] -version = "0.1" - -[dependencies.tokio] -version = "1.23" -default-features = false -features = ["macros", "rt-multi-thread"] - -[dependencies.toml] -version = "0.7.4" - -[dependencies.toml_edit] -version = "0.19.8" - -[dependencies.unicode-width] -version = "0.1" - -[dependencies.url] -version = "2.2" - -[dependencies.uuid] -version = "0.8" -features = ["v4"] +[dependencies] +anyhow = "1" +async-trait = "0.1" +chrono = "0.4.24" +clap = { version = "4.4", features = ["derive"] } +clap_complete = "4.4" +clap_mangen = "0.2" +console = "0.15.2" +dialoguer = "0.10.2" +dirs = "4.0" +email-lib = { version = "=0.16.0", default-features = false } +email_address = "0.2.4" +env_logger = "0.8" +erased-serde = "0.3" +indicatif = "0.17" +keyring-lib = "=0.2.0" +log = "0.4" +mail-builder = "0.3" +md5 = "0.7.0" +mml-lib = { version = "=1.0.2", default-features = false } +oauth-lib = "=0.1.0" +once_cell = "1.16" +process-lib = "=0.2.0" +secret-lib = "=0.2.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +shellexpand-utils = "=0.2.0" +termcolor = "1.1" +terminal_size = "0.1" +tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] } +toml = "0.7.4" +toml_edit = "0.19.8" +unicode-width = "0.1" +url = "2.2" +uuid = { version = "0.8", features = ["v4"] } [target.'cfg(target_env = "musl")'.dependencies.rusqlite] version = "0.29" @@ -175,4 +85,4 @@ version = "0.29" features = ["bundled"] [target.'cfg(not(windows))'.dependencies.coredump] -version = "=0.1.2" +version = "=0.1.2" \ No newline at end of file diff --git a/src/account/wizard.rs b/src/account/wizard.rs index c470011..cb31df9 100644 --- a/src/account/wizard.rs +++ b/src/account/wizard.rs @@ -1,11 +1,12 @@ use anyhow::{bail, Result}; -use dialoguer::Input; +use dialoguer::{Confirm, Input}; use email_address::EmailAddress; use crate::{ backend::{self, config::BackendConfig, BackendKind}, config::wizard::THEME, message::config::{MessageConfig, MessageSendConfig}, + wizard_prompt, }; use super::TomlAccountConfig; @@ -35,6 +36,14 @@ pub(crate) async fn configure() -> Result> { .interact()?, ); + config.downloads_dir = Some( + Input::with_theme(&*THEME) + .with_prompt("Downloads directory") + .default(String::from("~/Downloads")) + .interact()? + .into(), + ); + match backend::wizard::configure(&account_name, &config.email).await? { Some(BackendConfig::Maildir(mdir_config)) => { config.maildir = Some(mdir_config); @@ -78,5 +87,15 @@ pub(crate) async fn configure() -> Result> { _ => (), }; + config.sync = Some( + Confirm::new() + .with_prompt(wizard_prompt!( + "Do you need an offline access to your account?" + )) + .default(false) + .interact_opt()? + .unwrap_or_default(), + ); + Ok(Some((account_name, config))) } diff --git a/src/config/wizard.rs b/src/config/wizard.rs index 8d43942..e0f2a5c 100644 --- a/src/config/wizard.rs +++ b/src/config/wizard.rs @@ -1,7 +1,7 @@ use anyhow::Result; use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select}; use once_cell::sync::Lazy; -use shellexpand_utils::shellexpand_path; +use shellexpand_utils::expand; use std::{fs, io, path::PathBuf, process}; use toml_edit::{Document, Item}; @@ -94,7 +94,7 @@ pub(crate) async fn configure(path: PathBuf) -> Result { )) .default(path.to_string_lossy().to_string()) .interact()?; - let path = shellexpand_path(&path); + let path = expand::path(&path); println!("Writing the configuration to {path:?}…"); diff --git a/src/email/message/attachment/command/download.rs b/src/email/message/attachment/command/download.rs index 13be174..cb599dc 100644 --- a/src/email/message/attachment/command/download.rs +++ b/src/email/message/attachment/command/download.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use clap::Parser; use log::info; -use std::fs; +use std::{fs, path::PathBuf}; use uuid::Uuid; use crate::{ @@ -67,6 +67,8 @@ impl AttachmentDownloadCommand { for attachment in attachments { let filename = attachment .filename + .map(PathBuf::from) + .and_then(|f| f.file_name().map(|f| f.to_string_lossy().to_string())) .unwrap_or_else(|| Uuid::new_v4().to_string()); let filepath = account_config.download_fpath(&filename)?; printer.print_log(format!("Downloading {:?}…", filepath))?; diff --git a/src/email/message/command/save.rs b/src/email/message/command/save.rs index b20f919..e0e018e 100644 --- a/src/email/message/command/save.rs +++ b/src/email/message/command/save.rs @@ -1,8 +1,7 @@ use anyhow::Result; -use atty::Stream; use clap::Parser; use log::info; -use std::io::{self, BufRead}; +use std::io::{self, BufRead, IsTerminal}; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, @@ -40,7 +39,7 @@ impl MessageSaveCommand { config.clone().into_account_configs(account, cache)?; let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - let is_tty = atty::is(Stream::Stdin); + let is_tty = io::stdin().is_terminal(); let is_json = printer.is_json(); let msg = if is_tty || is_json { self.message.raw() diff --git a/src/email/message/command/send.rs b/src/email/message/command/send.rs index d8e1930..dc87267 100644 --- a/src/email/message/command/send.rs +++ b/src/email/message/command/send.rs @@ -1,9 +1,8 @@ use anyhow::Result; -use atty::Stream; use clap::Parser; use email::flag::Flag; use log::info; -use std::io::{self, BufRead}; +use std::io::{self, BufRead, IsTerminal}; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, @@ -38,7 +37,7 @@ impl MessageSendCommand { let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; let folder = account_config.sent_folder_alias()?; - let is_tty = atty::is(Stream::Stdin); + let is_tty = io::stdin().is_terminal(); let is_json = printer.is_json(); let msg = if is_tty || is_json { self.message.raw() diff --git a/src/email/message/template/command/save.rs b/src/email/message/template/command/save.rs index 5e1b1c6..c95a8c7 100644 --- a/src/email/message/template/command/save.rs +++ b/src/email/message/template/command/save.rs @@ -1,9 +1,8 @@ use anyhow::Result; -use atty::Stream; use clap::Parser; use log::info; use mml::MmlCompilerBuilder; -use std::io::{self, BufRead}; +use std::io::{self, BufRead, IsTerminal}; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, @@ -44,7 +43,7 @@ impl TemplateSaveCommand { config.clone().into_account_configs(account, cache)?; let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - let is_tty = atty::is(Stream::Stdin); + let is_tty = io::stdin().is_terminal(); let is_json = printer.is_json(); let tpl = if is_tty || is_json { self.template.raw() diff --git a/src/email/message/template/command/send.rs b/src/email/message/template/command/send.rs index c62d642..917affa 100644 --- a/src/email/message/template/command/send.rs +++ b/src/email/message/template/command/send.rs @@ -1,10 +1,9 @@ use anyhow::Result; -use atty::Stream; use clap::Parser; use email::flag::Flag; use log::info; use mml::MmlCompilerBuilder; -use std::io::{self, BufRead}; +use std::io::{self, BufRead, IsTerminal}; use crate::{ account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, @@ -41,7 +40,7 @@ impl TemplateSendCommand { let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; let folder = account_config.sent_folder_alias()?; - let is_tty = atty::is(Stream::Stdin); + let is_tty = io::stdin().is_terminal(); let is_json = printer.is_json(); let tpl = if is_tty || is_json { self.template.raw() diff --git a/src/output/output.rs b/src/output/output.rs index 278c59d..9fbc1ff 100644 --- a/src/output/output.rs +++ b/src/output/output.rs @@ -1,8 +1,11 @@ use anyhow::{anyhow, Error, Result}; -use atty::Stream; use clap::ValueEnum; use serde::Serialize; -use std::{fmt, str::FromStr}; +use std::{ + fmt, + io::{self, IsTerminal}, + str::FromStr, +}; use termcolor::ColorChoice; /// Represents the available output formats. @@ -78,7 +81,7 @@ impl From for ColorChoice { ColorFmt::Always => Self::Always, ColorFmt::Ansi => Self::AlwaysAnsi, ColorFmt::Auto => { - if atty::is(Stream::Stdout) { + if io::stdout().is_terminal() { // Otherwise let's `termcolor` decide by // inspecting the environment. From the [doc]: // From 2e0ec913cf08216a6bd3fce96020ea9072215cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Mon, 11 Dec 2023 18:38:00 +0100 Subject: [PATCH 28/29] refactor configs to match new nested api from lib --- Cargo.lock | 64 +++++----- Cargo.toml | 10 +- README.md | 29 ++--- config.sample.toml | 72 +++++++----- src/account/command/configure.rs | 17 +-- src/account/command/list.rs | 5 +- src/account/config.rs | 111 ++++-------------- src/account/wizard.rs | 24 ++-- src/backend/mod.rs | 4 +- src/config/mod.rs | 83 ++++--------- src/email/envelope/command/list.rs | 4 +- src/email/envelope/config.rs | 3 + .../message/attachment/command/download.rs | 9 +- src/email/message/command/mailto.rs | 4 +- src/email/message/command/send.rs | 4 +- src/email/message/config.rs | 19 ++- src/email/message/template/command/save.rs | 2 +- src/email/message/template/command/send.rs | 6 +- src/folder/command/list.rs | 2 +- src/folder/config.rs | 6 + src/imap/wizard.rs | 12 +- src/smtp/wizard.rs | 12 +- src/ui/editor.rs | 4 +- 23 files changed, 214 insertions(+), 292 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 663cb49..181a135 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,7 +272,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -307,7 +307,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -679,7 +679,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -898,7 +898,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1192,9 +1192,9 @@ dependencies = [ [[package]] name = "email-lib" -version = "0.16.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e3f51881639d657795ea95cd2fb065b8d9249912100b297730133ffbf38e0a5" +checksum = "743371f76482a94403ce0ab49da129065fc84cbcf9ed126524882c6ed5389efc" dependencies = [ "advisory-lock", "anyhow", @@ -1225,6 +1225,7 @@ dependencies = [ "rustls 0.22.1", "rustls-native-certs", "secret-lib", + "serde", "shellexpand-utils", "thiserror", "tokio", @@ -1269,7 +1270,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1290,7 +1291,7 @@ checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1557,7 +1558,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -2734,15 +2735,17 @@ dependencies = [ [[package]] name = "keyring-lib" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2271300047bf0f2f4c4b6c51cbf7f848621aa373a3e80742bbdc0148110578" +checksum = "2d5fa6d6a6a7d27d09c3e5e6c0371d97dd4cc3b0208eaba9f26dae728f1ae072" dependencies = [ "keyring", "log", "once_cell", "secret-service", + "serde", "thiserror", + "tokio", ] [[package]] @@ -3011,9 +3014,9 @@ dependencies = [ [[package]] name = "mml-lib" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bda7bfafd584f65bc7bb85a60be2a6ccf6f384f1e1d27b697d8b6524869213" +checksum = "a5498df8f63c5c204deae46529a6cbddbe7f5cfe70723165e6fde42e8e2490df" dependencies = [ "async-recursion", "chumsky", @@ -3026,6 +3029,7 @@ dependencies = [ "pgp-lib", "process-lib", "secret-lib", + "serde", "shellexpand-utils", "thiserror", "tree_magic_mini", @@ -3129,7 +3133,7 @@ checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -3641,12 +3645,13 @@ dependencies = [ [[package]] name = "process-lib" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "651bb0e9f091cf2ff67fbc6151d92c97bb2af864bfdbb85def0442250e10562b" +checksum = "76cb01de71b99c9d36dacf51218b950e03f80a86ee5f910ff723a1693796bad3" dependencies = [ "log", "once_cell", + "serde", "thiserror", "tokio", ] @@ -4152,12 +4157,13 @@ dependencies = [ [[package]] name = "secret-lib" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5064a00687ed7f19a36e30a8b2695739d60f3d423f229097c8d110f9f76a8a96" +checksum = "b06c6eda723fc17a853234defb5784ec2b3377d9bce8a6577c89788f1b6dc117" dependencies = [ "keyring-lib", "process-lib", + "serde", "thiserror", ] @@ -4226,7 +4232,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -4258,7 +4264,7 @@ checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -4490,9 +4496,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "13fa70a4ee923979ffb522cacce59d34421ebdea5625e1073c4326ef9d2dd42e" dependencies = [ "proc-macro2", "quote", @@ -4588,7 +4594,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -4664,7 +4670,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -4786,7 +4792,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -5014,7 +5020,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", "wasm-bindgen-shared", ] @@ -5048,7 +5054,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5487,7 +5493,7 @@ checksum = "be912bf68235a88fbefd1b73415cb218405958d1655b2ece9035a19920bdf6ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -5507,7 +5513,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7133954..0394966 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,20 +50,20 @@ clap_mangen = "0.2" console = "0.15.2" dialoguer = "0.10.2" dirs = "4.0" -email-lib = { version = "=0.16.0", default-features = false } +email-lib = { version = "=0.17.1", default-features = false } email_address = "0.2.4" env_logger = "0.8" erased-serde = "0.3" indicatif = "0.17" -keyring-lib = "=0.2.0" +keyring-lib = "=0.3.0" log = "0.4" mail-builder = "0.3" md5 = "0.7.0" -mml-lib = { version = "=1.0.2", default-features = false } +mml-lib = { version = "=1.0.3", default-features = false } oauth-lib = "=0.1.0" once_cell = "1.16" -process-lib = "=0.2.0" -secret-lib = "=0.2.0" +process-lib = "=0.3.0" +secret-lib = "=0.3.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" shellexpand-utils = "=0.2.0" diff --git a/README.md b/README.md index fa85dc8..0cbd544 100644 --- a/README.md +++ b/README.md @@ -85,35 +85,22 @@ Please read the [documentation](https://pimalaya.org/himalaya/cli/configuration/ ## Contributing -If you find a **bug** that [does not exist yet](https://todo.sr.ht/~soywod/pimalaya), please send an email at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht). +If you want to **report a bug** that [does not exist yet](https://todo.sr.ht/~soywod/pimalaya), please send an email at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht). -If you have a **question**, please send an email at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht). +If you want to **propose a feature** or **fix a bug**, please send a patch at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht) using [git send-email](https://git-scm.com/docs/git-send-email). Follow [this guide](https://git-send-email.io/) to configure git properly. -If you want to **propose a feature** or **fix a bug**, please send a patch at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht) using [git send-email](https://git-scm.com/docs/git-send-email) (see [this guide](https://git-send-email.io/) on how to configure it). +If you just want to **discuss** about the project, feel free to join the [Matrix](https://matrix.org/) workspace [#pimalaya.general](https://matrix.to/#/#pimalaya.general:matrix.org) or contact me directly [@soywod](https://matrix.to/#/@soywod:matrix.org). You can also use the mailing list [[send an email](mailto:~soywod/pimalaya@lists.sr.ht)|[subscribe](mailto:~soywod/pimalaya+subscribe@lists.sr.ht)|[unsubscribe](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht)]. -If you want to **subscribe** to the mailing list, please send an email at [~soywod/pimalaya+subscribe@lists.sr.ht](mailto:~soywod/pimalaya+subscribe@lists.sr.ht). - -If you want to **unsubscribe** to the mailing list, please send an email at [~soywod/pimalaya+unsubscribe@lists.sr.ht](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht). - -If you want to **discuss** about the project, feel free to join the [Matrix](https://matrix.org/) workspace [#pimalaya.himalaya](https://matrix.to/#/#pimalaya.himalaya:matrix.org) or contact me directly [@soywod](https://matrix.to/#/@soywod:matrix.org). - -## Credits +## Sponsoring [![nlnet](https://nlnet.nl/logo/banner-160x60.png)](https://nlnet.nl/project/Himalaya/index.html) -Special thanks to the [nlnet](https://nlnet.nl/project/Himalaya/index.html) foundation that helped Himalaya to receive financial support from the [NGI Assure](https://www.ngi.eu/ngi-projects/ngi-assure/) program of the European Commission in September, 2022. +Special thanks to the [NLnet foundation](https://nlnet.nl/project/Himalaya/index.html) and the [European Commission](https://www.ngi.eu/) that helped the project to receive financial support from: -- [IMAP RFC3501](https://tools.ietf.org/html/rfc3501) -- [Iris](https://github.com/soywod/iris.vim), the himalaya predecessor -- [isync](https://isync.sourceforge.io/), an email synchronizer for offline usage -- [NeoMutt](https://neomutt.org/), an email terminal user interface -- [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other email terminal user interface -- [mutt-wizard](https://github.com/LukeSmithxyz/mutt-wizard), a tool over NeoMutt and isync -- [rust-imap](https://github.com/jonhoo/rust-imap), a Rust IMAP library -- [lettre](https://github.com/lettre/lettre), a Rust mailer library -- [mailparse](https://github.com/staktrace/mailparse), a Rust MIME email parser. +- [NGI Assure](https://nlnet.nl/assure/) in 2022 +- [NGI Zero Untrust](https://nlnet.nl/entrust/) in 2023 -## Sponsoring +If you appreciate the project, feel free to donate using one of the following providers: [![GitHub](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors)](https://github.com/sponsors/soywod) [![PayPal](https://img.shields.io/badge/-PayPal-0079c1?logo=PayPal&logoColor=ffffff)](https://www.paypal.com/paypalme/soywod) diff --git a/config.sample.toml b/config.sample.toml index ca17825..31da751 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -1,30 +1,58 @@ [example] +# Make this account the default one to use when no account is given to +# commands. default = true -# The display-name and email are used to build the full email address: -# "My example account" +# The display-name and the email are used to build the full email +# address: "My example account" display-name = "My example account" email = "example@localhost" -sync = true -sync-dir = "/tmp/himalaya-sync-example" +# The signature can be a string or a path to a file. +signature = "Regards," +signature-delim = "-- \n" -# The default backend used for all the features like adding folders, +# Enable the synchronization for this account. Running the command +# `account sync example` will synchronize all folders and all emails +# to a local Maildir at `$XDG_DATA_HOME/himalaya/example`. +sync.enable = true + +# Override the default Maildir path for synchronization. +sync.dir = "/tmp/himalaya-sync-example" + +# Default backend used for all the features like adding folders, # listing envelopes or copying messages. backend = "imap" +envelope.list.page-size = 10 +envelope.list.datetime-fmt = "%F %R%:z" + +# Date are converted to the user's local timezone. +envelope.list.datetime-local-tz = true + +# Override the backend used for listing envelopes. +envelope.list.backend = "imap" + +# Override the backend used for sending messages. +message.send.backend = "smtp" + # IMAP config imap.host = "localhost" imap.port = 3143 imap.login = "example@localhost" imap.ssl = false imap.starttls = false -imap.insecure = true -imap.auth = "passwd" -imap.passwd.keyring = "example-passwd" +imap.auth = "passwd" # or oauth2 -# Override the backend used for sending messages. -message.send.backend = "smtp" +# Get password from the raw string (not safe) +# imap.passwd.raw = "password" + +# Get password from a shell command +imap.passwd.cmd = ["echo example-imap-password", "cat"] + +# Get password from your system keyring using secret service +# Keyring secrets can be (re)set with the command `account configure example` +# imap.passwd.keyring = "example-imap-password" # SMTP config smtp.host = "localhost" @@ -32,25 +60,9 @@ smtp.port = 3025 smtp.login = "example@localhost" smtp.ssl = false smtp.starttls = false -smtp.insecure = true smtp.auth = "passwd" -smtp.passwd.raw = "example" +smtp.passwd.raw = "password" -[example-2] -display-name = "My example account 2" -email = "example2@localhost" - -backend = "imap" - -imap.host = "localhost" -imap.port = 3143 -imap.login = "example2@localhost" -imap.ssl = false -imap.starttls = false -imap.insecure = true -imap.auth = "passwd" -imap.passwd.raw = "example" - -message.send.backend = "sendmail" - -sendmail.cmd = "sendmail" +# PGP needs to be enabled with one of those cargo feature: +# pgp-commands, pgp-gpg or pgp-native +# pgp.backend = "gpg" diff --git a/src/account/command/configure.rs b/src/account/command/configure.rs index 616e97c..49012d9 100644 --- a/src/account/command/configure.rs +++ b/src/account/command/configure.rs @@ -44,8 +44,8 @@ impl AccountConfigureCommand { #[cfg(feature = "imap")] if let Some(ref config) = account_config.imap { let reset = match &config.auth { - ImapAuthConfig::Passwd(config) => config.reset(), - ImapAuthConfig::OAuth2(config) => config.reset(), + ImapAuthConfig::Passwd(config) => config.reset().await, + ImapAuthConfig::OAuth2(config) => config.reset().await, }; if let Err(err) = reset { warn!("error while resetting imap secrets: {err}"); @@ -56,8 +56,8 @@ impl AccountConfigureCommand { #[cfg(feature = "smtp")] if let Some(ref config) = account_config.smtp { let reset = match &config.auth { - SmtpAuthConfig::Passwd(config) => config.reset(), - SmtpAuthConfig::OAuth2(config) => config.reset(), + SmtpAuthConfig::Passwd(config) => config.reset().await, + SmtpAuthConfig::OAuth2(config) => config.reset().await, }; if let Err(err) = reset { warn!("error while resetting smtp secrets: {err}"); @@ -67,7 +67,7 @@ impl AccountConfigureCommand { #[cfg(feature = "pgp")] if let Some(ref config) = account_config.pgp { - account_config.pgp.reset().await?; + config.reset().await?; } } @@ -100,10 +100,11 @@ impl AccountConfigureCommand { } #[cfg(feature = "pgp")] - if let Some(ref config) = config.pgp { + if let Some(ref config) = account_config.pgp { config - .pgp - .configure(&config.email, || prompt_passwd("PGP secret key password")) + .configure(&account_config.email, || { + prompt_passwd("PGP secret key password") + }) .await?; } diff --git a/src/account/command/list.rs b/src/account/command/list.rs index 5b6a72a..1c8e4ce 100644 --- a/src/account/command/list.rs +++ b/src/account/command/list.rs @@ -28,10 +28,7 @@ impl AccountListCommand { printer.print_table( Box::new(accounts), PrintTableOpts { - format: config - .email_reading_format - .as_ref() - .unwrap_or(&Default::default()), + format: &Default::default(), max_width: self.table.max_width, }, ) diff --git a/src/account/config.rs b/src/account/config.rs index a916dbb..85078be 100644 --- a/src/account/config.rs +++ b/src/account/config.rs @@ -4,26 +4,21 @@ //! account in the accounts section of the user configuration file. #[cfg(feature = "pgp")] -use email::account::PgpConfig; +use email::account::config::pgp::PgpConfig; #[cfg(feature = "imap")] use email::imap::config::ImapConfig; #[cfg(feature = "smtp")] use email::smtp::config::SmtpConfig; use email::{ - email::config::{EmailHooks, EmailTextPlainFormat}, - folder::sync::FolderSyncStrategy, - maildir::config::MaildirConfig, + account::sync::config::SyncConfig, maildir::config::MaildirConfig, sendmail::config::SendmailConfig, }; use serde::{Deserialize, Serialize}; -use std::{ - collections::{HashMap, HashSet}, - path::PathBuf, -}; +use std::{collections::HashSet, path::PathBuf}; use crate::{ - backend::BackendKind, config::prelude::*, envelope::config::EnvelopeConfig, - flag::config::FlagConfig, folder::config::FolderConfig, message::config::MessageConfig, + backend::BackendKind, envelope::config::EnvelopeConfig, flag::config::FlagConfig, + folder::config::FolderConfig, message::config::MessageConfig, }; /// Represents all existing kind of account config. @@ -34,93 +29,29 @@ pub struct TomlAccountConfig { pub email: String, pub display_name: Option, - pub signature_delim: Option, pub signature: Option, + pub signature_delim: Option, pub downloads_dir: Option, - pub folder_listing_page_size: Option, - pub folder_aliases: Option>, - - pub email_listing_page_size: Option, - pub email_listing_datetime_fmt: Option, - pub email_listing_datetime_local_tz: Option, - pub email_reading_headers: Option>, - #[serde( - default, - with = "OptionEmailTextPlainFormatDef", - skip_serializing_if = "Option::is_none" - )] - pub email_reading_format: Option, - pub email_writing_headers: Option>, - pub email_sending_save_copy: Option, - #[serde( - default, - with = "OptionEmailHooksDef", - skip_serializing_if = "Option::is_none" - )] - pub email_hooks: Option, - - pub sync: Option, - pub sync_dir: Option, - #[serde( - default, - with = "OptionFolderSyncStrategyDef", - skip_serializing_if = "Option::is_none" - )] - pub sync_folders_strategy: Option, - - pub backend: Option, - + pub sync: Option, pub folder: Option, pub envelope: Option, pub flag: Option, pub message: Option, - - #[cfg(feature = "imap")] - #[serde( - default, - with = "OptionImapConfigDef", - skip_serializing_if = "Option::is_none" - )] - pub imap: Option, - - #[serde( - default, - with = "OptionMaildirConfigDef", - skip_serializing_if = "Option::is_none" - )] - pub maildir: Option, - - #[cfg(feature = "notmuch")] - #[serde( - default, - with = "OptionNotmuchConfigDef", - skip_serializing_if = "Option::is_none" - )] - pub notmuch: Option, - - #[cfg(feature = "smtp")] - #[serde( - default, - with = "OptionSmtpConfigDef", - skip_serializing_if = "Option::is_none" - )] - pub smtp: Option, - - #[serde( - default, - with = "OptionSendmailConfigDef", - skip_serializing_if = "Option::is_none" - )] - pub sendmail: Option, - #[cfg(feature = "pgp")] - #[serde( - default, - with = "OptionPgpConfigDef", - skip_serializing_if = "Option::is_none" - )] pub pgp: Option, + + pub backend: Option, + #[cfg(feature = "maildir")] + pub maildir: Option, + #[cfg(feature = "imap")] + pub imap: Option, + #[cfg(feature = "notmuch")] + pub notmuch: Option, + #[cfg(feature = "smtp")] + pub smtp: Option, + #[cfg(feature = "sendmail")] + pub sendmail: Option, } impl TomlAccountConfig { @@ -207,7 +138,7 @@ impl TomlAccountConfig { pub fn add_raw_message_kind(&self) -> Option<&BackendKind> { self.message .as_ref() - .and_then(|msg| msg.add.as_ref()) + .and_then(|msg| msg.write.as_ref()) .and_then(|add| add.backend.as_ref()) .or_else(|| self.backend.as_ref()) } @@ -223,7 +154,7 @@ impl TomlAccountConfig { pub fn get_messages_kind(&self) -> Option<&BackendKind> { self.message .as_ref() - .and_then(|message| message.get.as_ref()) + .and_then(|message| message.read.as_ref()) .and_then(|get| get.backend.as_ref()) .or_else(|| self.backend.as_ref()) } diff --git a/src/account/wizard.rs b/src/account/wizard.rs index cb31df9..aff4bf4 100644 --- a/src/account/wizard.rs +++ b/src/account/wizard.rs @@ -1,5 +1,6 @@ use anyhow::{bail, Result}; use dialoguer::{Confirm, Input}; +use email::account::sync::config::SyncConfig; use email_address::EmailAddress; use crate::{ @@ -87,15 +88,20 @@ pub(crate) async fn configure() -> Result> { _ => (), }; - config.sync = Some( - Confirm::new() - .with_prompt(wizard_prompt!( - "Do you need an offline access to your account?" - )) - .default(false) - .interact_opt()? - .unwrap_or_default(), - ); + let should_configure_sync = Confirm::new() + .with_prompt(wizard_prompt!( + "Do you need an offline access to your account?" + )) + .default(false) + .interact_opt()? + .unwrap_or_default(); + + if should_configure_sync { + config.sync = Some(SyncConfig { + enable: Some(true), + ..Default::default() + }); + } Ok(Some((account_name, config))) } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index cae0b9b..05efa54 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -173,7 +173,7 @@ impl BackendBuilder { MaildirSessionBuilder::new(account_config.clone(), mdir_config.clone()) }), maildir_for_sync: Some(MaildirConfig { - root_dir: account_config.sync_dir()?, + root_dir: account_config.get_sync_dir()?, }) .filter(|_| is_maildir_for_sync_used) .map(|mdir_config| MaildirSessionBuilder::new(account_config.clone(), mdir_config)), @@ -691,7 +691,7 @@ impl Backend { id_mapper = IdMapper::new( &self.backend.account_config, folder, - self.backend.account_config.sync_dir()?, + self.backend.account_config.get_sync_dir()?, )?; } #[cfg(feature = "notmuch")] diff --git a/src/config/mod.rs b/src/config/mod.rs index 3d96fb1..3cd780d 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,14 +1,12 @@ pub mod args; -pub mod prelude; pub mod wizard; use anyhow::{anyhow, Context, Result}; use dialoguer::Confirm; use dirs::{config_dir, home_dir}; use email::{ - account::config::AccountConfig, - config::Config, - email::config::{EmailHooks, EmailTextPlainFormat}, + account::config::AccountConfig, config::Config, envelope::config::EnvelopeConfig, + folder::config::FolderConfig, message::config::MessageConfig, }; use serde::{Deserialize, Serialize}; use shellexpand_utils::{canonicalize, expand}; @@ -20,10 +18,7 @@ use std::{ }; use toml; -use crate::{ - account::config::TomlAccountConfig, backend::BackendKind, config::prelude::*, wizard_prompt, - wizard_warn, -}; +use crate::{account::config::TomlAccountConfig, backend::BackendKind, wizard_prompt, wizard_warn}; /// Represents the user config file. #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] @@ -31,32 +26,10 @@ use crate::{ pub struct TomlConfig { #[serde(alias = "name")] pub display_name: Option, - pub signature_delim: Option, pub signature: Option, + pub signature_delim: Option, pub downloads_dir: Option, - pub folder_listing_page_size: Option, - pub folder_aliases: Option>, - - pub email_listing_page_size: Option, - pub email_listing_datetime_fmt: Option, - pub email_listing_datetime_local_tz: Option, - pub email_reading_headers: Option>, - #[serde( - default, - with = "OptionEmailTextPlainFormatDef", - skip_serializing_if = "Option::is_none" - )] - pub email_reading_format: Option, - pub email_writing_headers: Option>, - pub email_sending_save_copy: Option, - #[serde( - default, - with = "OptionEmailHooksDef", - skip_serializing_if = "Option::is_none" - )] - pub email_hooks: Option, - #[serde(flatten)] pub accounts: HashMap, } @@ -203,7 +176,7 @@ impl TomlConfig { let (account_name, mut toml_account_config) = self.into_toml_account_config(account_name)?; - if let Some(true) = toml_account_config.sync { + if let Some(true) = toml_account_config.sync.as_ref().and_then(|c| c.enable) { if !disable_cache { toml_account_config.backend = Some(BackendKind::MaildirForSync); } @@ -211,22 +184,10 @@ impl TomlConfig { let config = Config { display_name: self.display_name, - signature_delim: self.signature_delim, signature: self.signature, + signature_delim: self.signature_delim, downloads_dir: self.downloads_dir, - folder_listing_page_size: self.folder_listing_page_size, - folder_aliases: self.folder_aliases, - - email_listing_page_size: self.email_listing_page_size, - email_listing_datetime_fmt: self.email_listing_datetime_fmt, - email_listing_datetime_local_tz: self.email_listing_datetime_local_tz, - email_reading_headers: self.email_reading_headers, - email_reading_format: self.email_reading_format, - email_writing_headers: self.email_writing_headers, - email_sending_save_copy: self.email_sending_save_copy, - email_hooks: self.email_hooks, - accounts: HashMap::from_iter(self.accounts.clone().into_iter().map( |(name, config)| { ( @@ -235,27 +196,23 @@ impl TomlConfig { name, email: config.email, display_name: config.display_name, - signature_delim: config.signature_delim, signature: config.signature, + signature_delim: config.signature_delim, downloads_dir: config.downloads_dir, - folder_listing_page_size: config.folder_listing_page_size, - folder_aliases: config.folder_aliases.unwrap_or_default(), - - email_listing_page_size: config.email_listing_page_size, - email_listing_datetime_fmt: config.email_listing_datetime_fmt, - email_listing_datetime_local_tz: config.email_listing_datetime_local_tz, - - email_reading_headers: config.email_reading_headers, - email_reading_format: config.email_reading_format.unwrap_or_default(), - email_writing_headers: config.email_writing_headers, - email_sending_save_copy: config.email_sending_save_copy, - email_hooks: config.email_hooks.unwrap_or_default(), - - sync: config.sync.unwrap_or_default(), - sync_dir: config.sync_dir, - sync_folders_strategy: config.sync_folders_strategy.unwrap_or_default(), - + folder: config.folder.map(|c| FolderConfig { + aliases: c.remote.aliases, + list: c.list.map(|c| c.remote), + }), + envelope: config.envelope.map(|c| EnvelopeConfig { + list: c.list.map(|c| c.remote), + }), + message: config.message.map(|c| MessageConfig { + read: c.read.map(|c| c.remote), + write: c.write.map(|c| c.remote), + send: c.send.map(|c| c.remote), + }), + sync: config.sync, #[cfg(feature = "pgp")] pgp: config.pgp, }, diff --git a/src/email/envelope/command/list.rs b/src/email/envelope/command/list.rs index 5528e00..a1a0721 100644 --- a/src/email/envelope/command/list.rs +++ b/src/email/envelope/command/list.rs @@ -58,7 +58,7 @@ impl EnvelopeListCommand { let page_size = self .page_size - .unwrap_or(account_config.email_listing_page_size()); + .unwrap_or(account_config.get_envelope_list_page_size()); let page = 1.max(self.page) - 1; let envelopes = backend.list_envelopes(folder, page_size, page).await?; @@ -66,7 +66,7 @@ impl EnvelopeListCommand { printer.print_table( Box::new(envelopes), PrintTableOpts { - format: &account_config.email_reading_format, + format: &account_config.get_message_read_format(), max_width: self.table.max_width, }, )?; diff --git a/src/email/envelope/config.rs b/src/email/envelope/config.rs index fa7f5d8..20855f8 100644 --- a/src/email/envelope/config.rs +++ b/src/email/envelope/config.rs @@ -28,6 +28,9 @@ impl EnvelopeConfig { #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct EnvelopeListConfig { pub backend: Option, + + #[serde(flatten)] + pub remote: email::envelope::list::config::EnvelopeListConfig, } impl EnvelopeListConfig { diff --git a/src/email/message/attachment/command/download.rs b/src/email/message/attachment/command/download.rs index cb599dc..c1d3b5c 100644 --- a/src/email/message/attachment/command/download.rs +++ b/src/email/message/attachment/command/download.rs @@ -65,12 +65,11 @@ impl AttachmentDownloadCommand { ))?; for attachment in attachments { - let filename = attachment + let filename: PathBuf = attachment .filename - .map(PathBuf::from) - .and_then(|f| f.file_name().map(|f| f.to_string_lossy().to_string())) - .unwrap_or_else(|| Uuid::new_v4().to_string()); - let filepath = account_config.download_fpath(&filename)?; + .unwrap_or_else(|| Uuid::new_v4().to_string()) + .into(); + let filepath = account_config.get_download_file_path(&filename)?; printer.print_log(format!("Downloading {:?}…", filepath))?; fs::write(&filepath, &attachment.body) .with_context(|| format!("cannot save attachment at {filepath:?}"))?; diff --git a/src/email/message/command/mailto.rs b/src/email/message/command/mailto.rs index a58f0ca..7a100fa 100644 --- a/src/email/message/command/mailto.rs +++ b/src/email/message/command/mailto.rs @@ -60,7 +60,7 @@ impl MessageMailtoCommand { } } - match account_config.signature() { + match account_config.find_full_signature() { Ok(Some(ref signature)) => builder = builder.text_body(body + "\n\n" + signature), Ok(None) => builder = builder.text_body(body), Err(err) => { @@ -71,7 +71,7 @@ impl MessageMailtoCommand { let tpl = account_config .generate_tpl_interpreter() - .with_show_only_headers(account_config.email_writing_headers()) + .with_show_only_headers(account_config.get_message_write_headers()) .build() .from_msg_builder(builder) .await?; diff --git a/src/email/message/command/send.rs b/src/email/message/command/send.rs index dc87267..1efceca 100644 --- a/src/email/message/command/send.rs +++ b/src/email/message/command/send.rs @@ -35,7 +35,7 @@ impl MessageSendCommand { let (toml_account_config, account_config) = config.clone().into_account_configs(account, cache)?; let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - let folder = account_config.sent_folder_alias()?; + let folder = account_config.get_sent_folder_alias()?; let is_tty = io::stdin().is_terminal(); let is_json = printer.is_json(); @@ -52,7 +52,7 @@ impl MessageSendCommand { backend.send_raw_message(msg.as_bytes()).await?; - if account_config.email_sending_save_copy.unwrap_or_default() { + if account_config.should_save_copy_sent_message() { backend .add_raw_message_with_flag(&folder, msg.as_bytes(), Flag::Seen) .await?; diff --git a/src/email/message/config.rs b/src/email/message/config.rs index 0e3f080..4d95ef4 100644 --- a/src/email/message/config.rs +++ b/src/email/message/config.rs @@ -5,12 +5,12 @@ use crate::backend::BackendKind; #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct MessageConfig { - pub add: Option, + pub write: Option, pub send: Option, pub peek: Option, - pub get: Option, + pub read: Option, pub copy: Option, - #[serde(default, rename = "move", skip_serializing_if = "Option::is_none")] + #[serde(rename = "move")] pub move_: Option, } @@ -18,7 +18,7 @@ impl MessageConfig { pub fn get_used_backends(&self) -> HashSet<&BackendKind> { let mut kinds = HashSet::default(); - if let Some(add) = &self.add { + if let Some(add) = &self.write { kinds.extend(add.get_used_backends()); } @@ -30,7 +30,7 @@ impl MessageConfig { kinds.extend(peek.get_used_backends()); } - if let Some(get) = &self.get { + if let Some(get) = &self.read { kinds.extend(get.get_used_backends()); } @@ -49,6 +49,9 @@ impl MessageConfig { #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct MessageAddConfig { pub backend: Option, + + #[serde(flatten)] + pub remote: email::message::add_raw::config::MessageWriteConfig, } impl MessageAddConfig { @@ -66,6 +69,9 @@ impl MessageAddConfig { #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct MessageSendConfig { pub backend: Option, + + #[serde(flatten)] + pub remote: email::message::send_raw::config::MessageSendConfig, } impl MessageSendConfig { @@ -100,6 +106,9 @@ impl MessagePeekConfig { #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct MessageGetConfig { pub backend: Option, + + #[serde(flatten)] + pub remote: email::message::get::config::MessageReadConfig, } impl MessageGetConfig { diff --git a/src/email/message/template/command/save.rs b/src/email/message/template/command/save.rs index c95a8c7..b893c12 100644 --- a/src/email/message/template/command/save.rs +++ b/src/email/message/template/command/save.rs @@ -60,7 +60,7 @@ impl TemplateSaveCommand { let mut compiler = MmlCompilerBuilder::new(); #[cfg(feature = "pgp")] - compiler.set_some_pgp(config.pgp.clone()); + compiler.set_some_pgp(account_config.pgp.clone()); let msg = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; backend.add_raw_message(folder, &msg).await?; diff --git a/src/email/message/template/command/send.rs b/src/email/message/template/command/send.rs index 917affa..f732212 100644 --- a/src/email/message/template/command/send.rs +++ b/src/email/message/template/command/send.rs @@ -38,7 +38,7 @@ impl TemplateSendCommand { let (toml_account_config, account_config) = config.clone().into_account_configs(account, cache)?; let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; - let folder = account_config.sent_folder_alias()?; + let folder = account_config.get_sent_folder_alias()?; let is_tty = io::stdin().is_terminal(); let is_json = printer.is_json(); @@ -57,13 +57,13 @@ impl TemplateSendCommand { let mut compiler = MmlCompilerBuilder::new(); #[cfg(feature = "pgp")] - compiler.set_some_pgp(config.pgp.clone()); + compiler.set_some_pgp(account_config.pgp.clone()); let msg = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; backend.send_raw_message(&msg).await?; - if account_config.email_sending_save_copy.unwrap_or_default() { + if account_config.should_save_copy_sent_message() { backend .add_raw_message_with_flag(&folder, &msg, Flag::Seen) .await?; diff --git a/src/folder/command/list.rs b/src/folder/command/list.rs index 8809b92..accf287 100644 --- a/src/folder/command/list.rs +++ b/src/folder/command/list.rs @@ -42,7 +42,7 @@ impl FolderListCommand { printer.print_table( Box::new(folders), PrintTableOpts { - format: &account_config.email_reading_format, + format: &account_config.get_message_read_format(), max_width: self.table.max_width, }, )?; diff --git a/src/folder/config.rs b/src/folder/config.rs index 16250f9..5f4bed4 100644 --- a/src/folder/config.rs +++ b/src/folder/config.rs @@ -10,6 +10,9 @@ pub struct FolderConfig { pub expunge: Option, pub purge: Option, pub delete: Option, + + #[serde(flatten)] + pub remote: email::folder::config::FolderConfig, } impl FolderConfig { @@ -60,6 +63,9 @@ impl FolderAddConfig { #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct FolderListConfig { pub backend: Option, + + #[serde(flatten)] + pub remote: email::folder::list::config::FolderListConfig, } impl FolderListConfig { diff --git a/src/imap/wizard.rs b/src/imap/wizard.rs index 5efccfa..a3cacd8 100644 --- a/src/imap/wizard.rs +++ b/src/imap/wizard.rs @@ -89,7 +89,8 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result { Secret::new_keyring_entry(format!("{account_name}-imap-passwd")) - .set_keyring_entry_secret(prompt_passwd("IMAP password")?)?; + .set_keyring_entry_secret(prompt_passwd("IMAP password")?) + .await?; PasswdConfig::default() } Some(idx) if SECRETS[idx] == RAW => PasswdConfig { @@ -130,7 +131,8 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result Result Result { Secret::new_keyring_entry(format!("{account_name}-smtp-passwd")) - .set_keyring_entry_secret(prompt_passwd("SMTP password")?)?; + .set_keyring_entry_secret(prompt_passwd("SMTP password")?) + .await?; PasswdConfig::default() } Some(idx) if SECRETS[idx] == RAW => PasswdConfig { @@ -130,7 +131,8 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result Result( backend.send_raw_message(&email).await?; - if config.email_sending_save_copy.unwrap_or_default() { - let sent_folder = config.sent_folder_alias()?; + if config.should_save_copy_sent_message() { + let sent_folder = config.get_sent_folder_alias()?; printer.print_log(format!("Adding email to the {} folder…", sent_folder))?; backend .add_raw_message_with_flag(&sent_folder, &email, Flag::Seen) From 3e3f111d3b42f53492c2a31be2464d74ad9dd7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Mon, 11 Dec 2023 22:01:48 +0100 Subject: [PATCH 29/29] fix typos --- config.sample.toml | 9 +++++++++ src/config/mod.rs | 2 +- src/folder/config.rs | 8 ++++---- src/imap/wizard.rs | 8 +++++++- src/smtp/wizard.rs | 8 +++++++- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/config.sample.toml b/config.sample.toml index 31da751..e7b3eff 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -20,6 +20,15 @@ sync.enable = true # Override the default Maildir path for synchronization. sync.dir = "/tmp/himalaya-sync-example" +# Define main folder aliases +folder.alias.inbox = "INBOX" +folder.alias.sent = "Sent" +folder.alias.drafts = "Drafts" +folder.alias.trash = "Trash" + +# Also define custom folder aliases +folder.alias.prev-year = "Archives/2023" + # Default backend used for all the features like adding folders, # listing envelopes or copying messages. backend = "imap" diff --git a/src/config/mod.rs b/src/config/mod.rs index 3cd780d..9fafd1f 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -201,7 +201,7 @@ impl TomlConfig { downloads_dir: config.downloads_dir, folder: config.folder.map(|c| FolderConfig { - aliases: c.remote.aliases, + aliases: c.alias, list: c.list.map(|c| c.remote), }), envelope: config.envelope.map(|c| EnvelopeConfig { diff --git a/src/folder/config.rs b/src/folder/config.rs index 5f4bed4..5b1f3c3 100644 --- a/src/folder/config.rs +++ b/src/folder/config.rs @@ -1,18 +1,18 @@ use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use crate::backend::BackendKind; #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct FolderConfig { + #[serde(alias = "aliases")] + pub alias: Option>, + pub add: Option, pub list: Option, pub expunge: Option, pub purge: Option, pub delete: Option, - - #[serde(flatten)] - pub remote: email::folder::config::FolderConfig, } impl FolderConfig { diff --git a/src/imap/wizard.rs b/src/imap/wizard.rs index a3cacd8..75674eb 100644 --- a/src/imap/wizard.rs +++ b/src/imap/wizard.rs @@ -51,13 +51,19 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result { config.ssl = Some(true); + config.starttls = Some(false); 993 } Some(idx) if PROTOCOLS[idx] == STARTTLS => { + config.ssl = Some(false); config.starttls = Some(true); 143 } - _ => 143, + _ => { + config.ssl = Some(false); + config.starttls = Some(false); + 143 + } }; config.port = Input::with_theme(&*THEME) diff --git a/src/smtp/wizard.rs b/src/smtp/wizard.rs index a6ea67a..bbc199b 100644 --- a/src/smtp/wizard.rs +++ b/src/smtp/wizard.rs @@ -51,13 +51,19 @@ pub(crate) async fn configure(account_name: &str, email: &str) -> Result { config.ssl = Some(true); + config.starttls = Some(false); 465 } Some(idx) if PROTOCOLS[idx] == STARTTLS => { + config.ssl = Some(false); config.starttls = Some(true); 587 } - _ => 25, + _ => { + config.ssl = Some(false); + config.starttls = Some(false); + 25 + } }; config.port = Input::with_theme(&*THEME)