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",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -328,7 +334,16 @@ checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"unicode-segmentation",
|
"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]]
|
[[package]]
|
||||||
@@ -513,16 +528,6 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
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]]
|
[[package]]
|
||||||
name = "gethostname"
|
name = "gethostname"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -599,6 +604,17 @@ version = "0.16.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
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]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -613,15 +629,18 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"comfy-table",
|
"comfy-table",
|
||||||
|
"convert_case",
|
||||||
"dirs",
|
"dirs",
|
||||||
"gethostname",
|
"gethostname",
|
||||||
"html2text",
|
"io-fs",
|
||||||
"io-imap",
|
"io-imap",
|
||||||
|
"io-maildir",
|
||||||
"io-process",
|
"io-process",
|
||||||
"io-smtp",
|
"io-smtp",
|
||||||
"io-stream",
|
"io-stream",
|
||||||
"log",
|
"log",
|
||||||
"mail-parser",
|
"mail-parser",
|
||||||
|
"mime_guess",
|
||||||
"open",
|
"open",
|
||||||
"pimalaya-toolbox",
|
"pimalaya-toolbox",
|
||||||
"rfc2047-decoder",
|
"rfc2047-decoder",
|
||||||
@@ -632,33 +651,6 @@ dependencies = [
|
|||||||
"url",
|
"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]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@@ -807,6 +799,15 @@ dependencies = [
|
|||||||
"serde_core",
|
"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]]
|
[[package]]
|
||||||
name = "io-imap"
|
name = "io-imap"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
@@ -820,6 +821,20 @@ dependencies = [
|
|||||||
"thiserror 2.0.18",
|
"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]]
|
[[package]]
|
||||||
name = "io-process"
|
name = "io-process"
|
||||||
version = "0.0.2"
|
version = "0.0.2"
|
||||||
@@ -943,6 +958,16 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "leb128fmt"
|
name = "leb128fmt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1021,33 +1046,14 @@ version = "0.4.29"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mac"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mail-parser"
|
name = "mail-parser"
|
||||||
version = "0.9.4"
|
version = "0.11.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93c3b9e5d8b17faf573330bbc43b37d6e918c0a3bf8a88e7d0a220ebc84af9fc"
|
checksum = "f82a3d6522697593ba4c683e0a6ee5a40fee93bc1a525e3cc6eeb3da11fd8897"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"encoding_rs",
|
"hashify",
|
||||||
]
|
"serde",
|
||||||
|
|
||||||
[[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",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1065,6 +1071,22 @@ dependencies = [
|
|||||||
"autocfg",
|
"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]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -1088,12 +1110,6 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "new_debug_unreachable"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
@@ -1240,44 +1256,6 @@ version = "2.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
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]]
|
[[package]]
|
||||||
name = "pimalaya-toolbox"
|
name = "pimalaya-toolbox"
|
||||||
version = "0.0.4"
|
version = "0.0.4"
|
||||||
@@ -1324,9 +1302,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic-util"
|
name = "portable-atomic-util"
|
||||||
version = "0.2.5"
|
version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
|
checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
]
|
]
|
||||||
@@ -1349,12 +1327,6 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "precomputed-hash"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.37"
|
version = "0.2.37"
|
||||||
@@ -1630,6 +1602,12 @@ dependencies = [
|
|||||||
"untrusted",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -1769,12 +1747,6 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "siphasher"
|
|
||||||
version = "1.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
@@ -1823,31 +1795,6 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -1895,17 +1842,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "terminal_size"
|
name = "terminal_size"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -2009,15 +1945,21 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uds_windows"
|
name = "uds_windows"
|
||||||
version = "1.2.0"
|
version = "1.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca"
|
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memoffset",
|
"memoffset",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
@@ -2030,12 +1972,6 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-width"
|
|
||||||
version = "0.1.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -2067,12 +2003,6 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "utf-8"
|
|
||||||
version = "0.7.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -2085,6 +2015,17 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
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]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
@@ -2125,6 +2066,51 @@ dependencies = [
|
|||||||
"wit-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "wasm-encoder"
|
name = "wasm-encoder"
|
||||||
version = "0.244.0"
|
version = "0.244.0"
|
||||||
|
|||||||
+9
-3
@@ -16,10 +16,11 @@ all-features = true
|
|||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["imap", "smtp", "rustls-ring"]
|
default = ["imap", "smtp", "maildir", "rustls-ring"]
|
||||||
|
|
||||||
imap = ["dep:io-imap", "pimalaya-toolbox/imap"]
|
imap = ["dep:io-imap", "pimalaya-toolbox/imap"]
|
||||||
smtp = ["dep:io-smtp", "pimalaya-toolbox/smtp"]
|
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"]
|
native-tls = ["pimalaya-toolbox/native-tls"]
|
||||||
rustls-aws = ["pimalaya-toolbox/rustls-aws"]
|
rustls-aws = ["pimalaya-toolbox/rustls-aws"]
|
||||||
@@ -35,15 +36,18 @@ anyhow = "1"
|
|||||||
chrono = { version = "0.4", default-features = false }
|
chrono = { version = "0.4", default-features = false }
|
||||||
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
|
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
|
||||||
comfy-table = "7"
|
comfy-table = "7"
|
||||||
|
convert_case = { version = "0.11", optional = true }
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
gethostname = "1"
|
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-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-process = { version = "0.0.2", default-features = false }
|
||||||
io-smtp = { version = "0.0.1", default-features = false, features = ["ext_auth", "starttls"], optional = true }
|
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"] }
|
io-stream = { version = "0.0.2", default-features = false, features = ["std"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mail-parser = "0.9"
|
mail-parser = "0.11"
|
||||||
|
mime_guess = { version = "2", optional = true }
|
||||||
open = "5"
|
open = "5"
|
||||||
pimalaya-toolbox = { version = "0.0.4", default-features = false, features = ["config", "terminal", "secret"] }
|
pimalaya-toolbox = { version = "0.0.4", default-features = false, features = ["config", "terminal", "secret"] }
|
||||||
rfc2047-decoder = "1"
|
rfc2047-decoder = "1"
|
||||||
@@ -56,7 +60,9 @@ url = { version = "2.2", features = ["serde"] }
|
|||||||
uds_windows = "1"
|
uds_windows = "1"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
io-fs.git = "https://github.com/pimalaya/io-fs"
|
||||||
io-imap = { git = "https://github.com/pimalaya/io-imap", branch = "io" }
|
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"
|
io-smtp.git = "https://github.com/pimalaya/io-smtp"
|
||||||
pimalaya-toolbox.git = "https://github.com/pimalaya/toolbox"
|
pimalaya-toolbox.git = "https://github.com/pimalaya/toolbox"
|
||||||
smtp-codec.git = "https://github.com/pimalaya/smtp-codec"
|
smtp-codec.git = "https://github.com/pimalaya/smtp-codec"
|
||||||
|
|||||||
+18
@@ -17,6 +17,8 @@ use pimalaya_toolbox::{
|
|||||||
|
|
||||||
#[cfg(feature = "imap")]
|
#[cfg(feature = "imap")]
|
||||||
use crate::imap::command::ImapCommand;
|
use crate::imap::command::ImapCommand;
|
||||||
|
#[cfg(feature = "maildir")]
|
||||||
|
use crate::maildir::command::MaildirCommand;
|
||||||
#[cfg(feature = "smtp")]
|
#[cfg(feature = "smtp")]
|
||||||
use crate::smtp::command::SmtpCommand;
|
use crate::smtp::command::SmtpCommand;
|
||||||
use crate::{account::Account, config::Config};
|
use crate::{account::Account, config::Config};
|
||||||
@@ -59,6 +61,9 @@ pub enum BackendCommand {
|
|||||||
#[cfg(feature = "imap")]
|
#[cfg(feature = "imap")]
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
Imap(ImapCommand),
|
Imap(ImapCommand),
|
||||||
|
#[cfg(feature = "maildir")]
|
||||||
|
#[command(subcommand)]
|
||||||
|
Maildir(MaildirCommand),
|
||||||
#[cfg(feature = "smtp")]
|
#[cfg(feature = "smtp")]
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
Smtp(SmtpCommand),
|
Smtp(SmtpCommand),
|
||||||
@@ -88,6 +93,19 @@ impl BackendCommand {
|
|||||||
|
|
||||||
cmd.execute(printer, account)
|
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")]
|
#[cfg(feature = "smtp")]
|
||||||
Self::Smtp(cmd) => {
|
Self::Smtp(cmd) => {
|
||||||
let config = Config::from_paths_or_default(config_paths)?;
|
let config = Config::from_paths_or_default(config_paths)?;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ pub struct AccountConfig {
|
|||||||
pub table_arrangement: Option<TableArrangementConfig>,
|
pub table_arrangement: Option<TableArrangementConfig>,
|
||||||
|
|
||||||
pub imap: Option<ImapConfig>,
|
pub imap: Option<ImapConfig>,
|
||||||
|
pub maildir: Option<MaildirConfig>,
|
||||||
pub smtp: Option<SmtpConfig>,
|
pub smtp: Option<SmtpConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +91,13 @@ pub struct ImapConfig {
|
|||||||
pub sasl: SaslConfig,
|
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.
|
/// SMTP configuration.
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
#[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
|
/// This command gives you access to the IMAP CLI API, and allows you
|
||||||
/// to manage IMAP mailboxes, envelopes, flags, messages etc.
|
/// to manage IMAP mailboxes, envelopes, flags, messages etc.
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
#[command(rename_all = "lowercase")]
|
#[command(rename_all = "kebab-case")]
|
||||||
pub enum ImapCommand {
|
pub enum ImapCommand {
|
||||||
Id(IdCommand),
|
Id(IdCommand),
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::imap::{
|
|||||||
account::ImapAccount,
|
account::ImapAccount,
|
||||||
message::{
|
message::{
|
||||||
copy::CopyMessageCommand, export::ExportMessageCommand, get::GetMessageCommand,
|
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),
|
Read(ReadMessageCommand),
|
||||||
Export(ExportMessageCommand),
|
Export(ExportMessageCommand),
|
||||||
Copy(CopyMessageCommand),
|
Copy(CopyMessageCommand),
|
||||||
Move(MoveMessageCommand),
|
Move(MoveMessagesCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageCommand {
|
impl MessageCommand {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use crate::imap::{
|
|||||||
/// from the source mailbox to the destination mailbox. Requires the
|
/// from the source mailbox to the destination mailbox. Requires the
|
||||||
/// MOVE IMAP extension.
|
/// MOVE IMAP extension.
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
pub struct MoveMessageCommand {
|
pub struct MoveMessagesCommand {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub mailbox_name: MailboxNameOptionalFlag,
|
pub mailbox_name: MailboxNameOptionalFlag,
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
@@ -35,7 +35,7 @@ pub struct MoveMessageCommand {
|
|||||||
pub seq: bool,
|
pub seq: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MoveMessageCommand {
|
impl MoveMessagesCommand {
|
||||||
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
|
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
|
||||||
let mut imap = account.new_imap_session()?;
|
let mut imap = account.new_imap_session()?;
|
||||||
let mailbox = self.mailbox_name.inner.try_into()?;
|
let mailbox = self.mailbox_name.inner.try_into()?;
|
||||||
|
|||||||
+42
-30
@@ -1,13 +1,13 @@
|
|||||||
use std::{fmt, num::NonZeroU32};
|
use std::{fmt, num::NonZeroU32};
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use io_imap::{
|
use io_imap::{
|
||||||
coroutines::{fetch::*, select::*},
|
coroutines::{fetch::*, select::*},
|
||||||
types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
|
types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
|
||||||
};
|
};
|
||||||
use io_stream::runtimes::std::handle;
|
use io_stream::runtimes::std::handle;
|
||||||
use mail_parser::MessageParser;
|
use mail_parser::{Message, MessageParser};
|
||||||
use pimalaya_toolbox::terminal::printer::Printer;
|
use pimalaya_toolbox::terminal::printer::Printer;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
@@ -99,47 +99,59 @@ impl ReadMessageCommand {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let Some(message) = MessageParser::new().parse(&raw) else {
|
let Some(message) = MessageParser::new().parse(&raw) else {
|
||||||
bail!("Invalid message");
|
bail!("Invalid MIME message");
|
||||||
};
|
};
|
||||||
|
|
||||||
let content = if self.html {
|
if self.html {
|
||||||
message
|
printer.out(MessageHtmlView { message })
|
||||||
.body_html(0)
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.ok_or_else(|| anyhow!("No HTML content found"))?
|
|
||||||
} else {
|
} else {
|
||||||
if let Some(text) = message.body_text(0) {
|
printer.out(MessagePlainView { message })
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Serialize)]
|
||||||
pub struct MessageContent {
|
#[serde(transparent)]
|
||||||
pub content: String,
|
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 {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
writeln!(f)?;
|
for (i, part) in self.message.text_bodies().enumerate() {
|
||||||
write!(f, "{}", self.content)?;
|
if i > 0 {
|
||||||
if !self.content.ends_with('\n') {
|
writeln!(f)?;
|
||||||
writeln!(f)?;
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(contents) = part.text_contents() {
|
||||||
|
write!(f, "{}", contents.trim_end())?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for MessageContent {
|
#[derive(Serialize)]
|
||||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
#[serde(transparent)]
|
||||||
self.content.serialize(serializer)
|
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;
|
mod config;
|
||||||
#[cfg(feature = "imap")]
|
#[cfg(feature = "imap")]
|
||||||
mod imap;
|
mod imap;
|
||||||
|
#[cfg(feature = "maildir")]
|
||||||
|
mod maildir;
|
||||||
#[cfg(feature = "smtp")]
|
#[cfg(feature = "smtp")]
|
||||||
mod 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,
|
/// you to manage SMTP mailboxes: list mailboxes, read messages,
|
||||||
/// add flags etc.
|
/// add flags etc.
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
#[command(rename_all = "lowercase")]
|
#[command(rename_all = "kebab-case")]
|
||||||
pub enum SmtpCommand {
|
pub enum SmtpCommand {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
#[command(aliases = ["msgs", "msg"])]
|
#[command(aliases = ["msgs", "msg"])]
|
||||||
|
|||||||
Reference in New Issue
Block a user