summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJannis M. Hoffmann <jannis@fehcom.de>2023-08-15 15:13:16 +0200
committerJannis M. Hoffmann <jannis@fehcom.de>2023-08-15 15:13:16 +0200
commit161b052713f5cee08f20ce6ade0881e274c47fdd (patch)
tree4f541f79bb4c4a4089373926ca66783a4af26119
initial commit
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock786
-rw-r--r--Cargo.toml24
-rw-r--r--src/arguments.rs125
-rw-r--r--src/cmd.rs90
-rw-r--r--src/cmd/count.rs27
-rw-r--r--src/cmd/folders.rs48
-rw-r--r--src/cmd/list.rs116
-rw-r--r--src/cmd/raw.rs89
-rw-r--r--src/error.rs69
-rw-r--r--src/main.rs89
-rw-r--r--src/rfc822.rs548
12 files changed, 2012 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..6d4ce94
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,786 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1"
+
+[[package]]
+name = "bumpalo"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "charset"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18e9079d1a12a2cc2bffb5db039c43661836ead4082120d5844f02555aca2d46"
+dependencies = [
+ "base64",
+ "encoding_rs",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-integer",
+ "num-traits",
+ "time 0.1.45",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "clap"
+version = "4.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42dfd32784433290c51d92c438bb72ea5063797fc3cc9a21a8c4346bebbb2098"
+dependencies = [
+ "bitflags 2.0.2",
+ "clap_derive",
+ "clap_lex",
+ "is-terminal",
+ "once_cell",
+ "strsim",
+ "termcolor",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cxx"
+version = "1.0.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c00419335c41018365ddf7e4d5f1c12ee3659ddcf3e01974650ba1de73d038"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb8307ad413a98fff033c8545ecf133e3257747b3bae935e7602aab8aa92d4ca"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "scratch",
+ "syn 2.0.4",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edc52e2eb08915cb12596d29d55f0b5384f00d697a646dbd269b6ecb0fbd9d31"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "631569015d0d8d54e6c241733f944042623ab6df7bc3be7466874b05fcdb1c5f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.4",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "gethostname"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c17cc76786e99f8d2f055c11159e7f0091c42474dcc3189fbab96072e873e6d"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+dependencies = [
+ "cxx",
+ "cxx-build",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e"
+dependencies = [
+ "hermit-abi",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
+
+[[package]]
+name = "js-sys"
+version = "0.3.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "jwebmail-extract"
+version = "0.6.0"
+dependencies = [
+ "chrono",
+ "clap",
+ "lazy_static",
+ "libc",
+ "log",
+ "maildir",
+ "mailparse",
+ "serde",
+ "serde_json",
+ "simplelog",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
+
+[[package]]
+name = "link-cplusplus"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "maildir"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879a6ae6743ab8219fdee64a569094485bfe18434e82b78b27fac5cce09e1437"
+dependencies = [
+ "gethostname",
+ "mailparse",
+]
+
+[[package]]
+name = "mailparse"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b56570f5f8c0047260d1c8b5b331f62eb9c660b9dd4071a8c46f8c7d3f280aa"
+dependencies = [
+ "charset",
+ "data-encoding",
+ "quoted_printable",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+
+[[package]]
+name = "os_str_bytes"
+version = "6.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "quoted_printable"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a24039f627d8285853cc90dcddf8c1ebfaa91f834566948872b225b9a28ed1b6"
+
+[[package]]
+name = "rustix"
+version = "0.36.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e"
+dependencies = [
+ "bitflags 1.3.2",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
+
+[[package]]
+name = "scratch"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
+
+[[package]]
+name = "serde"
+version = "1.0.158"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.158"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.4",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "simplelog"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acee08041c5de3d5048c8b3f6f13fafb3026b24ba43c6a695a0c76179b844369"
+dependencies = [
+ "log",
+ "termcolor",
+ "time 0.3.20",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c622ae390c9302e214c31013517c2061ecb2699935882c60a9b37f82f8625ae"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "time"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
+dependencies = [
+ "libc",
+ "wasi",
+ "winapi",
+]
+
+[[package]]
+name = "time"
+version = "0.3.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
+dependencies = [
+ "itoa",
+ "libc",
+ "num_threads",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
+
+[[package]]
+name = "time-macros"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
+dependencies = [
+ "time-core",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..5532356
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "jwebmail-extract"
+version = "0.6.0"
+authors = ["Jannis M. Hoffmann <jannis@fehcom.de>"]
+edition = "2021"
+rust-version = "1.62"
+resolver = "2"
+
+[profile.release]
+lto = true
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+libc = "0.2"
+maildir = "0.6"
+serde_json = "1.0"
+serde = { version = "1.0", features = ["derive"] }
+lazy_static = "1.4"
+mailparse = "0.14"
+chrono = "0.4"
+clap = { version = "4.0", features = ["derive"] }
+log = "0.4"
+simplelog = "0.12"
diff --git a/src/arguments.rs b/src/arguments.rs
new file mode 100644
index 0000000..505cc18
--- /dev/null
+++ b/src/arguments.rs
@@ -0,0 +1,125 @@
+use std::path::PathBuf;
+
+use clap::{value_parser, Parser, Subcommand};
+
+use crate::error::Error;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum SortKey {
+ Date,
+ Sender,
+ Subject,
+ Size,
+}
+
+#[derive(PartialEq, Eq, Clone)]
+pub enum SortOrder {
+ Ascending,
+ Descending,
+}
+
+#[derive(PartialEq, Eq, Clone)]
+pub struct SortInfo {
+ pub key: SortKey,
+ pub order: SortOrder,
+}
+
+impl std::str::FromStr for SortInfo {
+ type Err = Error;
+
+ fn from_str(mut value: &str) -> Result<Self, Self::Err> {
+ if value == "" {
+ return Ok(SortInfo {
+ key: SortKey::Date,
+ order: SortOrder::Ascending,
+ });
+ }
+ let order = if value.starts_with('!') {
+ value = &value[1..];
+ SortOrder::Descending
+ } else {
+ SortOrder::Ascending
+ };
+ let key = match value.to_ascii_lowercase().as_str() {
+ "date" => SortKey::Date,
+ "sender" => SortKey::Sender,
+ "subject" => SortKey::Subject,
+ "size" => SortKey::Size,
+ v => return Err(Error::SortOrder(format!("invalid sort order {:}", v))),
+ };
+ Ok(SortInfo { key, order })
+ }
+}
+
+impl std::fmt::Debug for SortInfo {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
+ write!(
+ f,
+ "{}{:?}",
+ match self.order {
+ SortOrder::Descending => "!",
+ SortOrder::Ascending => "",
+ },
+ self.key
+ )
+ }
+}
+
+#[derive(Subcommand)]
+pub enum Mode {
+ List {
+ subfolder: String,
+ start: usize,
+ end: usize,
+ #[arg(value_parser = value_parser!(SortInfo))]
+ sortby: SortInfo,
+ },
+ Search {
+ pattern: String,
+ subfolder: String,
+ },
+ Count {
+ subfolder: String,
+ },
+ Read {
+ subfolder: String,
+ mid: String,
+ },
+ Raw {
+ subfolder: String,
+ mid: String,
+ mime_path: String,
+ },
+ Folders,
+ Move {
+ mid: String,
+ from: String,
+ to: String,
+ },
+}
+
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+pub struct Arguments {
+ pub maildir_path: PathBuf,
+ pub sys_user: String,
+ pub mail_user: String,
+ #[command(subcommand)]
+ pub mode: Mode,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn sort_info() {
+ assert_eq!(
+ "!date".parse::<SortInfo>().unwrap(),
+ SortInfo {
+ key: SortKey::Date,
+ order: SortOrder::Descending
+ }
+ );
+ }
+}
diff --git a/src/cmd.rs b/src/cmd.rs
new file mode 100644
index 0000000..364fcfb
--- /dev/null
+++ b/src/cmd.rs
@@ -0,0 +1,90 @@
+use std::io::ErrorKind as IOErrKind;
+use std::path::PathBuf;
+
+use maildir::Maildir;
+use serde::Serialize as _;
+use serde::Serializer as _;
+
+mod count;
+mod folders;
+mod list;
+mod raw;
+
+use crate::error::Result;
+use crate::rfc822::{MIMEHeader, Mail, TopMailHeader};
+
+pub use count::{count, CountInfo};
+pub use folders::folders;
+pub use list::list;
+pub use raw::raw;
+
+pub enum Return {
+ Read(Mail),
+ Raw(MIMEHeader, Vec<u8>),
+ List(Vec<TopMailHeader>),
+ Folders(Vec<String>),
+ Count(CountInfo),
+ Search(Vec<Mail>),
+ Move,
+}
+
+pub fn serialize_to<W>(res: Result<Return>, mut write: W) -> std::io::Result<()>
+where
+ W: std::io::Write + Copy,
+{
+ let ser = &mut serde_json::Serializer::new(write);
+
+ match res {
+ Err(e) => {
+ e.serialize(ser)?;
+ std::process::exit(3)
+ }
+ Ok(r) => {
+ match r {
+ Return::Folders(fs) => fs.serialize(ser),
+ Return::Count(ci) => ci.serialize(ser),
+ Return::List(tmhs) => tmhs.serialize(ser),
+ Return::Search(ms) => ms.serialize(ser),
+ Return::Read(m) => m.serialize(ser),
+ Return::Raw(mh, b) => {
+ let r = match mh.serialize(ser) {
+ Ok(x) => x,
+ Err(e) => return Err(e.into()),
+ };
+ write.write_all(b"\n")?;
+ write.write_all(&b)?;
+ Ok(r)
+ }
+ Return::Move => ser.serialize_unit(),
+ }?;
+ Ok(())
+ }
+ }
+}
+
+pub fn open_submaildir(mut path: PathBuf, sub: &str) -> Maildir {
+ if sub != "" {
+ path.push(String::from(".") + sub);
+ }
+ Maildir::from(path)
+}
+
+pub fn read(md: &Maildir, mid: &str) -> Result<Mail> {
+ md.add_flags(mid, "S")?;
+
+ let mut mail = md.find(mid).ok_or_else(|| {
+ std::io::Error::new(IOErrKind::NotFound, format!("mail {} not found", mid))
+ })?;
+
+ Ok(mail.parsed()?.try_into()?)
+}
+
+pub fn move_mail(p: PathBuf, mid: &str, from_f: &str, to_f: &str) -> Result<()> {
+ let from = open_submaildir(p.clone(), from_f);
+ let to = open_submaildir(p, to_f);
+ from.move_to(mid, &to).map_err(|e| e.into())
+}
+
+pub fn search(_md: &Maildir, _pattern: &str) -> Result<Vec<Mail>> {
+ todo!()
+}
diff --git a/src/cmd/count.rs b/src/cmd/count.rs
new file mode 100644
index 0000000..0a6e883
--- /dev/null
+++ b/src/cmd/count.rs
@@ -0,0 +1,27 @@
+use maildir::Maildir;
+use serde::Serialize;
+
+use crate::error::Result;
+
+#[derive(Serialize)]
+pub struct CountInfo {
+ total_mails: u32,
+ byte_size: u64,
+ unread_mails: u32,
+}
+
+pub fn count(md: &Maildir) -> Result<CountInfo> {
+ Ok(CountInfo {
+ total_mails: md.count_cur() as u32,
+ unread_mails: md
+ .list_cur()
+ .filter(|x| x.as_ref().map_or(false, |z| !z.is_seen()))
+ .count() as u32,
+ byte_size: md
+ .path()
+ .join("cur")
+ .read_dir()?
+ .map(|x| x.map_or(0, |z| z.metadata().map_or(0, |y| y.len())))
+ .sum(),
+ })
+}
diff --git a/src/cmd/folders.rs b/src/cmd/folders.rs
new file mode 100644
index 0000000..7bf93f1
--- /dev/null
+++ b/src/cmd/folders.rs
@@ -0,0 +1,48 @@
+use std::collections::BTreeSet;
+use std::ffi::{OsStr, OsString};
+use std::path::Path;
+
+use lazy_static::lazy_static;
+use maildir::Maildir;
+
+use crate::error::Result;
+
+lazy_static! {
+ static ref REQUIRED_MAILDIR_DIRS: BTreeSet<OsString> = [
+ OsString::from("cur"),
+ "new".into(),
+ "tmp".into(),
+ "maildirfolder".into(),
+ ]
+ .into();
+}
+
+fn is_mailsubdir(p: &Path) -> bool {
+ p.is_dir()
+ && p.file_name()
+ .map_or(false, |fname| fname.to_string_lossy().starts_with('.'))
+ && p.read_dir()
+ .map(|dir| {
+ dir.filter_map(|child| {
+ child
+ .ok()
+ .and_then(|dir_entry| dir_entry.path().file_name().map(OsStr::to_owned))
+ })
+ .collect::<BTreeSet<_>>()
+ .is_superset(&REQUIRED_MAILDIR_DIRS)
+ })
+ .unwrap_or_default()
+}
+
+pub fn folders(md: &Maildir) -> Result<Vec<String>> {
+ let root_path = md.path();
+
+ let subdirs = root_path
+ .read_dir()?
+ .filter_map(|d| d.ok())
+ .filter(|d| is_mailsubdir(&d.path()))
+ .filter_map(|d| Some(d.path().file_name()?.to_string_lossy()[1..].to_owned()))
+ .collect();
+
+ Ok(subdirs)
+}
diff --git a/src/cmd/list.rs b/src/cmd/list.rs
new file mode 100644
index 0000000..0ec0389
--- /dev/null
+++ b/src/cmd/list.rs
@@ -0,0 +1,116 @@
+use std::cmp::Reverse;
+
+use log::warn;
+use maildir::Maildir;
+
+use crate::arguments::{SortInfo, SortKey, SortOrder};
+use crate::error::Result;
+use crate::rfc822::{MailHeader, TopMailHeader};
+
+fn from_or_sender<'a>(mh: &'a MailHeader) -> &'a str {
+ if mh.from.len() == 0 {
+ warn!("mail without from");
+ panic!()
+ }
+ if mh.from.len() == 1 {
+ &mh.from[0].address
+ } else {
+ &mh.sender.as_ref().unwrap().address
+ }
+}
+
+fn mid_to_rec_time(mid: &str) -> f64 {
+ let Some(dec) = mid.find('.') else {
+ warn!("Invaild mail-id {}", mid);
+ return 0.0;
+ };
+ let Some(sep) = mid[dec+1..].find('.') else {
+ return 0.0;
+ };
+ mid[..dec + 1 + sep].parse().unwrap()
+}
+
+fn sort_by_and_take(
+ mut entries: Vec<maildir::MailEntry>,
+ sortby: &SortInfo,
+ s: usize,
+ e: usize,
+) -> Vec<TopMailHeader> {
+ match sortby.key {
+ SortKey::Date => {
+ match sortby.order {
+ SortOrder::Ascending => entries.sort_by(|a, b| {
+ mid_to_rec_time(a.id())
+ .partial_cmp(&mid_to_rec_time(b.id()))
+ .unwrap()
+ }),
+ SortOrder::Descending => entries.sort_by(|b, a| {
+ mid_to_rec_time(a.id())
+ .partial_cmp(&mid_to_rec_time(b.id()))
+ .unwrap()
+ }),
+ }
+ entries
+ .drain(s..e)
+ .filter_map(|me| me.try_into().map_err(|e| warn!("{}", e)).ok())
+ .collect()
+ }
+ SortKey::Size => {
+ match sortby.order {
+ SortOrder::Ascending => {
+ entries.sort_by_cached_key(|a| a.path().metadata().map_or(0, |m| m.len()))
+ }
+ SortOrder::Descending => entries
+ .sort_by_cached_key(|a| Reverse(a.path().metadata().map_or(0, |m| m.len()))),
+ }
+ entries
+ .drain(s..e)
+ .filter_map(|me| me.try_into().map_err(|e| warn!("{}", e)).ok())
+ .collect()
+ }
+ SortKey::Subject => {
+ let mut x: Vec<TopMailHeader> = entries
+ .drain(..)
+ .filter_map(|me| me.try_into().map_err(|e| warn!("{}", e)).ok())
+ .collect();
+ match sortby.order {
+ SortOrder::Ascending => x.sort_by(|a, b| a.head.subject.cmp(&b.head.subject)),
+ SortOrder::Descending => x.sort_by(|b, a| a.head.subject.cmp(&b.head.subject)),
+ }
+ x.drain(s..e).collect()
+ }
+ SortKey::Sender => {
+ let mut x: Vec<TopMailHeader> = entries
+ .drain(..)
+ .filter_map(|me| me.try_into().map_err(|e| warn!("{}", e)).ok())
+ .collect();
+ match sortby.order {
+ SortOrder::Ascending => {
+ x.sort_by(|a, b| from_or_sender(&a.head).cmp(from_or_sender(&b.head)))
+ }
+ SortOrder::Descending => {
+ x.sort_by(|b, a| from_or_sender(&a.head).cmp(from_or_sender(&b.head)))
+ }
+ }
+ x.drain(s..e).collect()
+ }
+ }
+}
+
+pub fn list(md: &Maildir, i: usize, j: usize, sortby: &SortInfo) -> Result<Vec<TopMailHeader>> {
+ for r in md.list_new() {
+ match r {
+ Err(e) => warn!("{}", e),
+ Ok(me) => {
+ if let Err(e) = md.move_new_to_cur(me.id()) {
+ warn!("{}", e);
+ }
+ }
+ };
+ }
+
+ let a: Vec<_> = md.list_cur().filter_map(std::result::Result::ok).collect();
+ let start = std::cmp::min(a.len(), i);
+ let end = std::cmp::min(a.len(), j);
+ Ok(sort_by_and_take(a, sortby, start, end))
+}
diff --git a/src/cmd/raw.rs b/src/cmd/raw.rs
new file mode 100644
index 0000000..70e2632
--- /dev/null
+++ b/src/cmd/raw.rs
@@ -0,0 +1,89 @@
+use std::fs::read;
+use std::io::ErrorKind as IOErrKind;
+
+use maildir::Maildir;
+
+use crate::error::{Error, Result};
+use crate::rfc822::{parse_mail_content, MIMEHeader};
+
+pub fn raw(md: &Maildir, mid: &str, mime_path: &str) -> Result<(MIMEHeader, Vec<u8>)> {
+ let mut mail = md.find(mid).ok_or_else(|| {
+ std::io::Error::new(IOErrKind::NotFound, format!("mail {} not found", mid))
+ })?;
+
+ if mime_path.is_empty() {
+ let mh = MIMEHeader {
+ maintype: "message".to_owned(),
+ subtype: "rfc822".to_owned(),
+ filename: mail.id().to_owned(),
+ content_disposition: "".to_owned(),
+ };
+
+ return Ok((mh, read(mail.path())?));
+ }
+
+ let path = mime_path
+ .split('.')
+ .map(|x| {
+ x.parse()
+ .map_err(|pe: std::num::ParseIntError| Error::PathError {
+ msg: pe.to_string(),
+ path: mime_path.to_owned(),
+ })
+ })
+ .collect::<Result<Vec<_>>>()?;
+ let mut m = mail.parsed()?;
+
+ if path[0] != 0 {
+ return Err(Error::PathError {
+ msg: "Message must be accessed by a 0".to_owned(),
+ path: mime_path.to_owned(),
+ });
+ }
+
+ for i in &path[1..] {
+ match &m.ctype.mimetype {
+ x if x.starts_with("message/") => {
+ if *i != 0 {
+ return Err(Error::PathError {
+ msg: "Message must be accessed by a 0".to_owned(),
+ path: mime_path.to_owned(),
+ });
+ }
+ let s: &'static _ = m.get_body_raw()?.leak();
+ m = mailparse::parse_mail(s)?;
+ }
+ x if x.starts_with("multipart/") => {
+ if *i >= m.subparts.len() {
+ return Err(Error::PathError {
+ msg: "Out of bounds access".to_owned(),
+ path: mime_path.to_owned(),
+ });
+ }
+ m = m.subparts.swap_remove(*i);
+ }
+ _ => {
+ return Err(Error::PathError {
+ msg: "Unable to descent into leaf component".to_owned(),
+ path: mime_path.to_owned(),
+ })
+ }
+ }
+ }
+
+ if m.ctype.mimetype.starts_with("multipart/") {
+ return Err(Error::PathError {
+ msg: "Can not show multipart component".to_owned(),
+ path: mime_path.to_owned(),
+ });
+ }
+
+ let mime_part = parse_mail_content(&m)?;
+ let content = if m.ctype.mimetype.starts_with("text/") {
+ m.get_body()?.into_bytes()
+ } else {
+ m.get_body_raw()?
+ };
+
+ Ok((mime_part, content))
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..fc0a21a
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,69 @@
+use std::borrow::Cow;
+
+use maildir::MailEntryError;
+use mailparse::MailParseError;
+use serde::ser::SerializeStruct as _;
+use serde_json::Error as JSONError;
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+#[derive(Debug)]
+pub enum Error {
+ IoError(std::io::Error),
+ MailEntryError(MailEntryError),
+ SortOrder(String),
+ Setuid(String),
+ JSONError(JSONError),
+ PathError { msg: String, path: String },
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
+ write!(f, "{:?}", self)
+ }
+}
+
+impl std::error::Error for Error {}
+
+impl serde::Serialize for Error {
+ fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut state = serializer.serialize_struct("Error", 1)?;
+ let err_str: Cow<str> = match self {
+ Error::IoError(e) => Cow::Owned(e.to_string()),
+ Error::MailEntryError(e) => Cow::Owned(e.to_string()),
+ Error::SortOrder(s) => Cow::Borrowed(s),
+ Error::Setuid(s) => Cow::Borrowed(s),
+ Error::JSONError(e) => Cow::Owned(e.to_string()),
+ Error::PathError { msg, path } => Cow::Owned(format!("{} {:?}", msg, path)),
+ };
+ state.serialize_field("error", &err_str)?;
+ state.end()
+ }
+}
+
+impl From<std::io::Error> for Error {
+ fn from(io_err: std::io::Error) -> Self {
+ Error::IoError(io_err)
+ }
+}
+
+impl From<MailEntryError> for Error {
+ fn from(me_err: MailEntryError) -> Self {
+ Error::MailEntryError(me_err)
+ }
+}
+
+impl From<MailParseError> for Error {
+ fn from(mp_err: MailParseError) -> Self {
+ Error::MailEntryError(MailEntryError::ParseError(mp_err))
+ }
+}
+
+impl From<JSONError> for Error {
+ fn from(j_err: JSONError) -> Self {
+ Error::JSONError(j_err)
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..8a8b84e
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,89 @@
+use std::ffi::{CStr, CString};
+use std::io::stdout;
+
+use clap::Parser as _;
+use maildir::Maildir;
+
+mod arguments;
+mod cmd;
+mod error;
+mod rfc822;
+
+use arguments::{Arguments, Mode};
+use cmd::{
+ count, folders, list, move_mail, open_submaildir, raw, read, search, serialize_to, Return,
+};
+use error::{Error, Result};
+
+fn switch_to_user(sys_user: &str) -> Result<()> {
+ unsafe {
+ *libc::__errno_location() = 0;
+ }
+ let c_sys_user =
+ CString::new(sys_user).map_err(|e| Error::Setuid(format!("nul char in user, {}", e)))?;
+ let user_info: *const libc::passwd = unsafe { libc::getpwnam(c_sys_user.as_ptr()) };
+ let err = unsafe { *libc::__errno_location() };
+ if err != 0 {
+ return Err(Error::Setuid(format!(
+ "error calling getpwnam {:?}",
+ unsafe { libc::strerror(err) }
+ )));
+ };
+ if user_info.is_null() {
+ return Err(Error::Setuid(format!("user {:?} does not exist", sys_user)));
+ };
+ let rc = unsafe { libc::setuid((*user_info).pw_uid) };
+ if rc != 0 {
+ let err = unsafe { *libc::__errno_location() };
+ return Err(Error::Setuid(format!(
+ "error calling setuid {:?}",
+ unsafe { CStr::from_ptr(libc::strerror(err)) }
+ )));
+ }
+ Ok(())
+}
+
+fn main() -> std::io::Result<()> {
+ simplelog::TermLogger::init(
+ simplelog::LevelFilter::Info,
+ simplelog::Config::default(),
+ simplelog::TerminalMode::Stderr,
+ simplelog::ColorChoice::Never,
+ )
+ .unwrap();
+
+ let args = Arguments::parse();
+
+ std::env::remove_var("PATH");
+ if let Err(e) = switch_to_user(&args.sys_user) {
+ serialize_to(Err(e), &stdout())?
+ }
+
+ let path = args.maildir_path.join(args.mail_user);
+
+ let res = match args.mode {
+ Mode::Read { subfolder, mid } => {
+ read(&open_submaildir(path, &subfolder), &mid).map(Return::Read)
+ }
+ Mode::Raw {
+ subfolder,
+ mid,
+ mime_path,
+ } => raw(&open_submaildir(path, &subfolder), &mid, &mime_path)
+ .map(|(h, t)| Return::Raw(h, t)),
+ Mode::List {
+ subfolder,
+ start,
+ end,
+ ref sortby,
+ } => list(&open_submaildir(path, &subfolder), start, end, sortby).map(Return::List),
+ Mode::Folders => folders(&Maildir::from(path)).map(Return::Folders),
+ Mode::Count { subfolder } => count(&open_submaildir(path, &subfolder)).map(Return::Count),
+ Mode::Search { pattern, subfolder } => {
+ search(&open_submaildir(path, &subfolder), &pattern).map(Return::Search)
+ }
+ Mode::Move { mid, from, to } => move_mail(path, &mid, &from, &to).map(|()| Return::Move),
+ };
+
+ serialize_to(res, &stdout())
+}
diff --git a/src/rfc822.rs b/src/rfc822.rs
new file mode 100644
index 0000000..f6e9287
--- /dev/null
+++ b/src/rfc822.rs
@@ -0,0 +1,548 @@
+use chrono::{DateTime, NaiveDateTime, Utc};
+use mailparse::{addrparse_header, body::Body, dateparse, DispositionType, ParsedMail};
+use serde::{ser::SerializeSeq, Serialize, Serializer};
+
+use crate::error::Error;
+
+#[derive(Serialize, Eq, Ord, Debug)]
+pub struct MailAddr {
+ pub display_name: String,
+ pub address: String,
+}
+
+impl PartialEq for MailAddr {
+ fn eq(&self, r: &Self) -> bool {
+ self.address == r.address
+ }
+}
+
+impl PartialOrd for MailAddr {
+ fn partial_cmp(&self, r: &Self) -> Option<std::cmp::Ordering> {
+ Some(self.cmp(r))
+ }
+}
+
+fn parse_mail_addrs(
+ inp: &mailparse::MailHeader,
+) -> Result<Vec<MailAddr>, mailparse::MailParseError> {
+ let mut mal = addrparse_header(inp)?;
+
+ Ok(mal
+ .drain(..)
+ .flat_map(|mail_addr| match mail_addr {
+ mailparse::MailAddr::Group(mut g) => g
+ .addrs
+ .drain(..)
+ .map(|s| MailAddr {
+ display_name: s.display_name.unwrap_or_default(),
+ address: s.addr,
+ })
+ .collect(),
+ mailparse::MailAddr::Single(s) => vec![MailAddr {
+ display_name: s.display_name.unwrap_or_default(),
+ address: s.addr,
+ }],
+ })
+ .collect())
+}
+
+// ----------------
+
+fn serialize_date_time<S>(dt: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ s.serialize_str(&dt.to_rfc3339())
+}
+
+fn serialize_sender<S>(oma: &Option<MailAddr>, s: S) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ if let Some(ma) = oma {
+ let mut seq = s.serialize_seq(Some(1))?;
+ seq.serialize_element(ma)?;
+ seq.end()
+ } else {
+ let seq = s.serialize_seq(Some(0))?;
+ seq.end()
+ }
+}
+
+#[derive(Serialize, Debug)]
+pub struct MailHeader {
+ #[serde(serialize_with = "serialize_date_time")]
+ #[serde(rename = "date")]
+ pub orig_date: DateTime<Utc>,
+
+ // originator fields
+ pub from: Vec<MailAddr>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ #[serde(serialize_with = "serialize_sender")]
+ pub sender: Option<MailAddr>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ reply_to: Option<Vec<MailAddr>>,
+
+ // destination fields
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ to: Vec<MailAddr>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ cc: Vec<MailAddr>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ bcc: Option<Vec<MailAddr>>,
+
+ /* identification fields
+ #[serde(skip_serializing_if = "String::is_empty")]
+ message_id: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ in_reply_to: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ references: Option<String>,
+ */
+ // informational fields
+ pub subject: String,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ comments: Vec<String>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ keywords: Vec<String>,
+
+ mime: MIMEHeader,
+}
+
+#[derive(Serialize, Debug)]
+pub struct MIMEHeader {
+ #[serde(rename = "content_maintype")]
+ pub maintype: String,
+ #[serde(rename = "content_subtype")]
+ pub subtype: String,
+ #[serde(skip_serializing_if = "String::is_empty")]
+ pub content_disposition: String,
+ #[serde(skip_serializing_if = "String::is_empty")]
+ pub filename: String,
+}
+
+enum ContentDisposition {
+ None,
+ Inline,
+ Attachment { filename: Option<String> },
+}
+
+#[derive(Serialize)]
+pub struct MIMEPart {
+ pub head: MIMEHeader,
+ body: MailBody,
+}
+
+#[derive(Serialize)]
+#[serde(untagged)]
+pub enum MailBody {
+ Discrete(String),
+ Multipart {
+ #[serde(skip_serializing_if = "String::is_empty")]
+ preamble: String,
+ parts: Vec<MIMEPart>,
+ #[serde(skip_serializing_if = "String::is_empty")]
+ epilogue: String,
+ },
+ Message(Box<Mail>),
+}
+
+#[derive(Serialize)]
+pub struct Mail {
+ head: MailHeader,
+ pub body: MailBody,
+}
+
+#[derive(Serialize, Debug)]
+pub struct TopMailHeader {
+ byte_size: u64,
+ unread: bool,
+ #[serde(serialize_with = "serialize_date_time")]
+ pub date_received: DateTime<Utc>,
+ message_handle: String,
+ pub head: MailHeader,
+}
+
+fn get_received(me: &mut maildir::MailEntry) -> i64 {
+ me.received().unwrap_or_else(|_| {
+ let mut id = me.id();
+ id = &id[..id.find('.').unwrap()];
+ id.parse().unwrap_or_default()
+ })
+}
+
+impl TryFrom<maildir::MailEntry> for TopMailHeader {
+ type Error = Error;
+
+ fn try_from(mut me: maildir::MailEntry) -> Result<Self, Self::Error> {
+ Ok(TopMailHeader {
+ byte_size: me.path().metadata()?.len(),
+ unread: !me.is_seen(),
+ date_received: DateTime::<Utc>::from_utc(
+ NaiveDateTime::from_timestamp_opt(get_received(&mut me), 0).unwrap(),
+ Utc,
+ ),
+ message_handle: me.id().to_owned(),
+ head: parse_mail_header(&me.parsed()?)?,
+ })
+ }
+}
+
+pub fn parse_mail_content(v: &ParsedMail) -> Result<MIMEHeader, maildir::MailEntryError> {
+ let mut c = MIMEHeader {
+ maintype: String::new(),
+ subtype: String::new(),
+ content_disposition: String::new(),
+ filename: String::new(),
+ };
+
+ {
+ let mut val = v.ctype.mimetype.clone();
+ if let Some(i) = val.find(';') {
+ val.truncate(i);
+ }
+ let j = val.find('/').unwrap();
+ c.subtype = val.split_off(j + 1);
+ val.pop();
+ c.maintype = val;
+ }
+
+ match v.get_content_disposition().disposition {
+ DispositionType::Inline => c.content_disposition = "inline".to_owned(),
+ DispositionType::Attachment => {
+ c.content_disposition = "attachment".to_owned();
+ if let Some(fname) = v.get_content_disposition().params.remove("filename") {
+ c.filename = fname;
+ }
+ }
+ _ => {}
+ }
+
+ for h in &v.headers {
+ let mut key = h.get_key();
+ let val = h.get_value();
+
+ key.make_ascii_lowercase();
+
+ match key.as_ref() {
+ "filename" => {
+ c.filename = val;
+ }
+ _ => {}
+ }
+ }
+
+ Ok(c)
+}
+
+fn parse_mail_header(pm: &ParsedMail) -> Result<MailHeader, maildir::MailEntryError> {
+ let v = &pm.headers;
+
+ let mut mh = MailHeader {
+ orig_date: Utc::now(),
+ from: Vec::new(),
+ sender: None,
+ reply_to: None,
+ to: Vec::new(),
+ cc: Vec::new(),
+ bcc: None,
+ subject: String::new(),
+ comments: Vec::new(),
+ keywords: Vec::new(),
+ mime: MIMEHeader {
+ maintype: String::new(),
+ subtype: String::new(),
+ content_disposition: String::new(),
+ filename: String::new(),
+ },
+ };
+
+ {
+ let mut val = pm.ctype.mimetype.clone();
+ if let Some(i) = val.find(';') {
+ val.truncate(i);
+ }
+ let j = val.find('/').unwrap();
+ mh.mime.subtype = val.split_off(j + 1);
+ val.pop();
+ mh.mime.maintype = val;
+ }
+
+ let mut key = String::new();
+
+ for y in v {
+ key.push_str(&y.get_key_ref());
+ let mut val = y.get_value();
+
+ key.make_ascii_lowercase();
+
+ match key.as_str() {
+ "date" => {
+ mh.orig_date = DateTime::<Utc>::from_utc(
+ NaiveDateTime::from_timestamp_opt(dateparse(&val)?, 0).unwrap(),
+ Utc,
+ )
+ }
+ "from" => {
+ if !mh.from.is_empty() {
+ return Err("from already set".into());
+ }
+ mh.from = parse_mail_addrs(y)?
+ }
+ "sender" => mh.sender = parse_mail_addrs(y)?.drain(0..1).next(),
+ "reply-to" => mh.reply_to = Some(parse_mail_addrs(y)?),
+ "to" => mh.to = parse_mail_addrs(y)?,
+ "cc" => mh.cc = parse_mail_addrs(y)?,
+ "bcc" => mh.bcc = Some(parse_mail_addrs(y)?),
+ "subject" => {
+ mh.subject = val;
+ }
+ "comments" => {
+ mh.comments.push(val);
+ }
+ "keywords" => {
+ mh.keywords.push(val);
+ }
+ "mime-version" => {
+ strip_comments(&mut val);
+ if val.trim() != "1.0" {
+ return Err(maildir::MailEntryError::DateError("unknown mime version"));
+ }
+ }
+ "content-disposition" => {
+ mh.mime.content_disposition = val;
+ }
+ "filename" => {
+ mh.mime.filename = val;
+ }
+ _ => {}
+ };
+
+ key.clear();
+ }
+
+ Ok(mh)
+}
+
+fn parse_mail_body(pm: &ParsedMail) -> Result<MailBody, maildir::MailEntryError> {
+ let body = if pm.ctype.mimetype.starts_with("message/") {
+ MailBody::Message(Box::new(
+ mailparse::parse_mail(pm.get_body()?.as_ref())?.try_into()?,
+ ))
+ } else if pm.subparts.is_empty() && pm.ctype.mimetype.starts_with("text/") {
+ let b = pm.get_body()?;
+ MailBody::Discrete(b)
+ } else if pm.subparts.is_empty() {
+ let b = match pm.get_body_encoded() {
+ Body::Base64(eb) => {
+ let db = eb.get_raw();
+ if db.len() < 512 * 1024 {
+ String::from_utf8_lossy(db).into_owned()
+ } else {
+ String::new()
+ }
+ }
+ Body::SevenBit(eb) => eb.get_as_string()?,
+ _ => todo!(),
+ };
+ MailBody::Discrete(b)
+ } else {
+ MailBody::Multipart {
+ preamble: String::new(),
+ parts: pm
+ .subparts
+ .iter()
+ .map(|part| {
+ Ok(MIMEPart {
+ head: parse_mail_content(part)?,
+ body: parse_mail_body(part)?,
+ })
+ })
+ .filter_map(|p: Result<MIMEPart, maildir::MailEntryError>| p.ok())
+ .collect(),
+ epilogue: String::new(),
+ }
+ };
+ Ok(body)
+}
+
+enum FindMatchParen {
+ Open,
+ Close,
+}
+
+impl FindMatchParen {
+ fn value(&self) -> char {
+ match self {
+ FindMatchParen::Open => '(',
+ FindMatchParen::Close => ')',
+ }
+ }
+
+ fn len(&self) -> usize {
+ 1
+ }
+
+ fn of_char(c: char) -> Option<Self> {
+ match c {
+ '(' => Some(FindMatchParen::Open),
+ ')' => Some(FindMatchParen::Close),
+ _ => None,
+ }
+ }
+}
+
+fn find_in_header(s: &str, f: FindMatchParen) -> Option<usize> {
+ let mut in_q = false;
+ let mut q_pair = false;
+ let mut open_p = 0;
+
+ for (i, c) in s.char_indices() {
+ if q_pair {
+ q_pair = false;
+ continue;
+ }
+ match c {
+ '\\' => {
+ q_pair = true;
+ }
+ '"' => {
+ in_q = !in_q;
+ }
+ _ if !in_q => {
+ if open_p == 0 {
+ if c == f.value() {
+ return Some(i);
+ }
+ if c == FindMatchParen::Open.value() {
+ open_p += 1;
+ }
+ } else {
+ match FindMatchParen::of_char(c) {
+ Some(FindMatchParen::Open) => open_p += 1,
+ Some(FindMatchParen::Close) => open_p -= 1,
+ None => {}
+ }
+ }
+ }
+ _ => {}
+ };
+ }
+ None
+}
+
+fn find_pair(offset: usize, s: &str) -> Option<std::ops::Range<usize>> {
+ if let Some(open) = find_in_header(s, FindMatchParen::Open) {
+ if let Some(mut close) = find_in_header(
+ &s[open + FindMatchParen::Open.len()..],
+ FindMatchParen::Close,
+ ) {
+ close += open + FindMatchParen::Open.len();
+ Some(offset + open..offset + close + FindMatchParen::Close.len())
+ } else {
+ find_pair(
+ offset + open + FindMatchParen::Open.len(),
+ &s[open + FindMatchParen::Open.len()..],
+ )
+ }
+ } else {
+ None
+ }
+}
+
+fn strip_comments(s: &mut String) {
+ let mut off = 0;
+ loop {
+ if let Some(r) = find_pair(off, &s[off..]) {
+ s.drain(r.clone());
+ off = r.start;
+ } else {
+ break;
+ }
+ }
+}
+
+impl TryFrom<ParsedMail<'_>> for Mail {
+ type Error = maildir::MailEntryError;
+
+ fn try_from(m: ParsedMail) -> Result<Self, Self::Error> {
+ let head = parse_mail_header(&m)?;
+ let body = parse_mail_body(&m)?;
+
+ Ok(Mail { head, body })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn comment() {
+ let mut x = r#"(this is ((some) text)) a "some text with (comment \" in) quotes)(" (example) \( included) (xx)b()"#.to_owned();
+ strip_comments(&mut x);
+ assert_eq!(
+ &x,
+ r#" a "some text with (comment \" in) quotes)(" \( included) b"#
+ );
+ }
+
+ #[test]
+ fn unclosed_comment() {
+ let mut x = "(this is (some text) example b".to_owned();
+ strip_comments(&mut x);
+ assert_eq!(&x, "(this is example b");
+ }
+
+ #[test]
+ fn find_first_pair() {
+ let mut r = find_pair(0, "abc def");
+ assert_eq!(r, None);
+
+ r = find_pair(0, "abc ( def");
+ assert_eq!(r, None);
+
+ r = find_pair(0, "abc ) def");
+ assert_eq!(r, None);
+
+ let s = "(abc) def";
+ if let Some(i) = find_pair(0, s) {
+ assert_eq!(i, 0..5);
+ assert_eq!(&s[i], "(abc)");
+ } else {
+ assert!(false, "Got None expected Some!");
+ }
+
+ let s = "abc (def) ghi";
+ if let Some(i) = find_pair(0, s) {
+ assert_eq!(i, 4..9);
+ assert_eq!(&s[i], "(def)");
+ } else {
+ assert!(false, "Got None expected Some!");
+ }
+
+ let s = "(abc (def) ghi";
+ if let Some(i) = find_pair(0, s) {
+ assert_eq!(i, 5..10);
+ assert_eq!(&s[i], "(def)");
+ } else {
+ assert!(false, "Got None expected Some!");
+ }
+
+ let s = "abc ((def) ghi)";
+ if let Some(i) = find_pair(0, s) {
+ assert_eq!(i, 4..15);
+ assert_eq!(&s[i], "((def) ghi)");
+ } else {
+ assert!(false, "Got None expected Some!");
+ }
+
+ let s = r#" a "some text with (comment \" in) quotes)(" (example)"#;
+ if let Some(i) = find_pair(0, s) {
+ assert_eq!(i, 45..54);
+ assert_eq!(&s[i], "(example)");
+ } else {
+ assert!(false, "Got None expected Some!");
+ }
+ }
+}