mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-09-20 07:16:18 +08:00
Principal querying support + Draft import/export CLI implementation
This commit is contained in:
parent
1cb539ce52
commit
1f4204c6bf
327
Cargo.lock
generated
327
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"), ¶ms);
|
||||
let response = post(metadata.property("device_authorization_endpoint"), ¶ms).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, ¶ms);
|
||||
let mut response = post(token_endpoint, ¶ms).await;
|
||||
if let Some(serde_json::Value::String(access_token)) = response.remove("access_token") {
|
||||
Credentials::Bearer(access_token)
|
||||
} else {
|
||||
|
|
|
@ -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)]
|
||||
|
|
406
crates/cli/src/modules/export.rs
Normal file
406
crates/cli/src/modules/export.rs
Normal 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
|
||||
}
|
|
@ -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,10 +145,7 @@ 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([
|
||||
request.get_mailbox().properties([
|
||||
mailbox::Property::Name,
|
||||
mailbox::Property::ParentId,
|
||||
mailbox::Property::Role,
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
120
crates/jmap/src/principal/get.rs
Normal file
120
crates/jmap/src/principal/get.rs
Normal 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)
|
||||
}
|
||||
}
|
2
crates/jmap/src/principal/mod.rs
Normal file
2
crates/jmap/src/principal/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod get;
|
||||
pub mod query;
|
104
crates/jmap/src/principal/query.rs
Normal file
104
crates/jmap/src/principal/query.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue