diff options
author | Jannis M. Hoffmann <jannis@fehcom.de> | 2023-08-15 15:13:16 +0200 |
---|---|---|
committer | Jannis M. Hoffmann <jannis@fehcom.de> | 2023-08-15 15:13:16 +0200 |
commit | 161b052713f5cee08f20ce6ade0881e274c47fdd (patch) | |
tree | 4f541f79bb4c4a4089373926ca66783a4af26119 |
initial commit
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Cargo.lock | 786 | ||||
-rw-r--r-- | Cargo.toml | 24 | ||||
-rw-r--r-- | src/arguments.rs | 125 | ||||
-rw-r--r-- | src/cmd.rs | 90 | ||||
-rw-r--r-- | src/cmd/count.rs | 27 | ||||
-rw-r--r-- | src/cmd/folders.rs | 48 | ||||
-rw-r--r-- | src/cmd/list.rs | 116 | ||||
-rw-r--r-- | src/cmd/raw.rs | 89 | ||||
-rw-r--r-- | src/error.rs | 69 | ||||
-rw-r--r-- | src/main.rs | 89 | ||||
-rw-r--r-- | src/rfc822.rs | 548 |
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!"); + } + } +} |