mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-15 20:07:57 +08:00
feat: init maildir support with message command
This commit is contained in:
Generated
+156
-170
@@ -172,6 +172,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
@@ -328,7 +334,16 @@ checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.2",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -513,16 +528,6 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futf"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
|
||||
dependencies = [
|
||||
"mac",
|
||||
"new_debug_unreachable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "1.1.0"
|
||||
@@ -599,6 +604,17 @@ version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "hashify"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "149e3ea90eb5a26ad354cfe3cb7f7401b9329032d0235f2687d03a35f30e5d4c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -613,15 +629,18 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"comfy-table",
|
||||
"convert_case",
|
||||
"dirs",
|
||||
"gethostname",
|
||||
"html2text",
|
||||
"io-fs",
|
||||
"io-imap",
|
||||
"io-maildir",
|
||||
"io-process",
|
||||
"io-smtp",
|
||||
"io-stream",
|
||||
"log",
|
||||
"mail-parser",
|
||||
"mime_guess",
|
||||
"open",
|
||||
"pimalaya-toolbox",
|
||||
"rfc2047-decoder",
|
||||
@@ -632,33 +651,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html2text"
|
||||
version = "0.12.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "042a9677c258ac2952dd026bb0cd21972f00f644a5a38f5a215cb22cdaf6834e"
|
||||
dependencies = [
|
||||
"html5ever",
|
||||
"markup5ever",
|
||||
"tendril",
|
||||
"thiserror 1.0.69",
|
||||
"unicode-width 0.1.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4"
|
||||
dependencies = [
|
||||
"log",
|
||||
"mac",
|
||||
"markup5ever",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
@@ -807,6 +799,15 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-fs"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/pimalaya/io-fs#6c2305c52fdd5ec9ed05e902183fdf2942ea0590"
|
||||
dependencies = [
|
||||
"log",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-imap"
|
||||
version = "0.0.1"
|
||||
@@ -820,6 +821,20 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-maildir"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/pimalaya/io-maildir#e490e6ed98c2f5781b3e00319b61a4af7af15b1c"
|
||||
dependencies = [
|
||||
"gethostname",
|
||||
"io-fs",
|
||||
"log",
|
||||
"mail-parser",
|
||||
"memchr",
|
||||
"thiserror 2.0.18",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-process"
|
||||
version = "0.0.2"
|
||||
@@ -943,6 +958,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
@@ -1021,33 +1046,14 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "mail-parser"
|
||||
version = "0.9.4"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93c3b9e5d8b17faf573330bbc43b37d6e918c0a3bf8a88e7d0a220ebc84af9fc"
|
||||
checksum = "f82a3d6522697593ba4c683e0a6ee5a40fee93bc1a525e3cc6eeb3da11fd8897"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45"
|
||||
dependencies = [
|
||||
"log",
|
||||
"phf",
|
||||
"phf_codegen",
|
||||
"string_cache",
|
||||
"string_cache_codegen",
|
||||
"tendril",
|
||||
"hashify",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1065,6 +1071,22 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@@ -1088,12 +1110,6 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -1240,44 +1256,6 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pimalaya-toolbox"
|
||||
version = "0.0.4"
|
||||
@@ -1324,9 +1302,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
|
||||
checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
@@ -1349,12 +1327,6 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "precomputed-hash"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
@@ -1630,6 +1602,12 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -1769,12 +1747,6 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
@@ -1823,31 +1795,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
|
||||
dependencies = [
|
||||
"new_debug_unreachable",
|
||||
"parking_lot",
|
||||
"phf_shared",
|
||||
"precomputed-hash",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string_cache_codegen"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -1895,17 +1842,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tendril"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
|
||||
dependencies = [
|
||||
"futf",
|
||||
"mac",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.3"
|
||||
@@ -2009,15 +1945,21 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca"
|
||||
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
@@ -2030,12 +1972,6 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
@@ -2067,12 +2003,6 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@@ -2085,6 +2015,17 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
|
||||
dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
@@ -2125,6 +2066,51 @@ dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
|
||||
+9
-3
@@ -16,10 +16,11 @@ all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = ["imap", "smtp", "rustls-ring"]
|
||||
default = ["imap", "smtp", "maildir", "rustls-ring"]
|
||||
|
||||
imap = ["dep:io-imap", "pimalaya-toolbox/imap"]
|
||||
smtp = ["dep:io-smtp", "pimalaya-toolbox/smtp"]
|
||||
maildir = ["dep:convert_case", "dep:io-fs", "dep:io-maildir", "dep:mime_guess"]
|
||||
|
||||
native-tls = ["pimalaya-toolbox/native-tls"]
|
||||
rustls-aws = ["pimalaya-toolbox/rustls-aws"]
|
||||
@@ -35,15 +36,18 @@ anyhow = "1"
|
||||
chrono = { version = "0.4", default-features = false }
|
||||
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
|
||||
comfy-table = "7"
|
||||
convert_case = { version = "0.11", optional = true }
|
||||
dirs = "6"
|
||||
gethostname = "1"
|
||||
html2text = "0.12"
|
||||
io-fs = { version = "0.0.1", default-features = false, features = ["std"], optional = true }
|
||||
io-imap = { version = "0.0.1", default-features = false, optional = true }
|
||||
io-maildir = { version = "0.0.1", default-features = false, features = ["serde"], optional = true }
|
||||
io-process = { version = "0.0.2", default-features = false }
|
||||
io-smtp = { version = "0.0.1", default-features = false, features = ["ext_auth", "starttls"], optional = true }
|
||||
io-stream = { version = "0.0.2", default-features = false, features = ["std"] }
|
||||
log = "0.4"
|
||||
mail-parser = "0.9"
|
||||
mail-parser = "0.11"
|
||||
mime_guess = { version = "2", optional = true }
|
||||
open = "5"
|
||||
pimalaya-toolbox = { version = "0.0.4", default-features = false, features = ["config", "terminal", "secret"] }
|
||||
rfc2047-decoder = "1"
|
||||
@@ -56,7 +60,9 @@ url = { version = "2.2", features = ["serde"] }
|
||||
uds_windows = "1"
|
||||
|
||||
[patch.crates-io]
|
||||
io-fs.git = "https://github.com/pimalaya/io-fs"
|
||||
io-imap = { git = "https://github.com/pimalaya/io-imap", branch = "io" }
|
||||
io-maildir.git = "https://github.com/pimalaya/io-maildir"
|
||||
io-smtp.git = "https://github.com/pimalaya/io-smtp"
|
||||
pimalaya-toolbox.git = "https://github.com/pimalaya/toolbox"
|
||||
smtp-codec.git = "https://github.com/pimalaya/smtp-codec"
|
||||
|
||||
+18
@@ -17,6 +17,8 @@ use pimalaya_toolbox::{
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
use crate::imap::command::ImapCommand;
|
||||
#[cfg(feature = "maildir")]
|
||||
use crate::maildir::command::MaildirCommand;
|
||||
#[cfg(feature = "smtp")]
|
||||
use crate::smtp::command::SmtpCommand;
|
||||
use crate::{account::Account, config::Config};
|
||||
@@ -59,6 +61,9 @@ pub enum BackendCommand {
|
||||
#[cfg(feature = "imap")]
|
||||
#[command(subcommand)]
|
||||
Imap(ImapCommand),
|
||||
#[cfg(feature = "maildir")]
|
||||
#[command(subcommand)]
|
||||
Maildir(MaildirCommand),
|
||||
#[cfg(feature = "smtp")]
|
||||
#[command(subcommand)]
|
||||
Smtp(SmtpCommand),
|
||||
@@ -88,6 +93,19 @@ impl BackendCommand {
|
||||
|
||||
cmd.execute(printer, account)
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Self::Maildir(cmd) => {
|
||||
let config = Config::from_paths_or_default(config_paths)?;
|
||||
let (account_name, mut account_config) = config.get_account(account_name)?;
|
||||
|
||||
let Some(maildir_config) = account_config.maildir.take() else {
|
||||
bail!("Maildir config is missing for account `{account_name}`")
|
||||
};
|
||||
|
||||
let account = Account::new(config, account_config, maildir_config)?;
|
||||
|
||||
cmd.execute(printer, account)
|
||||
}
|
||||
#[cfg(feature = "smtp")]
|
||||
Self::Smtp(cmd) => {
|
||||
let config = Config::from_paths_or_default(config_paths)?;
|
||||
|
||||
@@ -55,6 +55,7 @@ pub struct AccountConfig {
|
||||
pub table_arrangement: Option<TableArrangementConfig>,
|
||||
|
||||
pub imap: Option<ImapConfig>,
|
||||
pub maildir: Option<MaildirConfig>,
|
||||
pub smtp: Option<SmtpConfig>,
|
||||
}
|
||||
|
||||
@@ -90,6 +91,13 @@ pub struct ImapConfig {
|
||||
pub sasl: SaslConfig,
|
||||
}
|
||||
|
||||
/// Maildir configuration.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct MaildirConfig {
|
||||
pub root: PathBuf,
|
||||
}
|
||||
|
||||
/// SMTP configuration.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ use crate::imap::{
|
||||
/// This command gives you access to the IMAP CLI API, and allows you
|
||||
/// to manage IMAP mailboxes, envelopes, flags, messages etc.
|
||||
#[derive(Debug, Subcommand)]
|
||||
#[command(rename_all = "lowercase")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub enum ImapCommand {
|
||||
Id(IdCommand),
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::imap::{
|
||||
account::ImapAccount,
|
||||
message::{
|
||||
copy::CopyMessageCommand, export::ExportMessageCommand, get::GetMessageCommand,
|
||||
r#move::MoveMessageCommand, read::ReadMessageCommand, save::SaveMessageCommand,
|
||||
r#move::MoveMessagesCommand, read::ReadMessageCommand, save::SaveMessageCommand,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ pub enum MessageCommand {
|
||||
Read(ReadMessageCommand),
|
||||
Export(ExportMessageCommand),
|
||||
Copy(CopyMessageCommand),
|
||||
Move(MoveMessageCommand),
|
||||
Move(MoveMessagesCommand),
|
||||
}
|
||||
|
||||
impl MessageCommand {
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::imap::{
|
||||
/// from the source mailbox to the destination mailbox. Requires the
|
||||
/// MOVE IMAP extension.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MoveMessageCommand {
|
||||
pub struct MoveMessagesCommand {
|
||||
#[command(flatten)]
|
||||
pub mailbox_name: MailboxNameOptionalFlag,
|
||||
#[command(flatten)]
|
||||
@@ -35,7 +35,7 @@ pub struct MoveMessageCommand {
|
||||
pub seq: bool,
|
||||
}
|
||||
|
||||
impl MoveMessageCommand {
|
||||
impl MoveMessagesCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
|
||||
let mut imap = account.new_imap_session()?;
|
||||
let mailbox = self.mailbox_name.inner.try_into()?;
|
||||
|
||||
+42
-30
@@ -1,13 +1,13 @@
|
||||
use std::{fmt, num::NonZeroU32};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_imap::{
|
||||
coroutines::{fetch::*, select::*},
|
||||
types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use mail_parser::MessageParser;
|
||||
use mail_parser::{Message, MessageParser};
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -99,47 +99,59 @@ impl ReadMessageCommand {
|
||||
};
|
||||
|
||||
let Some(message) = MessageParser::new().parse(&raw) else {
|
||||
bail!("Invalid message");
|
||||
bail!("Invalid MIME message");
|
||||
};
|
||||
|
||||
let content = if self.html {
|
||||
message
|
||||
.body_html(0)
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| anyhow!("No HTML content found"))?
|
||||
if self.html {
|
||||
printer.out(MessageHtmlView { message })
|
||||
} else {
|
||||
if let Some(text) = message.body_text(0) {
|
||||
text.to_string()
|
||||
} else if let Some(html) = message.body_html(0) {
|
||||
html2text::from_read(html.as_bytes(), self.width)
|
||||
} else {
|
||||
bail!("No text or HTML content found");
|
||||
}
|
||||
};
|
||||
|
||||
let output = MessageContent { content };
|
||||
printer.out(output)
|
||||
printer.out(MessagePlainView { message })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MessageContent {
|
||||
pub content: String,
|
||||
#[derive(Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct MessagePlainView<'a> {
|
||||
message: Message<'a>,
|
||||
}
|
||||
|
||||
impl fmt::Display for MessageContent {
|
||||
impl fmt::Display for MessagePlainView<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f)?;
|
||||
write!(f, "{}", self.content)?;
|
||||
if !self.content.ends_with('\n') {
|
||||
writeln!(f)?;
|
||||
for (i, part) in self.message.text_bodies().enumerate() {
|
||||
if i > 0 {
|
||||
writeln!(f)?;
|
||||
writeln!(f)?;
|
||||
}
|
||||
|
||||
if let Some(contents) = part.text_contents() {
|
||||
write!(f, "{}", contents.trim_end())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for MessageContent {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
self.content.serialize(serializer)
|
||||
#[derive(Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct MessageHtmlView<'a> {
|
||||
message: Message<'a>,
|
||||
}
|
||||
|
||||
impl fmt::Display for MessageHtmlView<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (i, part) in self.message.html_bodies().enumerate() {
|
||||
if i > 0 {
|
||||
writeln!(f)?;
|
||||
writeln!(f)?;
|
||||
}
|
||||
|
||||
if let Some(contents) = part.text_contents() {
|
||||
write!(f, "{}", contents.trim_end())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
use crate::{account::Account, config::MaildirConfig};
|
||||
|
||||
pub type MaildirAccount = Account<MaildirConfig>;
|
||||
@@ -0,0 +1,66 @@
|
||||
use clap::Parser;
|
||||
|
||||
const INBOX: &str = "INBOX";
|
||||
|
||||
/// The optional maildir name argument parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MaildirNameOptionalArg {
|
||||
/// The name of the maildir.
|
||||
#[arg(name = "maildir_name", value_name = "MAILDIR", default_value = INBOX)]
|
||||
pub inner: String,
|
||||
}
|
||||
|
||||
impl Default for MaildirNameOptionalArg {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: INBOX.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The optional maildir name flag parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MaildirPathOptionalFlag {
|
||||
/// The name of the maildir.
|
||||
#[arg(long = "maildir", short = 'm')]
|
||||
#[arg(name = "maildir_name", value_name = "NAME", default_value = INBOX)]
|
||||
pub inner: String,
|
||||
}
|
||||
|
||||
impl Default for MaildirPathOptionalFlag {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: INBOX.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MaildirNoSelectFlag {
|
||||
/// Do not select the given maildir before performing the current
|
||||
/// action.
|
||||
///
|
||||
/// This argument is useful when stateful IMAP sessions are used,
|
||||
/// for example with Sirup CLI:
|
||||
///
|
||||
/// https://github.com/pimalaya/sirup
|
||||
#[arg(long = "no-select", default_value_t)]
|
||||
#[arg(name = "maildir_no_select")]
|
||||
pub inner: bool,
|
||||
}
|
||||
|
||||
/// The required maildir name argument parser.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MaildirNameArg {
|
||||
/// The name of the maildir.
|
||||
#[arg(name = "maildir_name", value_name = "MAILDIR")]
|
||||
pub inner: String,
|
||||
}
|
||||
|
||||
/// The target maildir name argument parser.
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct TargetMaildirNameArg {
|
||||
/// The name of the target maildir.
|
||||
#[arg(name = "target_maildir_name", value_name = "TARGET")]
|
||||
pub inner: String,
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::maildir::{account::MaildirAccount, message::command::MessageCommand};
|
||||
|
||||
/// MAILDIR CLI (requires the `maildir` cargo feature).
|
||||
///
|
||||
/// This command gives you access to the MAILDIR CLI API, and allows you
|
||||
/// to manage MAILDIR mailboxes, envelopes, flags, messages etc.
|
||||
#[derive(Debug, Subcommand)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub enum MaildirCommand {
|
||||
#[command(subcommand)]
|
||||
#[command(aliases = ["msgs", "msg"])]
|
||||
Messages(MessageCommand),
|
||||
}
|
||||
|
||||
impl MaildirCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
|
||||
match self {
|
||||
Self::Messages(cmd) => cmd.execute(printer, account),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::maildir::{
|
||||
account::MaildirAccount,
|
||||
message::{
|
||||
copy::CopyMessagesCommand, export::ExportMessageCommand, get::GetMessageCommand,
|
||||
r#move::MoveMessagesCommand, read::ReadMessageCommand, save::SaveMessageCommand,
|
||||
},
|
||||
};
|
||||
|
||||
/// Manage MAILDIR messages.
|
||||
///
|
||||
/// A message is a complete email including headers and body. This
|
||||
/// subcommand allows you to save, get, read, export, copy, and move
|
||||
/// messages.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum MessageCommand {
|
||||
Save(SaveMessageCommand),
|
||||
Get(GetMessageCommand),
|
||||
Read(ReadMessageCommand),
|
||||
Export(ExportMessageCommand),
|
||||
Copy(CopyMessagesCommand),
|
||||
Move(MoveMessagesCommand),
|
||||
}
|
||||
|
||||
impl MessageCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
|
||||
match self {
|
||||
Self::Save(cmd) => cmd.execute(printer, account),
|
||||
Self::Get(cmd) => cmd.execute(printer, account),
|
||||
Self::Read(cmd) => cmd.execute(printer, account),
|
||||
Self::Export(cmd) => cmd.execute(printer, account),
|
||||
Self::Copy(cmd) => cmd.execute(printer, account),
|
||||
Self::Move(cmd) => cmd.execute(printer, account),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use io_fs::runtimes::std::handle;
|
||||
use io_maildir::{
|
||||
coroutines::copy_message::*,
|
||||
maildir::{Maildir, MaildirSubdir},
|
||||
};
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::maildir::account::MaildirAccount;
|
||||
|
||||
/// Copy Maildir message to the given mailbox.
|
||||
///
|
||||
/// This command copies message(s) identified by the given sequence
|
||||
/// set from the source mailbox to the destination mailbox.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct CopyMessagesCommand {
|
||||
/// Path to the source Maildir, where messages are copied from.
|
||||
#[arg(long = "source", short = 's')]
|
||||
#[arg(value_name = "PATH", default_value = "Inbox")]
|
||||
pub maildir_source_path: PathBuf,
|
||||
|
||||
/// Path to the target Maildir, where messages are copied into.
|
||||
#[arg(long = "target", short = 't')]
|
||||
#[arg(value_name = "PATH")]
|
||||
pub maildir_target_path: PathBuf,
|
||||
/// Subdir of the target Maildir.
|
||||
#[arg(long = "subdir", value_name = "NAME", value_enum)]
|
||||
pub maildir_target_subdir: MaildirSubdirArg,
|
||||
|
||||
/// Id(s) of message(s) to copy.
|
||||
#[arg(value_name = "ID", num_args = 1..)]
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl CopyMessagesCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
|
||||
let maildir_source = match Maildir::try_from(self.maildir_source_path.clone()) {
|
||||
Ok(maildir) => maildir,
|
||||
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir_source_path))?,
|
||||
};
|
||||
|
||||
let maildir_target = match Maildir::try_from(self.maildir_target_path.clone()) {
|
||||
Ok(maildir) => maildir,
|
||||
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir_target_path))?,
|
||||
};
|
||||
|
||||
for id in self.ids {
|
||||
let mut arg = None;
|
||||
let mut coroutine = CopyMaildirMessage::new(
|
||||
maildir_source.clone(),
|
||||
maildir_target.clone(),
|
||||
self.maildir_target_subdir.clone().into(),
|
||||
id,
|
||||
);
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
CopyMaildirMessageResult::Io(io) => arg = Some(handle(io)?),
|
||||
CopyMaildirMessageResult::Ok => break,
|
||||
CopyMaildirMessageResult::Err(err) => bail!(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printer.out(Message::new("Message(s) successfully copied"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, ValueEnum)]
|
||||
pub enum MaildirSubdirArg {
|
||||
Cur,
|
||||
New,
|
||||
Tmp,
|
||||
}
|
||||
|
||||
impl From<MaildirSubdirArg> for MaildirSubdir {
|
||||
fn from(value: MaildirSubdirArg) -> Self {
|
||||
match value {
|
||||
MaildirSubdirArg::Cur => MaildirSubdir::Cur,
|
||||
MaildirSubdirArg::New => MaildirSubdir::New,
|
||||
MaildirSubdirArg::Tmp => MaildirSubdir::Tmp,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
use std::{fmt, fs, path::PathBuf};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use convert_case::ccase;
|
||||
use io_fs::runtimes::std::handle;
|
||||
use io_maildir::{coroutines::get_message::*, maildir::Maildir, types::MimeHeaders};
|
||||
use mime_guess::{get_mime_extensions_str, mime::OCTET_STREAM};
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::maildir::account::MaildirAccount;
|
||||
|
||||
/// Export type for message export.
|
||||
#[derive(Clone, Debug, Default, ValueEnum)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
pub enum ExportType {
|
||||
#[default]
|
||||
/// Output raw RFC822 message to stdout.
|
||||
Raw,
|
||||
/// Export all MIME parts to separate files.
|
||||
Parts,
|
||||
}
|
||||
|
||||
/// Export a message.
|
||||
///
|
||||
/// This command exports a message in various formats:
|
||||
/// - raw: Output raw RFC822 message to stdout
|
||||
/// - eml: Save as .eml file
|
||||
/// - parts: Export all MIME parts to separate files
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ExportMessageCommand {
|
||||
/// Path to the Maildir containing the message looked for.
|
||||
#[arg(long = "maildir", short)]
|
||||
#[arg(value_name = "PATH", default_value = "Inbox")]
|
||||
pub maildir_path: PathBuf,
|
||||
|
||||
/// Id of message to export.
|
||||
#[arg()]
|
||||
pub id: String,
|
||||
|
||||
/// Type of the export.
|
||||
#[arg(long, short, value_enum, default_value_t)]
|
||||
pub r#type: ExportType,
|
||||
|
||||
/// Output directory (for eml and parts types).
|
||||
#[arg(long, short, value_name = "DIR")]
|
||||
pub directory: Option<PathBuf>,
|
||||
|
||||
/// Open exported content in default application, when applicable.
|
||||
#[arg(long, short)]
|
||||
pub open: bool,
|
||||
}
|
||||
|
||||
impl ExportMessageCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
|
||||
let maildir = match Maildir::try_from(self.maildir_path.clone()) {
|
||||
Ok(maildir) => maildir,
|
||||
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir_path))?,
|
||||
};
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine = GetMaildirMessage::new(maildir, &self.id);
|
||||
|
||||
let msg = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetMaildirMessageResult::Io(io) => arg = Some(handle(io)?),
|
||||
GetMaildirMessageResult::Ok(msg) => break msg,
|
||||
GetMaildirMessageResult::Err(err) => bail!(err),
|
||||
};
|
||||
};
|
||||
|
||||
match self.r#type {
|
||||
ExportType::Raw => {
|
||||
let contents = String::from_utf8(msg.into())?;
|
||||
printer.out(ExportRaw { contents })?;
|
||||
}
|
||||
ExportType::Parts => {
|
||||
let Some(msg) = msg.parsed() else {
|
||||
bail!("Invalid MIME message at {}", msg.path().display());
|
||||
};
|
||||
|
||||
let dir = self.directory.unwrap_or_else(|| PathBuf::from(self.id));
|
||||
fs::create_dir_all(&dir)?;
|
||||
|
||||
let mut parts = Vec::new();
|
||||
|
||||
for (i, part) in msg.parts.iter().enumerate() {
|
||||
let cr = part.content_type().map(|ct| match &ct.c_subtype {
|
||||
Some(sub) => format!("{}/{}", ct.c_type, sub),
|
||||
None => ct.c_type.to_string(),
|
||||
});
|
||||
|
||||
if let Some(ref ct) = cr {
|
||||
if ct.starts_with("multipart/") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let filename = match part.attachment_name() {
|
||||
Some(name) => ccase!(kebab, name),
|
||||
None => {
|
||||
let ext = match cr.as_deref().unwrap_or(OCTET_STREAM.as_str()) {
|
||||
"text/plain" => Some(&"txt"),
|
||||
"text/html" => Some(&"html"),
|
||||
ct => get_mime_extensions_str(ct).and_then(|ext| ext.first()),
|
||||
};
|
||||
|
||||
match ext {
|
||||
Some(ext) => format!("part_{i}.{ext}"),
|
||||
None => format!("part_{i}"),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let path = dir.join(&filename);
|
||||
let contents = part.contents();
|
||||
fs::write(&path, contents)?;
|
||||
parts.push(path);
|
||||
}
|
||||
|
||||
if self.open {
|
||||
for path in &parts {
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "html" {
|
||||
open::that(path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printer.out(ExportParts { parts })?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExportRaw {
|
||||
contents: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for ExportRaw {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.contents)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExportParts {
|
||||
parts: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl fmt::Display for ExportParts {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for path in &self.parts {
|
||||
writeln!(f, " - {}", path.display())?;
|
||||
}
|
||||
|
||||
writeln!(f)?;
|
||||
write!(f, "Exported {} part(s)", self.parts.len())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use std::{fmt, path::PathBuf};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_fs::runtimes::std::handle;
|
||||
use io_maildir::{
|
||||
coroutines::get_message::*,
|
||||
maildir::Maildir,
|
||||
types::{Message, PartType},
|
||||
};
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::maildir::account::MaildirAccount;
|
||||
|
||||
/// Get Maildir message to the given mailbox.
|
||||
///
|
||||
/// This command copies message(s) identified by the given sequence
|
||||
/// set from the source mailbox to the destination mailbox.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct GetMessageCommand {
|
||||
/// Path to the Maildir containing the message looked for.
|
||||
#[arg(long, short, value_name = "PATH")]
|
||||
#[arg(default_value = "Inbox")]
|
||||
pub maildir: PathBuf,
|
||||
|
||||
/// Id of message to get.
|
||||
#[arg(value_name = "ID")]
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
impl GetMessageCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
|
||||
let maildir = match Maildir::try_from(self.maildir.clone()) {
|
||||
Ok(maildir) => maildir,
|
||||
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir))?,
|
||||
};
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine = GetMaildirMessage::new(maildir, &self.id);
|
||||
|
||||
let msg = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetMaildirMessageResult::Io(io) => arg = Some(handle(io)?),
|
||||
GetMaildirMessageResult::Ok(msg) => break msg,
|
||||
GetMaildirMessageResult::Err(err) => bail!(err),
|
||||
};
|
||||
};
|
||||
|
||||
let Some(msg) = msg.parsed() else {
|
||||
bail!("Invalid MIME message at {}", msg.path().display());
|
||||
};
|
||||
|
||||
printer.out(MessageView(msg))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct MessageView<'a>(Message<'a>);
|
||||
|
||||
impl fmt::Display for MessageView<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let parts_len = self.0.parts.len();
|
||||
for (i, p) in self.0.parts.iter().enumerate() {
|
||||
writeln!(f, "---")?;
|
||||
writeln!(f, "Part {}/{parts_len}:", i + 1)?;
|
||||
writeln!(f)?;
|
||||
|
||||
for h in p.headers() {
|
||||
writeln!(f, "{}: {:?}", h.name.as_str(), h.value)?;
|
||||
}
|
||||
|
||||
writeln!(f)?;
|
||||
|
||||
match &p.body {
|
||||
PartType::Text(p) => writeln!(f, "{p}")?,
|
||||
PartType::Html(p) => writeln!(f, "{p}")?,
|
||||
PartType::Binary(p) => writeln!(f, "({} bytes)", p.len())?,
|
||||
PartType::InlineBinary(p) => writeln!(f, "({} inline bytes)", p.len())?,
|
||||
PartType::Multipart(_) => continue,
|
||||
PartType::Message(m) => write!(f, "{}", MessageView(m.clone()))?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod command;
|
||||
pub mod copy;
|
||||
pub mod export;
|
||||
pub mod get;
|
||||
pub mod r#move;
|
||||
pub mod read;
|
||||
pub mod save;
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use io_fs::runtimes::std::handle;
|
||||
use io_maildir::{
|
||||
coroutines::move_message::*,
|
||||
maildir::{Maildir, MaildirSubdir},
|
||||
};
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::maildir::account::MaildirAccount;
|
||||
|
||||
/// Move Maildir message to the given mailbox.
|
||||
///
|
||||
/// This command copies message(s) identified by the given sequence
|
||||
/// set from the source mailbox to the destination mailbox.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct MoveMessagesCommand {
|
||||
/// Path to the source Maildir, where messages are copied from.
|
||||
#[arg(long = "source", short = 's')]
|
||||
#[arg(value_name = "PATH", default_value = "Inbox")]
|
||||
pub maildir_source_path: PathBuf,
|
||||
|
||||
/// Path to the target Maildir, where messages are copied into.
|
||||
#[arg(long = "target", short = 't')]
|
||||
#[arg(value_name = "PATH")]
|
||||
pub maildir_target_path: PathBuf,
|
||||
/// Subdir of the target Maildir.
|
||||
#[arg(long = "subdir", value_name = "NAME", value_enum)]
|
||||
pub maildir_target_subdir: MaildirSubdirArg,
|
||||
|
||||
/// Id(s) of message(s) to move.
|
||||
#[arg(value_name = "ID", num_args = 1..)]
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl MoveMessagesCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
|
||||
let maildir_source = match Maildir::try_from(self.maildir_source_path.clone()) {
|
||||
Ok(maildir) => maildir,
|
||||
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir_source_path))?,
|
||||
};
|
||||
|
||||
let maildir_target = match Maildir::try_from(self.maildir_target_path.clone()) {
|
||||
Ok(maildir) => maildir,
|
||||
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir_target_path))?,
|
||||
};
|
||||
|
||||
for id in self.ids {
|
||||
let mut arg = None;
|
||||
let mut coroutine = MoveMaildirMessage::new(
|
||||
maildir_source.clone(),
|
||||
maildir_target.clone(),
|
||||
self.maildir_target_subdir.clone().into(),
|
||||
id,
|
||||
);
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
MoveMaildirMessageResult::Io(io) => arg = Some(handle(io)?),
|
||||
MoveMaildirMessageResult::Ok => break,
|
||||
MoveMaildirMessageResult::Err(err) => bail!(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printer.out(Message::new("Message(s) successfully copied"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, ValueEnum)]
|
||||
pub enum MaildirSubdirArg {
|
||||
Cur,
|
||||
New,
|
||||
Tmp,
|
||||
}
|
||||
|
||||
impl From<MaildirSubdirArg> for MaildirSubdir {
|
||||
fn from(value: MaildirSubdirArg) -> Self {
|
||||
match value {
|
||||
MaildirSubdirArg::Cur => MaildirSubdir::Cur,
|
||||
MaildirSubdirArg::New => MaildirSubdir::New,
|
||||
MaildirSubdirArg::Tmp => MaildirSubdir::Tmp,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
use std::{fmt, path::PathBuf};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_fs::runtimes::std::handle;
|
||||
use io_maildir::{coroutines::get_message::*, maildir::Maildir, types::Message};
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::maildir::account::MaildirAccount;
|
||||
|
||||
/// Read message content.
|
||||
///
|
||||
/// This command fetches a message and displays its text content.
|
||||
/// By default it shows plain text content; use --html to show HTML.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ReadMessageCommand {
|
||||
/// Path to the Maildir containing the message looked for.
|
||||
#[arg(long, short, value_name = "PATH")]
|
||||
#[arg(default_value = "Inbox")]
|
||||
pub maildir: PathBuf,
|
||||
|
||||
/// Id of message to read.
|
||||
#[arg()]
|
||||
pub id: String,
|
||||
|
||||
/// Show HTML content instead of plain text.
|
||||
#[arg(long)]
|
||||
pub html: bool,
|
||||
|
||||
/// Terminal width for text wrapping.
|
||||
#[arg(long, short, default_value = "80")]
|
||||
pub width: usize,
|
||||
}
|
||||
|
||||
impl ReadMessageCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
|
||||
let maildir = match Maildir::try_from(self.maildir.clone()) {
|
||||
Ok(maildir) => maildir,
|
||||
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir))?,
|
||||
};
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine = GetMaildirMessage::new(maildir, &self.id);
|
||||
|
||||
let message = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetMaildirMessageResult::Io(io) => arg = Some(handle(io)?),
|
||||
GetMaildirMessageResult::Ok(msg) => break msg,
|
||||
GetMaildirMessageResult::Err(err) => bail!(err),
|
||||
};
|
||||
};
|
||||
|
||||
let Some(message) = message.parsed() else {
|
||||
bail!("Invalid MIME message at {}", message.path().display());
|
||||
};
|
||||
|
||||
if self.html {
|
||||
printer.out(MessageHtmlView { message })
|
||||
} else {
|
||||
printer.out(MessagePlainView { message })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct MessagePlainView<'a> {
|
||||
message: Message<'a>,
|
||||
}
|
||||
|
||||
impl fmt::Display for MessagePlainView<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (i, part) in self.message.text_bodies().enumerate() {
|
||||
if i > 0 {
|
||||
writeln!(f)?;
|
||||
writeln!(f)?;
|
||||
}
|
||||
|
||||
if let Some(contents) = part.text_contents() {
|
||||
write!(f, "{}", contents.trim_end())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct MessageHtmlView<'a> {
|
||||
message: Message<'a>,
|
||||
}
|
||||
|
||||
impl fmt::Display for MessageHtmlView<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (i, part) in self.message.html_bodies().enumerate() {
|
||||
if i > 0 {
|
||||
writeln!(f)?;
|
||||
writeln!(f)?;
|
||||
}
|
||||
|
||||
if let Some(contents) = part.text_contents() {
|
||||
write!(f, "{}", contents.trim_end())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
use std::{
|
||||
fmt,
|
||||
io::{stdin, BufRead, IsTerminal},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use io_fs::runtimes::std::handle;
|
||||
use io_maildir::{
|
||||
coroutines::store_message::*,
|
||||
flag::Flag,
|
||||
maildir::{Maildir, MaildirSubdir},
|
||||
};
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::maildir::account::MaildirAccount;
|
||||
|
||||
/// Save a message to a mailbox.
|
||||
///
|
||||
/// This command appends a message to the specified mailbox. The
|
||||
/// message is read from stdin in RFC 5322 format (raw email).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SaveMessageCommand {
|
||||
/// Path to the Maildir to save message into.
|
||||
#[arg(long, short, value_name = "PATH")]
|
||||
#[arg(default_value = "Inbox")]
|
||||
pub maildir: PathBuf,
|
||||
|
||||
/// The subdirectory of the Maildir
|
||||
#[arg(long, short, value_name = "NAME", value_enum)]
|
||||
#[arg(default_value = "new")]
|
||||
pub subdir: MaildirSubdirArg,
|
||||
|
||||
/// The flags to add to the message.
|
||||
#[arg(long = "flag", short, num_args = 0..)]
|
||||
pub flags: Vec<FlagArg>,
|
||||
|
||||
/// The raw message, including headers and body.
|
||||
#[arg(trailing_var_arg = true)]
|
||||
#[arg(name = "message", value_name = "MESSAGE")]
|
||||
pub message: Vec<String>,
|
||||
}
|
||||
|
||||
impl SaveMessageCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
|
||||
let maildir = match Maildir::try_from(self.maildir.clone()) {
|
||||
Ok(maildir) => maildir,
|
||||
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir))?,
|
||||
};
|
||||
|
||||
let msg = if stdin().is_terminal() || printer.is_json() {
|
||||
self.message
|
||||
.join(" ")
|
||||
.replace('\r', "")
|
||||
.replace('\n', "\r\n")
|
||||
} else {
|
||||
stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.map_while(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
|
||||
let flags = self.flags.into_iter().map(Into::into).into();
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine =
|
||||
StoreMaildirMessage::new(maildir, self.subdir.into(), flags, msg.into_bytes());
|
||||
|
||||
let out = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
StoreMaildirMessageResult::Io(io) => arg = Some(handle(io)?),
|
||||
StoreMaildirMessageResult::Ok { id, path } => break StoredMessage { id, path },
|
||||
StoreMaildirMessageResult::Err(err) => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
printer.out(out)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, ValueEnum)]
|
||||
pub enum MaildirSubdirArg {
|
||||
Cur,
|
||||
New,
|
||||
Tmp,
|
||||
}
|
||||
|
||||
impl From<MaildirSubdirArg> for MaildirSubdir {
|
||||
fn from(value: MaildirSubdirArg) -> Self {
|
||||
match value {
|
||||
MaildirSubdirArg::Cur => MaildirSubdir::Cur,
|
||||
MaildirSubdirArg::New => MaildirSubdir::New,
|
||||
MaildirSubdirArg::Tmp => MaildirSubdir::Tmp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, ValueEnum)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
pub enum FlagArg {
|
||||
Passed,
|
||||
Replied,
|
||||
Seen,
|
||||
Trashed,
|
||||
Draft,
|
||||
Flagged,
|
||||
}
|
||||
|
||||
impl From<FlagArg> for Flag {
|
||||
fn from(flag: FlagArg) -> Self {
|
||||
match flag {
|
||||
FlagArg::Passed => Flag::Passed,
|
||||
FlagArg::Replied => Flag::Replied,
|
||||
FlagArg::Seen => Flag::Seen,
|
||||
FlagArg::Trashed => Flag::Trashed,
|
||||
FlagArg::Draft => Flag::Draft,
|
||||
FlagArg::Flagged => Flag::Flagged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct StoredMessage {
|
||||
id: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl fmt::Display for StoredMessage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let id = &self.id;
|
||||
let path = self.path.display();
|
||||
write!(f, "Message `{id}` successfully saved to {path}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod account;
|
||||
pub mod arg;
|
||||
pub mod command;
|
||||
pub mod message;
|
||||
@@ -3,6 +3,8 @@ mod cli;
|
||||
mod config;
|
||||
#[cfg(feature = "imap")]
|
||||
mod imap;
|
||||
#[cfg(feature = "maildir")]
|
||||
mod maildir;
|
||||
#[cfg(feature = "smtp")]
|
||||
mod smtp;
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ use crate::smtp::{account::SmtpAccount, message::command::MessageCommand};
|
||||
/// you to manage SMTP mailboxes: list mailboxes, read messages,
|
||||
/// add flags etc.
|
||||
#[derive(Debug, Subcommand)]
|
||||
#[command(rename_all = "lowercase")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub enum SmtpCommand {
|
||||
#[command(subcommand)]
|
||||
#[command(aliases = ["msgs", "msg"])]
|
||||
|
||||
Reference in New Issue
Block a user