Principal querying support + Draft import/export CLI implementation

This commit is contained in:
mdecimus 2023-07-05 19:34:57 +02:00
parent 1cb539ce52
commit 1f4204c6bf
24 changed files with 1529 additions and 237 deletions

327
Cargo.lock generated
View file

@ -273,9 +273,9 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.69"
version = "0.1.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2d0f03b3640e3a630367e40c468cb7f309529c708ed1d88597047b0e7c6ef7"
checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf"
dependencies = [
"proc-macro2",
"quote",
@ -484,7 +484,7 @@ dependencies = [
"lazycell",
"log",
"peeking_take_while",
"prettyplease 0.2.9",
"prettyplease 0.2.10",
"proc-macro2",
"quote",
"regex",
@ -505,6 +505,9 @@ name = "bitflags"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
dependencies = [
"serde",
]
[[package]]
name = "bitpacking"
@ -693,9 +696,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.3.10"
version = "4.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384e169cc618c613d5e3ca6404dda77a8685a63e08660dcc64abaf7da7cb0c7a"
checksum = "1640e5cc7fb47dbb8338fd471b105e7ed6c3cb2aeb00c2e067127ffd3764a05d"
dependencies = [
"clap_builder",
"clap_derive",
@ -704,9 +707,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.3.10"
version = "4.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef137bbe35aab78bdb468ccfba75a5f4d8321ae011d34063770780545176af2d"
checksum = "98c59138d527eeaf9b53f35a77fcc1fad9d883116070c63d5de1c7dc7b00c72b"
dependencies = [
"anstream",
"anstyle",
@ -787,9 +790,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cpufeatures"
version = "0.2.8"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c"
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
dependencies = [
"libc",
]
@ -959,17 +962,6 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "der"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
dependencies = [
"const-oid",
"pem-rfc7468 0.6.0",
"zeroize",
]
[[package]]
name = "der"
version = "0.7.7"
@ -977,7 +969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7ed52955ce76b1554f509074bb357d3fb8ac9b51288a65a3fd480d1dfba946"
dependencies = [
"const-oid",
"pem-rfc7468 0.7.0",
"pem-rfc7468",
"zeroize",
]
@ -1034,7 +1026,7 @@ dependencies = [
"password-hash 0.5.0",
"pbkdf2 0.12.1",
"pwhash",
"rustls 0.21.2",
"rustls 0.21.3",
"scrypt",
"sha1",
"sha2 0.10.7",
@ -1116,12 +1108,12 @@ version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0997c976637b606099b9985693efa3581e84e41f5c11ba5255f88711058ad428"
dependencies = [
"der 0.7.7",
"der",
"digest 0.10.7",
"elliptic-curve",
"rfc6979",
"signature",
"spki 0.7.2",
"spki",
]
[[package]]
@ -1164,8 +1156,8 @@ dependencies = [
"generic-array",
"group",
"hkdf",
"pem-rfc7468 0.7.0",
"pkcs8 0.10.2",
"pem-rfc7468",
"pkcs8",
"rand_core",
"sec1",
"subtle",
@ -1205,6 +1197,12 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "equivalent"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
[[package]]
name = "errno"
version = "0.3.1"
@ -1598,7 +1596,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap",
"indexmap 1.9.3",
"slab",
"tokio",
"tokio-util",
@ -1644,9 +1642,9 @@ dependencies = [
[[package]]
name = "hermit-abi"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hex"
@ -1813,13 +1811,14 @@ dependencies = [
[[package]]
name = "hyper-rustls"
version = "0.24.0"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7"
checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97"
dependencies = [
"futures-util",
"http",
"hyper 0.14.27",
"rustls 0.21.2",
"rustls 0.21.3",
"tokio",
"tokio-rustls 0.24.1",
]
@ -1894,7 +1893,7 @@ dependencies = [
"mail-send",
"md5",
"parking_lot",
"rustls 0.21.2",
"rustls 0.21.3",
"rustls-pemfile",
"store",
"tokio",
@ -1924,6 +1923,16 @@ dependencies = [
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
]
[[package]]
name = "indicatif"
version = "0.17.5"
@ -1991,7 +2000,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb"
dependencies = [
"hermit-abi",
"rustix 0.38.2",
"rustix 0.38.3",
"windows-sys 0.48.0",
]
@ -2006,9 +2015,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.7"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0aa48fab2893d8a49caa94082ae8488f4e1050d73b367881dcd2198f4199fd8"
checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
[[package]]
name = "jemalloc-sys"
@ -2089,7 +2098,7 @@ dependencies = [
[[package]]
name = "jmap-client"
version = "0.3.0"
source = "git+https://github.com/stalwartlabs/jmap-client#ab6a9e55c2008e18422b9346dbc5220ee0a47df3"
source = "git+https://github.com/stalwartlabs/jmap-client#a55af189d41a21cf5a51c1c69852cf6143cc8102"
dependencies = [
"ahash 0.8.3",
"async-stream",
@ -2099,7 +2108,7 @@ dependencies = [
"maybe-async 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot",
"reqwest",
"rustls 0.21.2",
"rustls 0.21.3",
"serde",
"serde_json",
"tokio",
@ -2180,7 +2189,7 @@ dependencies = [
"nom",
"percent-encoding",
"ring",
"rustls 0.21.2",
"rustls 0.21.3",
"rustls-native-certs",
"thiserror",
"tokio",
@ -2352,7 +2361,7 @@ dependencies = [
"mail-builder",
"md5",
"rand",
"rustls 0.21.2",
"rustls 0.21.3",
"smtp-proto",
"tokio",
"tokio-rustls 0.24.1",
@ -2391,7 +2400,7 @@ dependencies = [
"mail-send",
"md5",
"parking_lot",
"rustls 0.21.2",
"rustls 0.21.3",
"rustls-pemfile",
"sieve-rs",
"store",
@ -2419,7 +2428,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata",
"regex-automata 0.1.10",
]
[[package]]
@ -2800,7 +2809,7 @@ dependencies = [
"fnv",
"futures-channel",
"futures-util",
"indexmap",
"indexmap 1.9.3",
"js-sys",
"once_cell",
"pin-project-lite",
@ -2904,9 +2913,9 @@ dependencies = [
[[package]]
name = "paste"
version = "1.0.12"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79"
checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35"
[[package]]
name = "pbkdf2"
@ -2938,15 +2947,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "pem-rfc7468"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac"
dependencies = [
"base64ct",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
@ -2969,7 +2969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4"
dependencies = [
"fixedbitset",
"indexmap",
"indexmap 1.9.3",
]
[[package]]
@ -3058,24 +3058,13 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs1"
version = "0.4.1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der 0.6.1",
"pkcs8 0.9.0",
"spki 0.6.0",
"zeroize",
]
[[package]]
name = "pkcs8"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
dependencies = [
"der 0.6.1",
"spki 0.6.0",
"der",
"pkcs8",
"spki",
]
[[package]]
@ -3084,8 +3073,8 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der 0.7.7",
"spki 0.7.2",
"der",
"spki",
]
[[package]]
@ -3130,9 +3119,9 @@ dependencies = [
[[package]]
name = "prettyplease"
version = "0.2.9"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9825a04601d60621feed79c4e6b56d65db77cdca55cef43b46b0de1096d1c282"
checksum = "92139198957b410250d43fad93e630d956499a625c527eda65175c8680f83387"
dependencies = [
"proc-macro2",
"syn 2.0.23",
@ -3377,13 +3366,14 @@ dependencies = [
[[package]]
name = "regex"
version = "1.8.4"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
checksum = "89089e897c013b3deb627116ae56a6955a72b8bed395c9526af31c9fe528b484"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.2",
"regex-automata 0.3.0",
"regex-syntax 0.7.3",
]
[[package]]
@ -3395,6 +3385,17 @@ dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa250384981ea14565685dea16a9ccc4d1c541a13f82b9c168572264d1df8c56"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.3",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
@ -3403,9 +3404,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846"
[[package]]
name = "reqwest"
@ -3430,7 +3431,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls 0.21.2",
"rustls 0.21.3",
"rustls-pemfile",
"serde",
"serde_json",
@ -3523,20 +3524,22 @@ dependencies = [
[[package]]
name = "rsa"
version = "0.8.2"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55a77d189da1fee555ad95b7e50e7457d91c0e089ec68ca69ad2989413bbdab4"
checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8"
dependencies = [
"byteorder",
"const-oid",
"digest 0.10.7",
"num-bigint-dig",
"num-integer",
"num-iter",
"num-traits",
"pkcs1",
"pkcs8 0.9.0",
"pkcs8",
"rand_core",
"signature",
"spki",
"subtle",
"zeroize",
]
@ -3640,9 +3643,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.37.22"
version = "0.37.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8818fa822adcc98b18fedbb3632a6a33213c070556b5aa7c4c8cc21cff565c4c"
checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
dependencies = [
"bitflags 1.3.2",
"errno",
@ -3654,9 +3657,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.2"
version = "0.38.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aabcb0461ebd01d6b79945797c27f8529082226cb630a9865a71870ff63532a4"
checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4"
dependencies = [
"bitflags 2.3.3",
"errno",
@ -3679,13 +3682,13 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.21.2"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f"
checksum = "b19faa85ecb5197342b54f987b142fb3e30d0c90da40f80ef4fa9a726e6676ed"
dependencies = [
"log",
"ring",
"rustls-webpki",
"rustls-webpki 0.101.0",
"sct",
]
@ -3721,16 +3724,26 @@ dependencies = [
]
[[package]]
name = "rustversion"
version = "1.0.12"
name = "rustls-webpki"
version = "0.101.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
checksum = "89efed4bd0af2a8de0feb22ba38030244c93db56112b8aa67d27022286852b1c"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f"
[[package]]
name = "ryu"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
[[package]]
name = "salsa20"
@ -3743,11 +3756,11 @@ dependencies = [
[[package]]
name = "schannel"
version = "0.1.21"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
dependencies = [
"windows-sys 0.42.0",
"windows-sys 0.48.0",
]
[[package]]
@ -3794,9 +3807,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0aec48e813d6b90b15f0b8948af3c63483992dee44c03e9930b3eebdabe046e"
dependencies = [
"base16ct",
"der 0.7.7",
"der",
"generic-array",
"pkcs8 0.10.2",
"pkcs8",
"subtle",
"zeroize",
]
@ -3826,27 +3839,27 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.164"
version = "1.0.166"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d"
checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_bytes"
version = "0.11.9"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294"
checksum = "5a16be4fe5320ade08736447e3198294a5ea9a6d44dde6f35f0a5e06859c427a"
dependencies = [
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.164"
version = "1.0.166"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6"
dependencies = [
"proc-macro2",
"quote",
@ -3855,9 +3868,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.99"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3"
checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c"
dependencies = [
"itoa",
"ryu",
@ -4041,7 +4054,7 @@ dependencies = [
"rayon",
"regex",
"reqwest",
"rustls 0.21.2",
"rustls 0.21.3",
"rustls-pemfile",
"serde",
"serde_json",
@ -4098,16 +4111,6 @@ dependencies = [
"lock_api",
]
[[package]]
name = "spki"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
dependencies = [
"base64ct",
"der 0.6.1",
]
[[package]]
name = "spki"
version = "0.7.2"
@ -4115,7 +4118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a"
dependencies = [
"base64ct",
"der 0.7.7",
"der",
]
[[package]]
@ -4131,9 +4134,9 @@ dependencies = [
[[package]]
name = "sqlx"
version = "0.7.0-alpha.3"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afd8985c8822235a9ebeedf0bff971462470162759663d3184593c807ab6e898"
checksum = "91ef53c86d2066e04f0ac6b1364f16d13d82388e2d07f11a5c71782345555761"
dependencies = [
"sqlx-core",
"sqlx-macros",
@ -4144,13 +4147,12 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.7.0-alpha.3"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c12403de02d88e6808de30eb2153c6997d39cc9511a446b510d5944a3ea6727"
checksum = "8a22fd81e9c1ad53c562edb869ff042b215d4eadefefc4784bacfbfd19835945"
dependencies = [
"ahash 0.7.6",
"ahash 0.8.3",
"atoi",
"bitflags 1.3.2",
"byteorder",
"bytes",
"crc",
@ -4165,13 +4167,13 @@ dependencies = [
"futures-util",
"hashlink",
"hex",
"indexmap",
"indexmap 2.0.0",
"log",
"memchr",
"once_cell",
"paste",
"percent-encoding",
"rustls 0.21.2",
"rustls 0.21.3",
"rustls-pemfile",
"serde",
"serde_json",
@ -4188,9 +4190,9 @@ dependencies = [
[[package]]
name = "sqlx-macros"
version = "0.7.0-alpha.3"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2be74801a0852ace9d86bc8cc8ac36241e7dc712fea26b8f32bd80ce29c98a10"
checksum = "00bb7c096a202b8164c175614cbfb79fe0e1e0a3d50e0374526183ef2974e4a2"
dependencies = [
"proc-macro2",
"quote",
@ -4201,9 +4203,9 @@ dependencies = [
[[package]]
name = "sqlx-macros-core"
version = "0.7.0-alpha.3"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ce71dd8afc7ad2aeff001bb6affa7128c9087bbdcab07fa97a7952e8ee3d1da"
checksum = "37d644623ab9699014e5b3cb61a040d16caa50fd477008f63f1399ae35498a58"
dependencies = [
"dotenvy",
"either",
@ -4227,13 +4229,13 @@ dependencies = [
[[package]]
name = "sqlx-mysql"
version = "0.7.0-alpha.3"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c430536df19e8b5b048a9ae19b266aba77f9f3e2255b7195f465d678cb2d0a"
checksum = "8264c59b28b6858796acfcedc660aa4c9075cc6e4ec8eb03cdca2a3e725726db"
dependencies = [
"atoi",
"base64 0.21.2",
"bitflags 1.3.2",
"bitflags 2.3.3",
"byteorder",
"bytes",
"crc",
@ -4269,13 +4271,13 @@ dependencies = [
[[package]]
name = "sqlx-postgres"
version = "0.7.0-alpha.3"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "210e0a1523b6d46ca73db1c5197a233a8e14787596910ce88ff5d47a00da0241"
checksum = "1cab6147b81ca9213a7578f1b4c9d24c449a53953cd2222a7b5d7cd29a5c3139"
dependencies = [
"atoi",
"base64 0.21.2",
"bitflags 1.3.2",
"bitflags 2.3.3",
"byteorder",
"crc",
"dotenvy",
@ -4308,9 +4310,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.7.0-alpha.3"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f446c04b2d2d06b49b905e33c877b282e0f70b1b60a22513eacee8bf56d8afbe"
checksum = "59fba60afa64718104b71eec6984f8779d4caffff3b30cde91a75843c7efc126"
dependencies = [
"atoi",
"flume",
@ -4462,7 +4464,7 @@ dependencies = [
"cfg-if",
"fastrand",
"redox_syscall 0.3.5",
"rustix 0.37.22",
"rustix 0.37.23",
"windows-sys 0.48.0",
]
@ -4507,7 +4509,7 @@ dependencies = [
"num_cpus",
"rayon",
"reqwest",
"rustls 0.21.2",
"rustls 0.21.3",
"rustls-pemfile",
"serde",
"serde_json",
@ -4526,18 +4528,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.40"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
checksum = "c16a64ba9387ef3fdae4f9c1a7f07a0997fce91985c0336f1ddc1822b3b37802"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.40"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
checksum = "d14928354b01c4d6a4f0e549069adef399a284e7995c7ccca94e8a07a5346c59"
dependencies = [
"proc-macro2",
"quote",
@ -4675,7 +4677,7 @@ version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls 0.21.2",
"rustls 0.21.3",
"tokio",
]
@ -4698,7 +4700,7 @@ checksum = "ec509ac96e9a0c43427c74f003127d953a265737636129424288d27cb5c4b12c"
dependencies = [
"futures-util",
"log",
"rustls 0.21.2",
"rustls 0.21.3",
"tokio",
"tokio-rustls 0.24.1",
"tungstenite",
@ -4772,7 +4774,7 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"indexmap",
"indexmap 1.9.3",
"pin-project",
"pin-project-lite",
"rand",
@ -4972,7 +4974,7 @@ dependencies = [
"httparse",
"log",
"rand",
"rustls 0.21.2",
"rustls 0.21.3",
"sha1",
"thiserror",
"url",
@ -4994,9 +4996,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
version = "1.0.9"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
[[package]]
name = "unicode-normalization"
@ -5082,7 +5084,7 @@ dependencies = [
"opentelemetry-otlp",
"opentelemetry-semantic-conventions",
"privdrop",
"rustls 0.21.2",
"rustls 0.21.3",
"rustls-pemfile",
"serde",
"smtp-proto",
@ -5253,7 +5255,7 @@ version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338"
dependencies = [
"rustls-webpki",
"rustls-webpki 0.100.1",
]
[[package]]
@ -5320,21 +5322,6 @@ dependencies = [
"windows-targets 0.48.1",
]
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.45.0"

View file

@ -31,9 +31,10 @@ use console::style;
use jmap_client::client::{Client, Credentials};
use modules::{
cli::{Cli, Commands},
export::cmd_export,
get,
import::cmd_import,
post,
is_localhost, post,
queue::cmd_queue,
report::cmd_report,
};
@ -56,7 +57,7 @@ async fn main() -> std::io::Result<()> {
if !credentials.is_empty() {
parse_credentials(&credentials)
} else {
oauth(&args.url)
oauth(&args.url).await
}
} else {
parse_credentials(&rpassword::prompt_password("\nEnter SMTP admin credentials: ").unwrap())
@ -65,6 +66,7 @@ async fn main() -> std::io::Result<()> {
if is_jmap {
let client = Client::new()
.credentials(credentials)
.accept_invalid_certs(is_localhost(&args.url))
.connect(&args.url)
.await
.unwrap_or_else(|err| {
@ -74,12 +76,13 @@ async fn main() -> std::io::Result<()> {
match args.command {
Commands::Import(command) => cmd_import(client, command).await,
Commands::Export(command) => cmd_export(client, command).await,
Commands::Queue(_) | Commands::Report(_) => unreachable!(),
}
} else {
match args.command {
Commands::Queue(command) => cmd_queue(&args.url, credentials, command),
Commands::Report(command) => cmd_report(&args.url, credentials, command),
Commands::Queue(command) => cmd_queue(&args.url, credentials, command).await,
Commands::Report(command) => cmd_report(&args.url, credentials, command).await,
_ => unreachable!(),
}
}
@ -95,11 +98,11 @@ fn parse_credentials(credentials: &str) -> Credentials {
}
}
fn oauth(url: &str) -> Credentials {
let metadata = get(&format!("{}/.well-known/oauth-authorization-server", url));
async fn oauth(url: &str) -> Credentials {
let metadata = get(&format!("{}/.well-known/oauth-authorization-server", url)).await;
let token_endpoint = metadata.property("token_endpoint");
let mut params = HashMap::from_iter([("client_id".to_string(), "Stalwart_CLI".to_string())]);
let response = post(metadata.property("device_authorization_endpoint"), &params);
let response = post(metadata.property("device_authorization_endpoint"), &params).await;
params.insert(
"grant_type".to_string(),
@ -119,7 +122,7 @@ fn oauth(url: &str) -> Credentials {
std::io::stdout().flush().unwrap();
std::io::stdin().lock().lines().next();
let mut response = post(token_endpoint, &params);
let mut response = post(token_endpoint, &params).await;
if let Some(serde_json::Value::String(access_token)) = response.remove("access_token") {
Credentials::Bearer(access_token)
} else {

View file

@ -41,10 +41,14 @@ pub struct Cli {
#[derive(Subcommand)]
pub enum Commands {
/// Import accounts and domains
/// Import JMAP accounts and Maildir/mbox mailboxes
#[clap(subcommand)]
Import(ImportCommands),
/// Export JMAP accounts
#[clap(subcommand)]
Export(ExportCommands),
/// Manage SMTP message queue
#[clap(subcommand)]
Queue(QueueCommands),
@ -62,16 +66,44 @@ pub enum ImportCommands {
#[clap(short, long)]
format: MailboxFormat,
/// Number of threads to use for message import, defaults to the number of CPUs.
/// Number of messages to import concurrently, defaults to the number of CPUs.
#[clap(short, long)]
num_threads: Option<usize>,
num_concurrent: Option<usize>,
/// Account id to import messages into
account_id: String,
/// Account name or email to import messages into
account: String,
/// Path to the mailbox to import, or '-' for stdin (stdin only supported for mbox)
path: String,
},
/// Import a JMAP account
Account {
/// Number of concurrent requests, defaults to the number of CPUs.
#[clap(short, long)]
num_concurrent: Option<usize>,
/// Account name or email to import messages into
account: String,
/// Path to the exported account directory
path: String,
},
}
#[derive(Subcommand)]
pub enum ExportCommands {
/// Export a JMAP account
Account {
/// Number of concurrent blob downloads to perform, defaults to the number of CPUs.
#[clap(short, long)]
num_concurrent: Option<usize>,
/// Account name or email to import messages into
account: String,
/// Path to export the account to
path: String,
},
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]

View file

@ -0,0 +1,406 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use futures::{stream::FuturesUnordered, StreamExt};
use jmap_client::{
client::Client,
email::{self, Email},
identity::{self, Identity},
mailbox::{self, Mailbox},
sieve::{self, SieveScript},
vacation_response::{self, VacationResponse},
};
use serde::Serialize;
use tokio::io::AsyncWriteExt;
use super::{cli::ExportCommands, name_to_id, UnwrapResult};
pub async fn cmd_export(mut client: Client, command: ExportCommands) {
match command {
ExportCommands::Account {
num_concurrent,
account,
path,
} => {
client.set_default_account_id(name_to_id(&client, &account).await);
let max_objects_in_get = client
.session()
.core_capabilities()
.map(|c| c.max_objects_in_get())
.unwrap_or(500);
// Create directory
let mut path = PathBuf::from(path);
if !path.is_dir() {
eprintln!("Directory {} does not exist.", path.display());
std::process::exit(1);
}
path.push(&account);
if !path.is_dir() {
std::fs::create_dir(&path).unwrap_or_else(|_| {
eprintln!("Failed to create directory: {}", path.display());
std::process::exit(1);
});
}
// Export metadata
let mut blobs = Vec::new();
export_mailboxes(&client, max_objects_in_get, &path).await;
export_emails(&client, max_objects_in_get, &mut blobs, &path).await;
export_sieve_scripts(&client, max_objects_in_get, &mut blobs, &path).await;
export_identities(&client, &path).await;
export_vacation_responses(&client, &path).await;
// Export blobs
path.push("blobs");
if !path.exists() {
std::fs::create_dir(&path).unwrap_or_else(|_| {
eprintln!("Failed to create directory: {}", path.display());
std::process::exit(1);
});
}
let client = Arc::new(client);
let num_concurrent = num_concurrent.unwrap_or_else(|| num_cpus::get());
let mut futures = FuturesUnordered::new();
eprintln!("Exporting {} blobs...", blobs.len());
for blob_id in blobs {
let client = client.clone();
let mut blob_path = path.clone();
blob_path.push(&blob_id);
futures.push(async move {
let bytes = client
.download(&blob_id)
.await
.unwrap_result("download blob");
tokio::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&blob_path)
.await
.unwrap_result(&format!("open {}", blob_path.display()))
.write_all(&bytes)
.await
.unwrap_result(&format!("write {}", blob_path.display()));
});
if futures.len() == num_concurrent {
futures.next().await.unwrap();
}
}
// Wait for remaining futures
while let Some(_) = futures.next().await {}
}
}
}
pub async fn fetch_mailboxes(client: &Client, max_objects_in_get: usize) -> Vec<Mailbox> {
let mut position = 0;
let mut results = Vec::new();
loop {
let mut request = client.build();
let query_result = request
.query_mailbox()
.calculate_total(true)
.position(position)
.limit(max_objects_in_get)
.result_reference();
request.get_mailbox().ids_ref(query_result).properties([
mailbox::Property::Id,
mailbox::Property::Name,
mailbox::Property::IsSubscribed,
mailbox::Property::ParentId,
mailbox::Property::Role,
mailbox::Property::SortOrder,
mailbox::Property::ACL,
]);
let mut response = request
.send()
.await
.unwrap_result("send JMAP request")
.unwrap_method_responses();
if response.len() != 2 {
eprintln!("Invalid response while fetching mailboxes");
std::process::exit(1);
}
let mut get_response = response
.pop()
.unwrap()
.unwrap_get_mailbox()
.unwrap_result("fetch mailboxes");
let mailboxes_part = get_response.take_list();
let total_mailboxes = response
.pop()
.unwrap()
.unwrap_query_mailbox()
.unwrap_result("query mailboxes")
.total()
.unwrap_or(0);
let mailboxes_part_len = mailboxes_part.len();
if mailboxes_part_len > 0 {
results.extend(mailboxes_part);
if results.len() < total_mailboxes {
position += mailboxes_part_len as i32;
continue;
}
}
break;
}
results
}
async fn export_mailboxes(client: &Client, max_objects_in_get: usize, path: &Path) {
eprintln!(
"Exported {} mailboxes.",
write_file(
path,
"mailboxes.json",
fetch_mailboxes(client, max_objects_in_get).await,
)
.await
);
}
pub async fn fetch_emails(client: &Client, max_objects_in_get: usize) -> Vec<Email> {
let mut position = 0;
let mut results = Vec::new();
loop {
let mut request = client.build();
let query_result = request
.query_email()
.calculate_total(true)
.position(position)
.limit(max_objects_in_get)
.result_reference();
request.get_email().ids_ref(query_result).properties([
email::Property::Id,
email::Property::MailboxIds,
email::Property::Keywords,
email::Property::ReceivedAt,
email::Property::BlobId,
email::Property::MessageId,
]);
let mut response = request
.send()
.await
.unwrap_result("send JMAP request")
.unwrap_method_responses();
if response.len() != 2 {
eprintln!("Invalid response while fetching emails");
std::process::exit(1);
}
let mut get_response = response
.pop()
.unwrap()
.unwrap_get_email()
.unwrap_result("fetch emails");
let emails_part = get_response.take_list();
let total_emails = response
.pop()
.unwrap()
.unwrap_query_email()
.unwrap_result("query emails")
.total()
.unwrap_or(0);
let emails_part_len = emails_part.len();
if emails_part_len > 0 {
results.extend(emails_part);
if results.len() < total_emails {
position += emails_part_len as i32;
continue;
}
}
break;
}
results
}
async fn export_emails(
client: &Client,
max_objects_in_get: usize,
blobs: &mut Vec<String>,
path: &Path,
) {
let emails = fetch_emails(client, max_objects_in_get).await;
for email in &emails {
if let Some(blob_id) = email.blob_id() {
blobs.push(blob_id.to_string());
} else {
eprintln!(
"Warning: email {:?} has no blobId",
email.id().unwrap_or_default()
);
}
}
eprintln!(
"Exported {} emails.",
write_file(path, "emails.json", emails,).await
);
}
pub async fn fetch_sieve_scripts(client: &Client, max_objects_in_get: usize) -> Vec<SieveScript> {
let mut position = 0;
let mut results = Vec::new();
loop {
let mut request = client.build();
let query_result = request
.query_sieve_script()
.calculate_total(true)
.position(position)
.limit(max_objects_in_get)
.result_reference();
request
.get_sieve_script()
.ids_ref(query_result)
.properties([
sieve::Property::Id,
sieve::Property::Name,
sieve::Property::BlobId,
sieve::Property::IsActive,
]);
let mut response = request
.send()
.await
.unwrap_result("send JMAP request")
.unwrap_method_responses();
if response.len() != 2 {
eprintln!("Invalid response while fetching sieve_scripts");
std::process::exit(1);
}
let mut get_response = response
.pop()
.unwrap()
.unwrap_get_sieve_script()
.unwrap_result("fetch sieve_scripts");
let sieve_scripts_part = get_response.take_list();
let total_sieve_scripts = response
.pop()
.unwrap()
.unwrap_query_sieve_script()
.unwrap_result("query sieve_scripts")
.total()
.unwrap_or(0);
let sieve_scripts_part_len = sieve_scripts_part.len();
if sieve_scripts_part_len > 0 {
results.extend(sieve_scripts_part);
if results.len() < total_sieve_scripts {
position += sieve_scripts_part_len as i32;
continue;
}
}
break;
}
results
}
async fn export_sieve_scripts(
client: &Client,
max_objects_in_get: usize,
blobs: &mut Vec<String>,
path: &Path,
) {
let sieves = fetch_sieve_scripts(client, max_objects_in_get).await;
for sieve in &sieves {
if let Some(blob_id) = sieve.blob_id() {
blobs.push(blob_id.to_string());
} else {
eprintln!(
"Warning: sieve script {:?} has no blobId",
sieve.id().unwrap_or_default()
);
}
}
eprintln!(
"Exported {} sieve scripts.",
write_file(path, "sieve.json", sieves,).await
);
}
pub async fn fetch_identities(client: &Client) -> Vec<Identity> {
let mut request = client.build();
request.get_identity().properties([
identity::Property::Id,
identity::Property::Name,
identity::Property::Email,
identity::Property::ReplyTo,
identity::Property::Bcc,
identity::Property::TextSignature,
identity::Property::HtmlSignature,
]);
request
.send_get_identity()
.await
.unwrap_result("send JMAP request")
.take_list()
}
async fn export_identities(client: &Client, path: &Path) {
eprintln!(
"Exported {} identities.",
write_file(path, "identities.json", fetch_identities(client).await).await
);
}
pub async fn fetch_vacation_responses(client: &Client) -> Vec<VacationResponse> {
let mut request = client.build();
request.get_vacation_response().properties([
vacation_response::Property::Id,
vacation_response::Property::FromDate,
vacation_response::Property::ToDate,
vacation_response::Property::Subject,
vacation_response::Property::TextBody,
vacation_response::Property::HtmlBody,
vacation_response::Property::IsEnabled,
]);
request
.send_get_vacation_response()
.await
.unwrap_result("send JMAP request")
.take_list()
}
async fn export_vacation_responses(client: &Client, path: &Path) {
eprintln!(
"Exported {} vacation responses.",
write_file(
path,
"vacation.json",
fetch_vacation_responses(client).await
)
.await
);
}
async fn write_file<T: Serialize>(path: &Path, name: &str, contents: Vec<T>) -> usize {
let mut path = PathBuf::from(path);
path.push(name);
let len = contents.len();
tokio::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&path)
.await
.unwrap_result(&format!("open {}", path.display()))
.write_all(serde_json::to_string(&contents).unwrap().as_bytes())
.await
.unwrap_result(&format!("write to {}", path.display()));
len
}

View file

@ -22,8 +22,9 @@
*/
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
io::{self, Cursor},
path::{Path, PathBuf},
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
@ -42,11 +43,17 @@ use mail_parser::mailbox::{
maildir,
mbox::{self, MessageIterator},
};
use serde::de::DeserializeOwned;
use tokio::{fs::File, io::AsyncReadExt};
use crate::modules::UnwrapResult;
use crate::modules::{name_to_id, UnwrapResult};
use super::{
cli::{ImportCommands, MailboxFormat},
export::{
fetch_emails, fetch_identities, fetch_mailboxes, fetch_sieve_scripts,
fetch_vacation_responses,
},
read_file,
};
@ -71,15 +78,15 @@ struct Message {
contents: Vec<u8>,
}
pub async fn cmd_import(client: Client, command: ImportCommands) {
pub async fn cmd_import(mut client: Client, command: ImportCommands) {
match command {
ImportCommands::Messages {
num_threads,
num_concurrent,
format,
account_id,
account,
path,
} => {
let account_id = Arc::new(account_id);
client.set_default_account_id(name_to_id(&client, &account).await);
let mut create_mailboxes = Vec::new();
let mut create_mailbox_names = Vec::new();
let mut create_mailbox_ids = Vec::new();
@ -138,15 +145,12 @@ pub async fn cmd_import(client: Client, command: ImportCommands) {
let mut children: HashMap<Option<&str>, Vec<&str>> =
HashMap::from_iter([(None, Vec::new())]);
let mut request = client.build();
request
.get_mailbox()
.account_id(account_id.as_ref())
.properties([
mailbox::Property::Name,
mailbox::Property::ParentId,
mailbox::Property::Role,
mailbox::Property::Id,
]);
request.get_mailbox().properties([
mailbox::Property::Name,
mailbox::Property::ParentId,
mailbox::Property::Role,
mailbox::Property::Id,
]);
let response = request
.send_get_mailbox()
.await
@ -215,7 +219,7 @@ pub async fn cmd_import(client: Client, command: ImportCommands) {
// Create any missing mailboxes
if has_missing_mailboxes {
let mut request = client.build();
let set_request = request.set_mailbox().account_id(account_id.as_ref());
let set_request = request.set_mailbox();
for pos in 0..create_mailbox_ids.len() {
if let MailboxId::None = create_mailbox_ids[pos] {
@ -267,13 +271,13 @@ pub async fn cmd_import(client: Client, command: ImportCommands) {
let client = Arc::new(client);
let total_imported = Arc::new(AtomicUsize::from(0));
let m = MultiProgress::new();
let num_threads = num_threads.unwrap_or_else(|| num_cpus::get());
let num_concurrent = num_concurrent.unwrap_or_else(|| num_cpus::get());
let spinner_style =
ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}")
.unwrap()
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ");
let pbs = Arc::new(Mutex::new((
(0..num_threads)
(0..num_concurrent)
.map(|n| {
let pb = m.add(ProgressBar::new(40));
pb.set_style(spinner_style.clone());
@ -292,7 +296,6 @@ pub async fn cmd_import(client: Client, command: ImportCommands) {
.zip(create_mailbox_names)
{
let mut futures = FuturesUnordered::new();
let mut outputs = Vec::new();
let mailbox_id = Arc::new(match mailbox_id {
MailboxId::ExistingId(id) => id.to_string(),
MailboxId::CreateId(id) => id,
@ -309,7 +312,6 @@ pub async fn cmd_import(client: Client, command: ImportCommands) {
Ok(message) => {
message_num += 1;
let client = client.clone();
let account_id = account_id.clone();
let mailbox_id = mailbox_id.clone();
let mailbox_name = mailbox_name.clone();
let total_imported = total_imported.clone();
@ -330,8 +332,7 @@ pub async fn cmd_import(client: Client, command: ImportCommands) {
}
if let Err(err) = client
.email_import_account(
&account_id,
.email_import(
message.contents,
[mailbox_id.as_ref()],
if !message.flags.is_empty() {
@ -370,8 +371,8 @@ pub async fn cmd_import(client: Client, command: ImportCommands) {
}
});
if futures.len() == num_threads {
outputs.push(futures.next().await.unwrap());
if futures.len() == num_concurrent {
futures.next().await.unwrap();
}
}
Err(e) => {
@ -384,9 +385,7 @@ pub async fn cmd_import(client: Client, command: ImportCommands) {
}
// Wait for remaining futures
while let Some(item) = futures.next().await {
outputs.push(item);
}
while let Some(_) = futures.next().await {}
}
// Done
@ -406,6 +405,544 @@ pub async fn cmd_import(client: Client, command: ImportCommands) {
}
}
}
ImportCommands::Account {
num_concurrent,
account,
path,
} => {
client.set_default_account_id(name_to_id(&client, &account).await);
let path = PathBuf::from(path);
if !path.exists() {
eprintln!("Path '{}' does not exist.", path.display());
return;
}
let num_concurrent = num_concurrent.unwrap_or_else(|| num_cpus::get());
// Import objects
import_emails(
&client,
&path,
import_mailboxes(&client, &path).await.into(),
num_concurrent,
)
.await;
import_sieve_scripts(&client, &path, num_concurrent).await;
import_identities(&client, &path).await;
import_vacation_responses(&client, &path).await;
}
}
}
async fn import_mailboxes(client: &Client, path: &Path) -> HashMap<String, String> {
// Deserialize mailboxes
let mailboxes = read_json::<jmap_client::mailbox::Mailbox>(path, "mailboxes.json").await;
if mailboxes.is_empty() {
return HashMap::new();
}
// Obtain current mailboxes
let existing_mailboxes = fetch_mailboxes(
client,
client
.session()
.core_capabilities()
.map(|c| c.max_objects_in_get())
.unwrap_or(500),
)
.await;
let nested_existing_mailboxes = build_mailbox_tree(&existing_mailboxes);
let mut id_mappings: HashMap<String, String> = HashMap::new();
let mut id_missing = Vec::new();
for (path, mailbox) in build_mailbox_tree(&mailboxes) {
let id = mailbox.id().unwrap_result("obtain mailbox id");
// Find existing mailbox based on role
if !matches!(mailbox.role(), Role::None) {
if let Some(existing_mailbox) = existing_mailboxes
.iter()
.find(|m| m.role() != mailbox.role())
{
id_mappings.insert(
id.to_string(),
existing_mailbox
.id()
.unwrap_result("obtain mailbox id")
.to_string(),
);
continue;
}
}
// Find existing mailbox by name
if let Some(mailbox) = nested_existing_mailboxes.get(&path) {
id_mappings.insert(
id.to_string(),
mailbox.id().unwrap_result("obtain mailbox id").to_string(),
);
} else {
id_missing.push(id);
}
}
let mut total_imported = 0;
if !id_missing.is_empty() {
let mut request = client.build();
let set_request = request.set_mailbox();
for mailbox in &mailboxes {
// Skip if mailbox already exists
let id = mailbox.id().unwrap_result("obtain mailbox id").to_string();
if id_mappings.contains_key(&id) {
continue;
}
let create_request = set_request
.create_with_id(&id)
.name(mailbox.name().unwrap())
.role(mailbox.role());
if let Some(parent_id) = mailbox.parent_id() {
if let Some(existing_id) = id_mappings.get(parent_id) {
create_request.parent_id(Some(existing_id.to_string()));
} else {
create_request.parent_id_ref(parent_id);
}
} else {
create_request.parent_id(None::<String>);
}
if mailbox.sort_order() > 0 {
create_request.sort_order(mailbox.sort_order());
}
if let Some(acls) = mailbox.acl() {
create_request.acls(acls.clone().into_iter());
}
if mailbox.is_subscribed() {
create_request.is_subscribed(true);
}
}
// Create mailboxes
let mut response = request
.send_set_mailbox()
.await
.unwrap_result("create mailboxes");
for missing_id in id_missing {
id_mappings.insert(
missing_id.to_string(),
response
.created(missing_id)
.unwrap_result("create mailbox")
.take_id(),
);
total_imported += 1;
}
}
eprintln!("Successfully imported {} mailboxes.", total_imported);
id_mappings
}
async fn import_emails(
client: &Client,
path: &Path,
mailbox_ids: Arc<HashMap<String, String>>,
num_concurrent: usize,
) {
// Deserialize emails
let emails = read_json::<jmap_client::email::Email>(path, "emails.json").await;
if emails.is_empty() {
return;
}
// Obtain existing emails
let existing_emails = fetch_emails(
client,
client
.session()
.core_capabilities()
.map(|c| c.max_objects_in_get())
.unwrap_or(500),
)
.await;
let existing_ids = existing_emails
.iter()
.filter_map(|email| email.message_id())
.collect::<HashSet<_>>();
let mut futures = FuturesUnordered::new();
let total_imported = Arc::new(AtomicUsize::from(0));
let mut path = PathBuf::from(path);
path.push("blobs");
for email in emails {
// Skip messages that already exist in the server
if let Some(message_ids) = email.message_id() {
if existing_ids.contains(message_ids) {
continue;
}
}
// Spawn import tasks
let mailbox_ids = mailbox_ids.clone();
let mut path = path.clone();
let total_imported = total_imported.clone();
futures.push(async move {
// Obtain mailbox ids
let id = if let Some(id) = email.id() {
id
} else {
eprintln!("Skipping email with no id");
return;
};
if email.mailbox_ids().is_empty() {
eprintln!("Skipping emailId {id} with no mailboxIds");
return;
}
let mut mailboxes = Vec::with_capacity(email.mailbox_ids().len());
for mailbox_id in email.mailbox_ids() {
if let Some(mailbox_id) = mailbox_ids.get(mailbox_id) {
mailboxes.push(mailbox_id.to_string());
} else {
eprintln!("Skipping emailId {id} with unknown mailboxId {mailbox_id}");
return;
}
}
let keywords = email.keywords();
// Read blob
if let Some(blob_id) = email.blob_id() {
path.push(blob_id);
} else {
eprintln!("Skipping emailId {id} with no blobId");
return;
}
let mut contents = vec![];
match File::open(&path).await {
Ok(mut file) => match file.read_to_end(&mut contents).await {
Ok(_) => {}
Err(err) => {
eprintln!(
"Failed to read blob file for emailId {id} at {path:?}: {err}",
id = id,
path = path,
err = err
);
return;
}
},
Err(err) => {
eprintln!(
"Failed to open blob file for emailId {id} at {path:?}: {err}",
id = id,
path = path,
err = err
);
return;
}
}
if let Err(err) = client
.email_import(
contents,
mailboxes,
if !keywords.is_empty() {
Some(keywords)
} else {
None
},
email.received_at(),
)
.await
{
eprintln!("Failed to import emailId {id}: {err}");
} else {
total_imported.fetch_add(1, Ordering::Relaxed);
}
});
if futures.len() == num_concurrent {
futures.next().await.unwrap();
}
}
// Wait for remaining futures
while let Some(_) = futures.next().await {}
// Done
eprintln!(
"Successfully imported {} messages.",
total_imported.load(Ordering::Relaxed)
);
}
async fn import_sieve_scripts(client: &Client, path: &Path, num_concurrent: usize) {
// Deserialize scripts
let scripts = read_json::<jmap_client::sieve::SieveScript>(path, "sieve.json").await;
if scripts.is_empty() {
return;
}
let existing_scripts = fetch_sieve_scripts(
client,
client
.session()
.core_capabilities()
.map(|c| c.max_objects_in_get())
.unwrap_or(500),
)
.await;
let mut path = PathBuf::from(path);
path.push("blobs");
// Spawn tasks
let mut futures = FuturesUnordered::new();
let total_imported = Arc::new(AtomicUsize::from(0));
'outer: for script in scripts {
// Skip scripts that already exist
for existing_script in &existing_scripts {
if existing_script.name() == script.name() {
continue 'outer;
}
}
let mut path = path.clone();
let total_imported = total_imported.clone();
futures.push(async move {
let id = if let Some(id) = script.id() {
id
} else {
eprintln!("Skipping script with no id.");
return;
};
// Read blob
let name = if let (Some(blob_id), Some(name)) = (script.blob_id(), script.name()) {
path.push(blob_id);
name
} else {
eprintln!("Skipping script {id} with no blobId and/or name");
return;
};
let mut contents = vec![];
match File::open(&path).await {
Ok(mut file) => match file.read_to_end(&mut contents).await {
Ok(_) => {}
Err(err) => {
eprintln!(
"Failed to read blob file for script {id} at {path:?}: {err}",
id = id,
path = path,
err = err
);
return;
}
},
Err(err) => {
eprintln!(
"Failed to open blob file for script {id} at {path:?}: {err}",
id = id,
path = path,
err = err
);
return;
}
}
// Upload blob
match client
.sieve_script_create(name, contents, script.is_active())
.await
{
Ok(_) => {
total_imported.fetch_add(1, Ordering::Relaxed);
}
Err(err) => {
eprintln!("Failed to import script {id}: {err}");
}
}
});
if futures.len() == num_concurrent {
futures.next().await.unwrap();
}
}
// Wait for remaining futures
while let Some(_) = futures.next().await {}
// Done
eprintln!(
"Successfully imported {} sieve script.",
total_imported.load(Ordering::Relaxed)
);
}
async fn import_identities(client: &Client, path: &Path) {
// Deserialize mailboxes
let identities = read_json::<jmap_client::identity::Identity>(path, "identities.json").await;
if identities.is_empty() {
return;
}
let existing_identities = fetch_identities(client).await;
let mut request = client.build();
let set_request = request.set_identity();
let mut create_ids = Vec::new();
'outer: for identity in &identities {
for existing_identity in &existing_identities {
if identity.name() == existing_identity.name()
&& identity.email() == existing_identity.email()
{
continue 'outer;
}
}
if let (Some(id), Some(name), Some(email)) =
(identity.id(), identity.name(), identity.email())
{
if name == "vacation" {
continue;
}
create_ids.push(id);
let create_request = set_request.create_with_id(id).name(name).email(email);
if let Some(reply_to) = identity.reply_to() {
create_request.reply_to(reply_to.iter().cloned().into());
}
if let Some(bcc) = identity.bcc() {
create_request.bcc(bcc.iter().cloned().into());
}
if let Some(html_signature) = identity.html_signature() {
create_request.html_signature(html_signature);
}
if let Some(text_signature) = identity.text_signature() {
create_request.text_signature(text_signature);
}
} else {
eprintln!("Skipping identity with no id, name, and/or email.");
continue;
}
}
match request.send_set_identity().await {
Ok(mut response) => {
let mut total_imported = 0;
for id in create_ids {
if let Err(err) = response.created(&id) {
eprintln!("Failed to import identity {id}: {err}");
} else {
total_imported += 1;
}
}
eprintln!("Successfully imported {} identities.", total_imported);
}
Err(err) => {
eprintln!("Failed to import identities: {err}");
}
}
}
async fn import_vacation_responses(client: &Client, path: &Path) {
// Deserialize mailboxes
let vacation_responses =
read_json::<jmap_client::vacation_response::VacationResponse>(path, "vacation.json").await;
if vacation_responses.is_empty() {
return;
}
let existing_vacation_responses = fetch_vacation_responses(client).await;
if !existing_vacation_responses.is_empty() {
return;
}
let vacation_response = vacation_responses.into_iter().next().unwrap();
let mut request = client.build();
let set_request = request.set_vacation_response().create();
if vacation_response.is_enabled() {
set_request.is_enabled(true);
}
if let Some(from_date) = vacation_response.from_date() {
set_request.from_date(from_date.into());
}
if let Some(to_date) = vacation_response.to_date() {
set_request.to_date(to_date.into());
}
if let Some(subject) = vacation_response.subject() {
set_request.subject(subject.into());
}
if let Some(text_body) = vacation_response.text_body() {
set_request.text_body(text_body.into());
}
if let Some(html_body) = vacation_response.html_body() {
set_request.html_body(html_body.into());
}
let create_id = set_request.create_id().unwrap();
match request.send_set_vacation_response().await {
Ok(mut response) => {
if let Err(err) = response.created(&create_id) {
eprintln!("Failed to import vacation response: {err}");
} else {
eprintln!("Successfully imported 1 vacation response.");
}
}
Err(err) => {
eprintln!("Failed to import vacation response: {err}");
}
}
}
fn build_mailbox_tree(
mailboxes: &[jmap_client::mailbox::Mailbox],
) -> HashMap<Vec<&str>, &jmap_client::mailbox::Mailbox> {
let mut path = Vec::new();
let mut parent_id = None;
let mut mailboxes_iter = mailboxes.iter();
let mut stack = Vec::new();
let mut results = HashMap::with_capacity(mailboxes.len());
let parents = mailboxes
.iter()
.map(|m| m.parent_id())
.collect::<HashSet<_>>();
'outer: loop {
while let Some(mailbox) = mailboxes_iter.next() {
if parent_id == mailbox.parent_id() {
if parents.contains(&mailbox.id()) {
stack.push((path.clone(), parent_id, mailboxes_iter));
parent_id = mailbox.id();
path.push(mailbox.name().unwrap_result("obtain mailbox name"));
mailboxes_iter = mailboxes.iter();
continue 'outer;
} else {
let mut path = path.clone();
path.push(mailbox.name().unwrap_result("obtain mailbox name"));
results.insert(path, mailbox);
}
}
}
if let Some((prev_path, prev_parent_id, prev_iter)) = stack.pop() {
parent_id = prev_parent_id;
path = prev_path;
mailboxes_iter = prev_iter;
} else {
break;
}
}
results
}
async fn read_json<T: DeserializeOwned>(path: &Path, filename: &str) -> Vec<T> {
let mut path = PathBuf::from(path);
path.push(&filename);
if path.exists() {
let mut file = File::open(path).await.unwrap_result("open file");
let mut contents = String::new();
file.read_to_string(&mut contents)
.await
.unwrap_result("read file");
serde_json::from_str(&contents).unwrap_result("parse JSON")
} else {
Vec::new()
}
}

View file

@ -23,9 +23,16 @@
use std::{collections::HashMap, fmt::Display, io::Read};
use jmap_client::principal::Property;
use jmap_client::{
client::Client,
principal::{
query::{self},
Property,
},
};
pub mod cli;
pub mod export;
pub mod import;
pub mod queue;
pub mod report;
@ -104,37 +111,76 @@ pub fn read_file(path: &str) -> Vec<u8> {
}
}
pub fn get(url: &str) -> HashMap<String, serde_json::Value> {
pub async fn get(url: &str) -> HashMap<String, serde_json::Value> {
serde_json::from_slice(
&reqwest::blocking::Client::builder()
.danger_accept_invalid_certs(true)
&reqwest::Client::builder()
.danger_accept_invalid_certs(is_localhost(url))
.build()
.unwrap_or_default()
.get(url)
.send()
.await
.unwrap_result("send OAuth GET request")
.bytes()
.await
.unwrap_result("fetch bytes"),
)
.unwrap_result("deserialize OAuth GET response")
}
pub fn post(url: &str, params: &HashMap<String, String>) -> HashMap<String, serde_json::Value> {
pub async fn post(
url: &str,
params: &HashMap<String, String>,
) -> HashMap<String, serde_json::Value> {
serde_json::from_slice(
&reqwest::blocking::Client::builder()
.danger_accept_invalid_certs(true)
&reqwest::Client::builder()
.danger_accept_invalid_certs(is_localhost(url))
.build()
.unwrap_or_default()
.post(url)
.form(params)
.send()
.await
.unwrap_result("send OAuth POST request")
.bytes()
.await
.unwrap_result("fetch bytes"),
)
.unwrap_result("deserialize OAuth POST response")
}
pub async fn name_to_id(client: &Client, name: &str) -> String {
let filter = if name.contains('@') {
query::Filter::email(name)
} else {
query::Filter::name(name)
};
let mut response = client
.principal_query(filter.into(), None::<Vec<_>>)
.await
.unwrap_result("query principals");
match response.ids().len() {
1 => response.take_ids().pop().unwrap(),
0 => {
eprintln!("Error: No principal found with name '{}'.", name);
std::process::exit(1);
}
_ => {
eprintln!("Error: Multiple principals found with name '{}'.", name);
std::process::exit(1);
}
}
}
pub fn is_localhost(url: &str) -> bool {
url.split_once("://")
.map(|(_, url)| url.split_once('/').map_or(url, |(host, _)| host))
.map_or(false, |host| {
let host = host.rsplit_once(':').map_or(host, |(host, _)| host);
host == "localhost" || host == "127.0.0.1" || host == "[::1]"
})
}
pub trait OAuthResponse {
fn property(&self, name: &str) -> &str;
}

View file

@ -21,7 +21,7 @@
* for more details.
*/
use super::{cli::QueueCommands, UnwrapResult};
use super::{cli::QueueCommands, is_localhost, UnwrapResult};
use console::Term;
use human_size::{Byte, SpecificSize};
use jmap_client::client::Credentials;
@ -76,7 +76,7 @@ pub enum Status {
PermanentFailure(String),
}
pub fn cmd_queue(url: &str, credentials: Credentials, command: QueueCommands) {
pub async fn cmd_queue(url: &str, credentials: Credentials, command: QueueCommands) {
match command {
QueueCommands::List {
sender,
@ -86,7 +86,7 @@ pub fn cmd_queue(url: &str, credentials: Credentials, command: QueueCommands) {
page_size,
} => {
let stdout = Term::buffered_stdout();
let ids = query_messages(url, &credentials, &sender, &rcpt, &before, &after);
let ids = query_messages(url, &credentials, &sender, &rcpt, &before, &after).await;
let ids_len = ids.len();
let page_size = page_size.map(|p| std::cmp::max(p, 1)).unwrap_or(20);
let pages_total = (ids_len as f64 / page_size as f64).ceil() as usize;
@ -103,6 +103,7 @@ pub fn cmd_queue(url: &str, credentials: Credentials, command: QueueCommands) {
&build_query(url, "/queue/status?ids=", chunk),
&credentials,
)
.await
.into_iter()
.zip(chunk)
{
@ -174,6 +175,7 @@ pub fn cmd_queue(url: &str, credentials: Credentials, command: QueueCommands) {
&build_query(url, "/queue/status?ids=", &parse_ids(&ids)),
&credentials,
)
.await
.into_iter()
.zip(&ids)
{
@ -294,7 +296,7 @@ pub fn cmd_queue(url: &str, credentials: Credentials, command: QueueCommands) {
let (parsed_ids, ids) = if ids.is_empty() {
if sender.is_some() || domain.is_some() || before.is_some() || after.is_some() {
let parsed_ids =
query_messages(url, &credentials, &sender, &domain, &before, &after);
query_messages(url, &credentials, &sender, &domain, &before, &after).await;
let ids = parsed_ids.iter().map(|id| format!("{id:X}")).collect();
(parsed_ids, ids)
} else {
@ -322,6 +324,7 @@ pub fn cmd_queue(url: &str, credentials: Credentials, command: QueueCommands) {
let mut success_count = 0;
let mut failed_list = vec![];
for (success, id) in smtp_manage_request::<Vec<bool>>(&query.finish(), &credentials)
.await
.into_iter()
.zip(ids)
{
@ -347,7 +350,7 @@ pub fn cmd_queue(url: &str, credentials: Credentials, command: QueueCommands) {
let (parsed_ids, ids) = if ids.is_empty() {
if sender.is_some() || rcpt.is_some() || before.is_some() || after.is_some() {
let parsed_ids =
query_messages(url, &credentials, &sender, &rcpt, &before, &after);
query_messages(url, &credentials, &sender, &rcpt, &before, &after).await;
let ids = parsed_ids.iter().map(|id| format!("{id:X}")).collect();
(parsed_ids, ids)
} else {
@ -372,6 +375,7 @@ pub fn cmd_queue(url: &str, credentials: Credentials, command: QueueCommands) {
let mut success_count = 0;
let mut failed_list = vec![];
for (success, id) in smtp_manage_request::<Vec<bool>>(&query.finish(), &credentials)
.await
.into_iter()
.zip(ids)
{
@ -400,10 +404,10 @@ pub enum Response<T> {
Error { error: String, details: String },
}
pub fn smtp_manage_request<T: DeserializeOwned>(url: &str, credentials: &Credentials) -> T {
pub async fn smtp_manage_request<T: DeserializeOwned>(url: &str, credentials: &Credentials) -> T {
match serde_json::from_slice::<Response<T>>(
&reqwest::blocking::Client::builder()
.danger_accept_invalid_certs(url.starts_with("https://127.0.0.1"))
&reqwest::Client::builder()
.danger_accept_invalid_certs(is_localhost(url))
.build()
.unwrap_or_default()
.get(url)
@ -415,8 +419,10 @@ pub fn smtp_manage_request<T: DeserializeOwned>(url: &str, credentials: &Credent
},
)
.send()
.await
.unwrap_result("send GET request")
.bytes()
.await
.unwrap_result("fetch bytes"),
)
.unwrap_result("deserialize response")
@ -429,7 +435,7 @@ pub fn smtp_manage_request<T: DeserializeOwned>(url: &str, credentials: &Credent
}
}
fn query_messages(
async fn query_messages(
url: &str,
credentials: &Credentials,
from: &Option<String>,
@ -452,7 +458,7 @@ fn query_messages(
query.append_pair("after", &after.to_rfc3339());
}
smtp_manage_request::<Vec<u64>>(&query.finish(), credentials)
smtp_manage_request::<Vec<u64>>(&query.finish(), credentials).await
}
fn deserialize_maybe_datetime<'de, D>(deserializer: D) -> Result<Option<DateTime>, D::Error>

View file

@ -42,7 +42,7 @@ pub struct Report {
pub size: usize,
}
pub fn cmd_report(url: &str, credentials: Credentials, command: ReportCommands) {
pub async fn cmd_report(url: &str, credentials: Credentials, command: ReportCommands) {
match command {
ReportCommands::List {
domain,
@ -59,7 +59,7 @@ pub fn cmd_report(url: &str, credentials: Credentials, command: ReportCommands)
query.append_pair("type", format.id());
}
let ids = smtp_manage_request::<Vec<String>>(&query.finish(), &credentials);
let ids = smtp_manage_request::<Vec<String>>(&query.finish(), &credentials).await;
let ids_len = ids.len();
let page_size = page_size.map(|p| std::cmp::max(p, 1)).unwrap_or(20);
let pages_total = (ids_len as f64 / page_size as f64).ceil() as usize;
@ -76,6 +76,7 @@ pub fn cmd_report(url: &str, credentials: Credentials, command: ReportCommands)
&format!("{url}/report/status?ids={}", chunk.join(",")),
&credentials,
)
.await
.into_iter()
.zip(chunk)
{
@ -112,6 +113,7 @@ pub fn cmd_report(url: &str, credentials: Credentials, command: ReportCommands)
&format!("{url}/report/status?ids={}", ids.join(",")),
&credentials,
)
.await
.into_iter()
.zip(&ids)
{
@ -165,6 +167,7 @@ pub fn cmd_report(url: &str, credentials: Credentials, command: ReportCommands)
&format!("{url}/report/cancel?ids={}", ids.join(",")),
&credentials,
)
.await
.into_iter()
.zip(ids)
{

View file

@ -13,7 +13,7 @@ mail-builder = { git = "https://github.com/stalwartlabs/mail-builder", features
tokio = { version = "1.23", features = ["net"] }
tokio-rustls = { version = "0.24.0"}
rustls = "0.21.0"
sqlx = { version = "0.7.0-alpha.3", features = [ "runtime-tokio-rustls", "postgres", "mysql", "sqlite" ] }
sqlx = { version = "0.7", features = [ "runtime-tokio-rustls", "postgres", "mysql", "sqlite" ] }
ldap3 = { version = "0.11.1", default-features = false, features = ["tls-rustls"] }
bb8 = "0.8.1"
parking_lot = "0.12"

View file

@ -132,6 +132,18 @@ impl Debug for Lookup {
}
}
impl Type {
pub fn to_jmap(&self) -> &'static str {
match self {
Self::Individual => "individual",
Self::Group => "group",
Self::Resource => "resource",
Self::Location => "location",
Self::Other => "other",
}
}
}
#[derive(Debug, Default)]
struct DirectoryOptions {
catch_all: bool,

View file

@ -51,6 +51,7 @@ pub enum RequestArguments {
PushSubscription,
SieveScript,
VacationResponse,
Principal,
}
#[derive(Debug, Clone, serde::Serialize)]
@ -83,6 +84,7 @@ impl JsonObjectParser for GetRequest<RequestArguments> {
MethodObject::PushSubscription => RequestArguments::PushSubscription,
MethodObject::SieveScript => RequestArguments::SieveScript,
MethodObject::VacationResponse => RequestArguments::VacationResponse,
MethodObject::Principal => RequestArguments::Principal,
_ => {
return Err(Error::Method(MethodError::UnknownMethod(format!(
"{}/get",

View file

@ -158,6 +158,7 @@ pub enum RequestArguments {
Mailbox(mailbox::QueryArguments),
EmailSubmission,
SieveScript,
Principal,
}
impl JsonObjectParser for QueryRequest<RequestArguments> {
@ -171,6 +172,7 @@ impl JsonObjectParser for QueryRequest<RequestArguments> {
MethodObject::Mailbox => RequestArguments::Mailbox(Default::default()),
MethodObject::EmailSubmission => RequestArguments::EmailSubmission,
MethodObject::SieveScript => RequestArguments::SieveScript,
MethodObject::Principal => RequestArguments::Principal,
_ => {
return Err(Error::Method(MethodError::UnknownMethod(format!(
"{}/query",

View file

@ -27,7 +27,7 @@ aes-gcm-siv = "0.11.1"
bincode = "1.3.3"
form-data = { version = "0.4.2", features = ["sync"], default-features = false }
mime = "0.3.17"
sqlx = { version = "0.7.0-alpha.3", features = [ "runtime-tokio-rustls", "postgres", "mysql", "sqlite" ] }
sqlx = { version = "0.7", features = [ "runtime-tokio-rustls", "postgres", "mysql", "sqlite" ] }
futures-util = "0.3.28"
async-stream = "0.3.5"
base64 = "0.21"

View file

@ -137,10 +137,13 @@ impl crate::Config {
web_socket_timeout: settings.property_or_static("jmap.web-socket.timeout", "10m")?,
web_socket_heartbeat: settings.property_or_static("jmap.web-socket.heartbeat", "1m")?,
push_max_total: settings.property_or_static("jmap.push.max-total", "100")?,
superusers_group_name: settings
.value("jmap.superusers-group")
principal_superusers: settings
.value("jmap.principal.superusers")
.unwrap_or("superusers")
.to_string(),
principal_allow_lookups: settings
.property("jmap.principal.allow-lookups")?
.unwrap_or(true),
};
config.add_capabilites(settings);
Ok(config)

View file

@ -168,6 +168,15 @@ impl JMAP {
self.vacation_response_get(req).await?.into()
}
get::RequestArguments::Principal => {
if self.config.principal_allow_lookups || access_token.is_super_user() {
self.principal_get(req).await?.into()
} else {
return Err(MethodError::Forbidden(
"Principal lookups are disabled".to_string(),
));
}
}
},
RequestMethod::Query(mut req) => match req.take_arguments() {
query::RequestArguments::Email(arguments) => {
@ -194,6 +203,15 @@ impl JMAP {
self.sieve_script_query(req).await?.into()
}
query::RequestArguments::Principal => {
if self.config.principal_allow_lookups || access_token.is_super_user() {
self.principal_query(req).await?.into()
} else {
return Err(MethodError::Forbidden(
"Principal lookups are disabled".to_string(),
));
}
}
},
RequestMethod::Set(mut req) => match req.take_arguments() {
set::RequestArguments::Email => {

View file

@ -237,7 +237,7 @@ impl JMAP {
pub async fn map_member_of(&self, names: Vec<String>) -> Result<Vec<u32>, MethodError> {
let mut ids = Vec::with_capacity(names.len());
for name in names {
if !name.eq_ignore_ascii_case(&self.config.superusers_group_name) {
if !name.eq_ignore_ascii_case(&self.config.principal_superusers) {
ids.push(self.get_account_id(&name).await?);
} else {
ids.push(SUPERUSER_ID);

View file

@ -69,6 +69,7 @@ pub mod changes;
pub mod email;
pub mod identity;
pub mod mailbox;
pub mod principal;
pub mod push;
pub mod services;
pub mod sieve;
@ -150,7 +151,9 @@ pub struct Config {
pub oauth_expiry_refresh_token_renew: u64,
pub oauth_max_auth_attempts: u32,
pub superusers_group_name: String,
pub principal_superusers: String,
pub principal_allow_lookups: bool,
pub capabilities: BaseCapabilities,
}

View file

@ -0,0 +1,120 @@
/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of the Stalwart JMAP Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use jmap_proto::{
error::method::MethodError,
method::get::{GetRequest, GetResponse, RequestArguments},
object::Object,
types::{collection::Collection, property::Property, state::State, value::Value},
};
use crate::JMAP;
impl JMAP {
pub async fn principal_get(
&self,
mut request: GetRequest<RequestArguments>,
) -> Result<GetResponse, MethodError> {
let ids = request.unwrap_ids(self.config.get_max_objects)?;
let properties = request.unwrap_properties(&[
Property::Id,
Property::Type,
Property::Name,
Property::Description,
Property::Email,
//Property::Timezone,
//Property::Capabilities,
]);
let email_submission_ids = self
.get_document_ids(u32::MAX, Collection::EmailSubmission)
.await?
.unwrap_or_default();
let ids = if let Some(ids) = ids {
ids
} else {
email_submission_ids
.iter()
.take(self.config.get_max_objects)
.map(Into::into)
.collect::<Vec<_>>()
};
let mut response = GetResponse {
account_id: request.account_id.into(),
state: State::Initial.into(),
list: Vec::with_capacity(ids.len()),
not_found: vec![],
};
for id in ids {
// Obtain the principal name
let name = if let Some(name) = self.get_account_name(id.document_id()).await? {
name
} else {
response.not_found.push(id);
continue;
};
// Obtain the principal
let principal = if let Some(principal) = self
.directory
.principal(&name)
.await
.map_err(|_| MethodError::ServerPartialFail)?
{
principal
} else {
response.not_found.push(id);
continue;
};
let mut result = Object::with_capacity(properties.len());
for property in &properties {
let value = match property {
Property::Id => Value::Id(id),
Property::Type => Value::Text(principal.typ.to_jmap().to_string()),
Property::Name => Value::Text(principal.name.clone()),
Property::Description => principal
.description
.clone()
.map(Value::Text)
.unwrap_or(Value::Null),
Property::Email => self
.directory
.emails_by_name(&name)
.await
.map_err(|_| MethodError::ServerPartialFail)?
.into_iter()
.next()
.map(|email| Value::Text(email))
.unwrap_or(Value::Null),
_ => Value::Null,
};
result.append(property.clone(), value);
}
response.list.push(result);
}
Ok(response)
}
}

View file

@ -0,0 +1,2 @@
pub mod get;
pub mod query;

View file

@ -0,0 +1,104 @@
/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of the Stalwart JMAP Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use jmap_proto::{
error::method::MethodError,
method::query::{Filter, QueryRequest, QueryResponse, RequestArguments},
types::collection::Collection,
};
use store::{query::ResultSet, roaring::RoaringBitmap};
use crate::JMAP;
impl JMAP {
pub async fn principal_query(
&self,
mut request: QueryRequest<RequestArguments>,
) -> Result<QueryResponse, MethodError> {
let account_id = request.account_id.document_id();
let mut result_set = ResultSet {
account_id,
collection: Collection::Principal.into(),
results: RoaringBitmap::new(),
};
let mut is_set = true;
for cond in std::mem::take(&mut request.filter) {
match cond {
Filter::Name(name) => {
if let Some(principal) = self
.directory
.principal(&name)
.await
.map_err(|_| MethodError::ServerPartialFail)?
{
let account_id = self.get_account_id(&principal.name).await?;
if is_set {
result_set.results =
RoaringBitmap::from_sorted_iter([account_id]).unwrap();
} else if result_set.results.contains(account_id) {
result_set.results.remove(account_id);
} else {
result_set.results = RoaringBitmap::new();
}
} else {
}
is_set = false;
}
Filter::Email(email) => {
let mut ids = RoaringBitmap::new();
for name in self
.directory
.names_by_email(&email)
.await
.map_err(|_| MethodError::ServerPartialFail)?
{
ids.insert(self.get_account_id(&name).await?);
}
if is_set {
result_set.results = ids;
} else {
result_set.results &= ids;
}
}
Filter::Type(_) => {}
other => return Err(MethodError::UnsupportedFilter(other.to_string())),
}
}
if is_set {
result_set.results = self
.get_document_ids(u32::MAX, Collection::Principal)
.await?
.unwrap_or_default();
}
let (response, paginate) = self.build_query_response(&result_set, &request).await?;
if let Some(paginate) = paginate {
self.sort(result_set, Vec::new(), paginate, response).await
} else {
Ok(response)
}
}
}

View file

@ -53,6 +53,9 @@ impl JMAP {
Filter::IsActive(is_active) => {
filters.push(query::Filter::eq(Property::IsActive, is_active as u32))
}
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
filters.push(cond.into());
}
other => return Err(MethodError::UnsupportedFilter(other.to_string())),
}
}

View file

@ -74,6 +74,9 @@ impl JMAP {
Property::SendAt,
after.timestamp() as u64,
)),
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
filters.push(cond.into());
}
other => return Err(MethodError::UnsupportedFilter(other.to_string())),
}
}

View file

@ -40,7 +40,7 @@ blake3 = "1.3"
lru-cache = "0.1.2"
rand = "0.8.5"
x509-parser = "0.15.0"
sqlx = { version = "0.7.0-alpha.3", features = [ "runtime-tokio-rustls", "postgres", "mysql", "sqlite" ] }
sqlx = { version = "0.7", features = [ "runtime-tokio-rustls", "postgres", "mysql", "sqlite" ] }
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots", "blocking"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View file

@ -46,7 +46,7 @@ base64 = "0.21"
dashmap = "5.4"
ahash = { version = "0.8" }
serial_test = "2.0.0"
sqlx = { version = "0.7.0-alpha.3", features = [ "runtime-tokio-rustls", "postgres", "mysql", "sqlite" ] }
sqlx = { version = "0.7", features = [ "runtime-tokio-rustls", "postgres", "mysql", "sqlite" ] }
num_cpus = "1.15.0"
async-trait = "0.1.68"
chrono = "0.4"