Merge branch 'main' of github.com:stalwartlabs/vandelay
This commit is contained in:
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
|
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## [1.0.11] - 2026-09-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Replaced `base64` with `encodify`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- IMAP: import failed after authenticating to Dovecot when a capability contained `:`, such as `IMAPSIEVE=sieve://...` (fixes #43)
|
||||||
|
|
||||||
## [1.0.10] - 2026-08-27
|
## [1.0.10] - 2026-08-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Generated
+180
-170
@@ -157,7 +157,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.4",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -174,9 +174,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-rs"
|
name = "aws-lc-rs"
|
||||||
version = "1.18.0"
|
version = "1.18.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e"
|
checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-sys",
|
"aws-lc-sys",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -184,9 +184,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-sys"
|
name = "aws-lc-sys"
|
||||||
version = "0.44.0"
|
version = "0.45.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483"
|
checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cmake",
|
"cmake",
|
||||||
@@ -258,9 +258,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.1"
|
version = "2.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blake3"
|
name = "blake3"
|
||||||
@@ -283,7 +283,7 @@ checksum = "dbe8358268799ebb3e4df23cb9d47f4c72bbc4f5247e2fa6a1bf7b6c0baea220"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bitflags 2.13.1",
|
"bitflags 2.13.2",
|
||||||
"bollard-buildkit-proto",
|
"bollard-buildkit-proto",
|
||||||
"bollard-stubs",
|
"bollard-stubs",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -301,7 +301,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"num",
|
"num",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rand 0.10.2",
|
"rand 0.10.3",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-native-certs",
|
"rustls-native-certs",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@@ -371,9 +371,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "calcard"
|
name = "calcard"
|
||||||
version = "0.3.13"
|
version = "0.3.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75b779382e675380a1ff8a4873acee5ba60158be7a00458fb98e6b85aad1f2ee"
|
checksum = "c601473ec15a875626bce73db1a1f0fc81e9c949ca25f1463b714947c0a70a1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -389,9 +389,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.4.4"
|
version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
|
checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
@@ -401,15 +401,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chacha20"
|
name = "chacha20"
|
||||||
version = "0.10.1"
|
version = "0.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
@@ -442,9 +442,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.6"
|
version = "4.6.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -452,9 +452,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.6.6"
|
version = "4.6.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -464,21 +464,21 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.6.4"
|
version = "4.6.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.4",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_lex"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cmake"
|
name = "cmake"
|
||||||
@@ -540,7 +540,7 @@ dependencies = [
|
|||||||
"cookie",
|
"cookie",
|
||||||
"document-features",
|
"document-features",
|
||||||
"idna",
|
"idna",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.2",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
@@ -567,42 +567,42 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.1"
|
version = "1.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550"
|
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-channel"
|
name = "crossbeam-channel"
|
||||||
version = "0.5.16"
|
version = "0.5.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
|
checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.22"
|
version = "0.8.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.23.0"
|
version = "0.24.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"darling_macro",
|
"darling_macro",
|
||||||
@@ -610,26 +610,26 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_core"
|
name = "darling_core"
|
||||||
version = "0.23.0"
|
version = "0.24.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
|
checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ident_case",
|
"ident_case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"strsim",
|
||||||
"syn 2.0.119",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.23.0"
|
version = "0.24.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -680,7 +680,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.4",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -721,6 +721,16 @@ version = "1.18.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encodify"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fda1e5c788c2b59096e152ad1a76ce72a2ab80c75985eb94ca82de0b6555db7"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"simdutf8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -772,24 +782,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4"
|
checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rand 0.10.2",
|
"rand 0.10.3",
|
||||||
"web-time",
|
"web-time",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.11"
|
version = "0.1.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
|
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.9"
|
version = "1.1.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
|
"zlib-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -875,7 +886,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.4",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -954,7 +965,7 @@ dependencies = [
|
|||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"http",
|
"http",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.2",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -991,7 +1002,7 @@ version = "0.2.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd1246c0e5493286aeb2dde35b1f4eb9c4ce00e628641210a5e553fc001a1f26"
|
checksum = "dd1246c0e5493286aeb2dde35b1f4eb9c4ce00e628641210a5e553fc001a1f26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.2",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
@@ -999,9 +1010,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashlink"
|
name = "hashlink"
|
||||||
version = "0.12.1"
|
version = "0.12.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248"
|
checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.17.1",
|
"hashbrown 0.17.1",
|
||||||
]
|
]
|
||||||
@@ -1014,9 +1025,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.5.2"
|
version = "0.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex"
|
name = "hex"
|
||||||
@@ -1080,9 +1091,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.11.0"
|
version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
|
checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1116,9 +1127,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-rustls"
|
name = "hyper-rustls"
|
||||||
version = "0.27.9"
|
version = "0.27.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"hyper",
|
"hyper",
|
||||||
@@ -1144,15 +1155,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.20"
|
version = "0.1.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
|
"httparse",
|
||||||
"hyper",
|
"hyper",
|
||||||
"libc",
|
"libc",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -1324,9 +1336,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.0"
|
version = "2.14.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.17.1",
|
"hashbrown 0.17.1",
|
||||||
@@ -1357,9 +1369,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff"
|
name = "jiff"
|
||||||
version = "0.2.35"
|
version = "0.2.37"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
|
checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt",
|
"defmt",
|
||||||
"jiff-core",
|
"jiff-core",
|
||||||
@@ -1374,18 +1386,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff-core"
|
name = "jiff-core"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
|
checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt",
|
"defmt",
|
||||||
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff-static"
|
name = "jiff-static"
|
||||||
version = "0.2.35"
|
version = "0.2.37"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
|
checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jiff-core",
|
"jiff-core",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -1480,9 +1493,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.104"
|
version = "0.3.106"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
|
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -1541,15 +1554,15 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mail-builder"
|
name = "mail-builder"
|
||||||
version = "0.5.0"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c942e8a4b83f9351236c1e531ea9fa0237913d63c7fc36818430e0128a1ddf3"
|
checksum = "ccbc27cb02a58a311a404769a22f5abf63fe3173b290d80be7c534f9c7023151"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mail-parser"
|
name = "mail-parser"
|
||||||
version = "0.11.8"
|
version = "0.11.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b3a9080c1fb8190e232df37a10aa1b3d6b08be084e537069913f025e0ce86c5"
|
checksum = "4ec00bda90c6e645a54506c630c2820cd6b1890cfd2b0a169b50f74b2b8c7c86"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashify",
|
"hashify",
|
||||||
]
|
]
|
||||||
@@ -1574,9 +1587,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler2",
|
"adler2",
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
@@ -1584,9 +1597,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.2"
|
version = "1.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
|
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
@@ -1836,9 +1849,9 @@ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic-util"
|
name = "portable-atomic-util"
|
||||||
version = "0.2.7"
|
version = "0.2.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
|
checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
]
|
]
|
||||||
@@ -1910,9 +1923,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "psl"
|
name = "psl"
|
||||||
version = "2.1.226"
|
version = "2.1.238"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7bc88482eea924ca3a2f56a547454169af58deef35567965eb4fc2392a834841"
|
checksum = "2e6aa2cd4e8e062e78ac5d7df6206e2f8e39b624f658efd6f34f9aa1928ac5da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"psl-types",
|
"psl-types",
|
||||||
]
|
]
|
||||||
@@ -1925,9 +1938,9 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.41.0"
|
version = "0.42.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
|
checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
@@ -1965,9 +1978,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.10.2"
|
version = "0.10.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chacha20",
|
"chacha20",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
@@ -2005,7 +2018,7 @@ version = "0.5.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.1",
|
"bitflags 2.13.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2025,7 +2038,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.4",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2087,7 +2100,7 @@ version = "0.40.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3"
|
checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.1",
|
"bitflags 2.13.2",
|
||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
"fallible-streaming-iterator",
|
"fallible-streaming-iterator",
|
||||||
"hashlink",
|
"hashlink",
|
||||||
@@ -2113,11 +2126,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.4"
|
version = "1.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.1",
|
"bitflags 2.13.2",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
@@ -2126,9 +2139,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.43"
|
version = "0.23.45"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"log",
|
"log",
|
||||||
@@ -2163,9 +2176,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-platform-verifier"
|
name = "rustls-platform-verifier"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
@@ -2184,9 +2197,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-platform-verifier-android"
|
name = "rustls-platform-verifier-android"
|
||||||
version = "0.1.1"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
@@ -2266,7 +2279,7 @@ version = "3.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.1",
|
"bitflags 2.13.2",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -2316,7 +2329,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.4",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2340,7 +2353,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.4",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2357,16 +2370,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "3.22.0"
|
version = "3.24.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a"
|
checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.23.1",
|
||||||
"bs58",
|
"bs58",
|
||||||
"chrono",
|
"chrono",
|
||||||
"hex",
|
"hex",
|
||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.2",
|
||||||
"jiff",
|
"jiff",
|
||||||
"schemars 0.9.0",
|
"schemars 0.9.0",
|
||||||
"schemars 1.2.2",
|
"schemars 1.2.2",
|
||||||
@@ -2378,14 +2391,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with_macros"
|
name = "serde_with_macros"
|
||||||
version = "3.22.0"
|
version = "3.24.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46"
|
checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2440,9 +2453,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "1.0.3"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
@@ -2452,9 +2465,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.15.2"
|
version = "1.16.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
@@ -2532,9 +2545,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "3.0.4"
|
version = "3.0.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
|
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2549,13 +2562,13 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "synstructure"
|
name = "synstructure"
|
||||||
version = "0.13.2"
|
version = "0.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2604,22 +2617,22 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.20"
|
version = "2.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.20"
|
version = "2.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.4",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2664,18 +2677,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.12.0"
|
version = "1.13.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
|
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
|
||||||
dependencies = [
|
|
||||||
"tinyvec_macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tinyvec_macros"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
@@ -2702,14 +2706,14 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.4",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-rustls"
|
name = "tokio-rustls"
|
||||||
version = "0.26.4"
|
version = "0.26.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -2788,7 +2792,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.2",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"slab",
|
"slab",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
@@ -2850,9 +2854,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
@@ -2862,9 +2866,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ureq"
|
name = "ureq"
|
||||||
version = "3.4.0"
|
version = "3.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d"
|
checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.23.1",
|
"base64 0.23.1",
|
||||||
"cookie_store",
|
"cookie_store",
|
||||||
@@ -2882,9 +2886,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ureq-proto"
|
name = "ureq-proto"
|
||||||
version = "0.6.1"
|
version = "0.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613"
|
checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.23.1",
|
"base64 0.23.1",
|
||||||
"http",
|
"http",
|
||||||
@@ -2925,26 +2929,26 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.26.0"
|
version = "1.26.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
|
checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sha1_smol",
|
"sha1_smol",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vandelay"
|
name = "vandelay"
|
||||||
version = "1.0.10"
|
version = "1.0.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.23.1",
|
|
||||||
"blake3",
|
"blake3",
|
||||||
"calcard",
|
"calcard",
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
"clap",
|
"clap",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
|
"encodify",
|
||||||
"flate2",
|
"flate2",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.2",
|
||||||
"mail-parser",
|
"mail-parser",
|
||||||
"mockito",
|
"mockito",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
@@ -3012,9 +3016,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.127"
|
version = "0.2.129"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
|
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -3025,9 +3029,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.127"
|
version = "0.2.129"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
|
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -3035,22 +3039,22 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.127"
|
version = "0.2.129"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
|
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 3.0.6",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.127"
|
version = "0.2.129"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
|
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -3281,30 +3285,30 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke-derive"
|
name = "yoke-derive"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 3.0.6",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.56"
|
version = "0.8.59"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
|
checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.56"
|
version = "0.8.59"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
|
checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3322,13 +3326,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom-derive"
|
name = "zerofrom-derive"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 3.0.6",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3368,9 +3372,15 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.4",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zlib-rs"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
|
|||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "vandelay"
|
name = "vandelay"
|
||||||
description = "JMAP account migration utility"
|
description = "JMAP account migration utility"
|
||||||
version = "1.0.10"
|
version = "1.0.11"
|
||||||
authors = ["Stalwart Labs LLC <[email protected]>"]
|
authors = ["Stalwart Labs LLC <[email protected]>"]
|
||||||
license = "Apache-2.0 OR MIT"
|
license = "Apache-2.0 OR MIT"
|
||||||
repository = "https://github.com/stalwartlabs/vandelay"
|
repository = "https://github.com/stalwartlabs/vandelay"
|
||||||
@@ -26,7 +26,7 @@ bundled-sqlite = ["rusqlite/bundled"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
indexmap = { version = "2.14.0", features = ["serde"] }
|
indexmap = { version = "2.14", features = ["serde"] }
|
||||||
rusqlite = { version = "0.40", features = ["blob"] }
|
rusqlite = { version = "0.40", features = ["blob"] }
|
||||||
blake3 = "1.8.5"
|
blake3 = "1.8.5"
|
||||||
time = { version = "0.3.47", features = ["serde-well-known", "macros"] }
|
time = { version = "0.3.47", features = ["serde-well-known", "macros"] }
|
||||||
@@ -36,14 +36,14 @@ ureq = { version = "3.3.0", default-features = false, features = ["json", "gzip"
|
|||||||
clap = { version = "4.6.1", features = ["derive"] }
|
clap = { version = "4.6.1", features = ["derive"] }
|
||||||
crossbeam-channel = "0.5.15"
|
crossbeam-channel = "0.5.15"
|
||||||
num_cpus = "1.17.0"
|
num_cpus = "1.17.0"
|
||||||
base64 = "0.23"
|
encodify = "1.0"
|
||||||
mail-parser = "0.11.3"
|
mail-parser = "0.11.3"
|
||||||
calcard = "0.3"
|
calcard = "0.3"
|
||||||
rustls = { version = "0.23.40", default-features = false, features = ["aws-lc-rs", "std", "tls12", "logging"] }
|
rustls = { version = "0.23.40", default-features = false, features = ["aws-lc-rs", "std", "tls12", "logging"] }
|
||||||
rustls-platform-verifier = "0.7"
|
rustls-platform-verifier = "0.7"
|
||||||
regex = "1.13"
|
regex = "1.13"
|
||||||
flate2 = "1.1.9"
|
flate2 = "1.1.9"
|
||||||
quick-xml = "0.41"
|
quick-xml = "0.42"
|
||||||
chrono = { version = "0.4", default-features = false, features = ["std"] }
|
chrono = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
chrono-tz = { version = "0.10", default-features = false }
|
chrono-tz = { version = "0.10", default-features = false }
|
||||||
psl = "2.1.226"
|
psl = "2.1.226"
|
||||||
|
|||||||
+3
-11
@@ -4,6 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use encodify::hex::decode_pair;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
@@ -111,9 +112,9 @@ fn percent_decode(s: &str) -> String {
|
|||||||
while i < bytes.len() {
|
while i < bytes.len() {
|
||||||
if bytes[i] == b'%'
|
if bytes[i] == b'%'
|
||||||
&& i + 2 < bytes.len()
|
&& i + 2 < bytes.len()
|
||||||
&& let (Some(h), Some(l)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2]))
|
&& let Some(byte) = decode_pair(bytes[i + 1], bytes[i + 2])
|
||||||
{
|
{
|
||||||
buf.push((h << 4) | l);
|
buf.push(byte);
|
||||||
i += 3;
|
i += 3;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -126,15 +127,6 @@ fn percent_decode(s: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hex_val(b: u8) -> Option<u8> {
|
|
||||||
match b {
|
|
||||||
b'0'..=b'9' => Some(b - b'0'),
|
|
||||||
b'a'..=b'f' => Some(b - b'a' + 10),
|
|
||||||
b'A'..=b'F' => Some(b - b'A' + 10),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn percent_encode_safe(s: &str) -> String {
|
fn percent_encode_safe(s: &str) -> String {
|
||||||
let mut out = String::with_capacity(s.len());
|
let mut out = String::with_capacity(s.len());
|
||||||
for byte in s.bytes() {
|
for byte in s.bytes() {
|
||||||
|
|||||||
+72
-78
@@ -78,13 +78,13 @@ fn classify_ns(ns: &ResolveResult<'_>) -> NsKind {
|
|||||||
match ns {
|
match ns {
|
||||||
ResolveResult::Bound(prefix) => {
|
ResolveResult::Bound(prefix) => {
|
||||||
let p = prefix.as_ref();
|
let p = prefix.as_ref();
|
||||||
if p == NS_DAV.as_bytes() {
|
if p == NS_DAV {
|
||||||
NsKind::Dav
|
NsKind::Dav
|
||||||
} else if p == NS_CALDAV.as_bytes() {
|
} else if p == NS_CALDAV {
|
||||||
NsKind::Caldav
|
NsKind::Caldav
|
||||||
} else if p == NS_CARDDAV.as_bytes() {
|
} else if p == NS_CARDDAV {
|
||||||
NsKind::Carddav
|
NsKind::Carddav
|
||||||
} else if p == NS_APPLE_ICAL.as_bytes() {
|
} else if p == NS_APPLE_ICAL {
|
||||||
NsKind::Apple
|
NsKind::Apple
|
||||||
} else {
|
} else {
|
||||||
NsKind::Other
|
NsKind::Other
|
||||||
@@ -94,7 +94,7 @@ fn classify_ns(ns: &ResolveResult<'_>) -> NsKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is(ns: NsKind, local: &[u8], expected_ns: NsKind, target: &[u8]) -> bool {
|
fn is(ns: NsKind, local: &str, expected_ns: NsKind, target: &str) -> bool {
|
||||||
ns == expected_ns && local.eq_ignore_ascii_case(target)
|
ns == expected_ns && local.eq_ignore_ascii_case(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,17 +102,17 @@ fn is(ns: NsKind, local: &[u8], expected_ns: NsKind, target: &[u8]) -> bool {
|
|||||||
enum Step {
|
enum Step {
|
||||||
StartElement {
|
StartElement {
|
||||||
ns: NsKind,
|
ns: NsKind,
|
||||||
local: Vec<u8>,
|
local: String,
|
||||||
attrs: Vec<(Vec<u8>, Vec<u8>)>,
|
attrs: Vec<(String, String)>,
|
||||||
},
|
},
|
||||||
EmptyElement {
|
EmptyElement {
|
||||||
ns: NsKind,
|
ns: NsKind,
|
||||||
local: Vec<u8>,
|
local: String,
|
||||||
attrs: Vec<(Vec<u8>, Vec<u8>)>,
|
attrs: Vec<(String, String)>,
|
||||||
},
|
},
|
||||||
EndElement {
|
EndElement {
|
||||||
ns: NsKind,
|
ns: NsKind,
|
||||||
local: Vec<u8>,
|
local: String,
|
||||||
},
|
},
|
||||||
Text(String),
|
Text(String),
|
||||||
CData(String),
|
CData(String),
|
||||||
@@ -125,7 +125,7 @@ fn resolve_entity(g: &BytesRef) -> Option<char> {
|
|||||||
if let Ok(Some(c)) = g.resolve_char_ref() {
|
if let Ok(Some(c)) = g.resolve_char_ref() {
|
||||||
return Some(c);
|
return Some(c);
|
||||||
}
|
}
|
||||||
match g.decode().ok()?.as_ref() {
|
match g.as_ref() {
|
||||||
"amp" => Some('&'),
|
"amp" => Some('&'),
|
||||||
"lt" => Some('<'),
|
"lt" => Some('<'),
|
||||||
"gt" => Some('>'),
|
"gt" => Some('>'),
|
||||||
@@ -141,11 +141,12 @@ fn next_step<R: BufRead>(xml: &mut NsReader<R>, buf: &mut Vec<u8>) -> Result<Ste
|
|||||||
let ns_kind = classify_ns(&ns);
|
let ns_kind = classify_ns(&ns);
|
||||||
Ok(match ev {
|
Ok(match ev {
|
||||||
Event::Start(e) => {
|
Event::Start(e) => {
|
||||||
let local = e.local_name().as_ref().to_vec();
|
let local = e.local_name().as_ref().to_owned();
|
||||||
let mut attrs: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
|
let attrs = e
|
||||||
for a in e.attributes().flatten() {
|
.attributes()
|
||||||
attrs.push((a.key.as_ref().to_vec(), a.value.as_ref().to_vec()));
|
.flatten()
|
||||||
}
|
.map(|a| (a.key.as_ref().to_owned(), a.value.into_owned()))
|
||||||
|
.collect();
|
||||||
Step::StartElement {
|
Step::StartElement {
|
||||||
ns: ns_kind,
|
ns: ns_kind,
|
||||||
local,
|
local,
|
||||||
@@ -153,11 +154,12 @@ fn next_step<R: BufRead>(xml: &mut NsReader<R>, buf: &mut Vec<u8>) -> Result<Ste
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Empty(e) => {
|
Event::Empty(e) => {
|
||||||
let local = e.local_name().as_ref().to_vec();
|
let local = e.local_name().as_ref().to_owned();
|
||||||
let mut attrs: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
|
let attrs = e
|
||||||
for a in e.attributes().flatten() {
|
.attributes()
|
||||||
attrs.push((a.key.as_ref().to_vec(), a.value.as_ref().to_vec()));
|
.flatten()
|
||||||
}
|
.map(|a| (a.key.as_ref().to_owned(), a.value.into_owned()))
|
||||||
|
.collect();
|
||||||
Step::EmptyElement {
|
Step::EmptyElement {
|
||||||
ns: ns_kind,
|
ns: ns_kind,
|
||||||
local,
|
local,
|
||||||
@@ -166,13 +168,10 @@ fn next_step<R: BufRead>(xml: &mut NsReader<R>, buf: &mut Vec<u8>) -> Result<Ste
|
|||||||
}
|
}
|
||||||
Event::End(e) => Step::EndElement {
|
Event::End(e) => Step::EndElement {
|
||||||
ns: ns_kind,
|
ns: ns_kind,
|
||||||
local: e.local_name().as_ref().to_vec(),
|
local: e.local_name().as_ref().to_owned(),
|
||||||
},
|
},
|
||||||
Event::Text(t) => {
|
Event::Text(t) => Step::Text(t.into_inner().into_owned()),
|
||||||
let s = t.decode().map_err(|e| ParseError::Xml(e.to_string()))?;
|
Event::CData(cd) => Step::CData(cd.into_inner().into_owned()),
|
||||||
Step::Text(s.into_owned())
|
|
||||||
}
|
|
||||||
Event::CData(cd) => Step::CData(String::from_utf8_lossy(cd.as_ref()).into_owned()),
|
|
||||||
Event::GeneralRef(g) => match resolve_entity(&g) {
|
Event::GeneralRef(g) => match resolve_entity(&g) {
|
||||||
Some(c) => Step::Entity(c),
|
Some(c) => Step::Entity(c),
|
||||||
None => Step::Other,
|
None => Step::Other,
|
||||||
@@ -194,7 +193,7 @@ pub fn parse_multistatus<R: BufRead>(
|
|||||||
loop {
|
loop {
|
||||||
let step = next_step(&mut xml, &mut buf)?;
|
let step = next_step(&mut xml, &mut buf)?;
|
||||||
match step {
|
match step {
|
||||||
Step::StartElement { ns, local, .. } if is(ns, &local, NsKind::Dav, b"response") => {
|
Step::StartElement { ns, local, .. } if is(ns, &local, NsKind::Dav, "response") => {
|
||||||
if let Some(r) = parse_response(&mut xml, base_url)?
|
if let Some(r) = parse_response(&mut xml, base_url)?
|
||||||
&& seen.insert(r.href.as_str().to_owned())
|
&& seen.insert(r.href.as_str().to_owned())
|
||||||
{
|
{
|
||||||
@@ -222,15 +221,15 @@ fn parse_response<R: BufRead>(
|
|||||||
let step = next_step(xml, &mut buf)?;
|
let step = next_step(xml, &mut buf)?;
|
||||||
match step {
|
match step {
|
||||||
Step::StartElement { ns, local, .. } => {
|
Step::StartElement { ns, local, .. } => {
|
||||||
if is(ns, &local, NsKind::Dav, b"href") {
|
if is(ns, &local, NsKind::Dav, "href") {
|
||||||
let text = read_token(xml)?;
|
let text = read_token(xml)?;
|
||||||
if href.is_none() {
|
if href.is_none() {
|
||||||
href = Some(text);
|
href = Some(text);
|
||||||
}
|
}
|
||||||
} else if is(ns, &local, NsKind::Dav, b"status") {
|
} else if is(ns, &local, NsKind::Dav, "status") {
|
||||||
let text = read_token(xml)?;
|
let text = read_token(xml)?;
|
||||||
status = parse_http_status(&text);
|
status = parse_http_status(&text);
|
||||||
} else if is(ns, &local, NsKind::Dav, b"propstat") {
|
} else if is(ns, &local, NsKind::Dav, "propstat") {
|
||||||
let (block_status, block_props) = parse_propstat(xml)?;
|
let (block_status, block_props) = parse_propstat(xml)?;
|
||||||
match block_status {
|
match block_status {
|
||||||
Some(s) if (200..300).contains(&s) => merge_props(&mut props, block_props),
|
Some(s) if (200..300).contains(&s) => merge_props(&mut props, block_props),
|
||||||
@@ -241,7 +240,7 @@ fn parse_response<R: BufRead>(
|
|||||||
skip_element(xml, &local)?;
|
skip_element(xml, &local)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, b"response") => break,
|
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, "response") => break,
|
||||||
Step::Eof => return Err(ParseError::Xml("unexpected EOF in <response>".into())),
|
Step::Eof => return Err(ParseError::Xml("unexpected EOF in <response>".into())),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -270,16 +269,16 @@ fn parse_propstat<R: BufRead>(
|
|||||||
let step = next_step(xml, &mut buf)?;
|
let step = next_step(xml, &mut buf)?;
|
||||||
match step {
|
match step {
|
||||||
Step::StartElement { ns, local, .. } => {
|
Step::StartElement { ns, local, .. } => {
|
||||||
if is(ns, &local, NsKind::Dav, b"status") {
|
if is(ns, &local, NsKind::Dav, "status") {
|
||||||
let text = read_token(xml)?;
|
let text = read_token(xml)?;
|
||||||
status = parse_http_status(&text);
|
status = parse_http_status(&text);
|
||||||
} else if is(ns, &local, NsKind::Dav, b"prop") {
|
} else if is(ns, &local, NsKind::Dav, "prop") {
|
||||||
parse_prop_block(xml, &mut props)?;
|
parse_prop_block(xml, &mut props)?;
|
||||||
} else {
|
} else {
|
||||||
skip_element(xml, &local)?;
|
skip_element(xml, &local)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, b"propstat") => break,
|
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, "propstat") => break,
|
||||||
Step::Eof => return Err(ParseError::Xml("unexpected EOF in <propstat>".into())),
|
Step::Eof => return Err(ParseError::Xml("unexpected EOF in <propstat>".into())),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -299,7 +298,7 @@ fn parse_prop_block<R: BufRead>(
|
|||||||
handle_prop_element(xml, props, ns, &local)?;
|
handle_prop_element(xml, props, ns, &local)?;
|
||||||
}
|
}
|
||||||
Step::EmptyElement { .. } => {}
|
Step::EmptyElement { .. } => {}
|
||||||
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, b"prop") => break,
|
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, "prop") => break,
|
||||||
Step::Eof => return Err(ParseError::Xml("unexpected EOF in <prop>".into())),
|
Step::Eof => return Err(ParseError::Xml("unexpected EOF in <prop>".into())),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -311,53 +310,53 @@ fn handle_prop_element<R: BufRead>(
|
|||||||
xml: &mut NsReader<R>,
|
xml: &mut NsReader<R>,
|
||||||
props: &mut ResourceProps,
|
props: &mut ResourceProps,
|
||||||
ns: NsKind,
|
ns: NsKind,
|
||||||
local: &[u8],
|
local: &str,
|
||||||
) -> Result<(), ParseError> {
|
) -> Result<(), ParseError> {
|
||||||
if is(ns, local, NsKind::Dav, b"resourcetype") {
|
if is(ns, local, NsKind::Dav, "resourcetype") {
|
||||||
consume_resourcetype(xml, props)?;
|
consume_resourcetype(xml, props)?;
|
||||||
} else if is(ns, local, NsKind::Dav, b"displayname") {
|
} else if is(ns, local, NsKind::Dav, "displayname") {
|
||||||
props.displayname = Some(read_token(xml)?);
|
props.displayname = Some(read_token(xml)?);
|
||||||
} else if is(ns, local, NsKind::Dav, b"current-user-principal") {
|
} else if is(ns, local, NsKind::Dav, "current-user-principal") {
|
||||||
props.current_user_principal = read_first_href(xml, b"current-user-principal")?;
|
props.current_user_principal = read_first_href(xml, "current-user-principal")?;
|
||||||
} else if is(ns, local, NsKind::Caldav, b"calendar-home-set") {
|
} else if is(ns, local, NsKind::Caldav, "calendar-home-set") {
|
||||||
props.calendar_home_set = read_first_href(xml, b"calendar-home-set")?;
|
props.calendar_home_set = read_first_href(xml, "calendar-home-set")?;
|
||||||
} else if is(ns, local, NsKind::Carddav, b"addressbook-home-set") {
|
} else if is(ns, local, NsKind::Carddav, "addressbook-home-set") {
|
||||||
props.addressbook_home_set = read_first_href(xml, b"addressbook-home-set")?;
|
props.addressbook_home_set = read_first_href(xml, "addressbook-home-set")?;
|
||||||
} else if is(ns, local, NsKind::Dav, b"getetag") {
|
} else if is(ns, local, NsKind::Dav, "getetag") {
|
||||||
let raw = read_token(xml)?;
|
let raw = read_token(xml)?;
|
||||||
if !raw.is_empty() {
|
if !raw.is_empty() {
|
||||||
props.etag = Some(raw);
|
props.etag = Some(raw);
|
||||||
}
|
}
|
||||||
} else if is(ns, local, NsKind::Dav, b"getcontenttype") {
|
} else if is(ns, local, NsKind::Dav, "getcontenttype") {
|
||||||
props.content_type = Some(read_token(xml)?);
|
props.content_type = Some(read_token(xml)?);
|
||||||
} else if is(ns, local, NsKind::Dav, b"getlastmodified") {
|
} else if is(ns, local, NsKind::Dav, "getlastmodified") {
|
||||||
props.last_modified = Some(read_token(xml)?);
|
props.last_modified = Some(read_token(xml)?);
|
||||||
} else if is(ns, local, NsKind::Dav, b"creationdate") {
|
} else if is(ns, local, NsKind::Dav, "creationdate") {
|
||||||
props.creation_date = Some(read_token(xml)?);
|
props.creation_date = Some(read_token(xml)?);
|
||||||
} else if is(ns, local, NsKind::Dav, b"getcontentlength") {
|
} else if is(ns, local, NsKind::Dav, "getcontentlength") {
|
||||||
let raw = read_token(xml)?;
|
let raw = read_token(xml)?;
|
||||||
props.content_length = raw.parse::<u64>().ok();
|
props.content_length = raw.parse::<u64>().ok();
|
||||||
} else if is(ns, local, NsKind::Caldav, b"calendar-description") {
|
} else if is(ns, local, NsKind::Caldav, "calendar-description") {
|
||||||
props.calendar_description = Some(read_token(xml)?);
|
props.calendar_description = Some(read_token(xml)?);
|
||||||
} else if is(ns, local, NsKind::Carddav, b"addressbook-description") {
|
} else if is(ns, local, NsKind::Carddav, "addressbook-description") {
|
||||||
props.addressbook_description = Some(read_token(xml)?);
|
props.addressbook_description = Some(read_token(xml)?);
|
||||||
} else if is(ns, local, NsKind::Apple, b"calendar-color") {
|
} else if is(ns, local, NsKind::Apple, "calendar-color") {
|
||||||
props.calendar_color = Some(read_token(xml)?);
|
props.calendar_color = Some(read_token(xml)?);
|
||||||
} else if is(ns, local, NsKind::Apple, b"calendar-order") {
|
} else if is(ns, local, NsKind::Apple, "calendar-order") {
|
||||||
let raw = read_token(xml)?;
|
let raw = read_token(xml)?;
|
||||||
props.calendar_order = raw.parse::<i64>().ok();
|
props.calendar_order = raw.parse::<i64>().ok();
|
||||||
} else if is(ns, local, NsKind::Caldav, b"calendar-timezone") {
|
} else if is(ns, local, NsKind::Caldav, "calendar-timezone") {
|
||||||
props.calendar_timezone = Some(read_token(xml)?);
|
props.calendar_timezone = Some(read_token(xml)?);
|
||||||
} else if is(
|
} else if is(
|
||||||
ns,
|
ns,
|
||||||
local,
|
local,
|
||||||
NsKind::Caldav,
|
NsKind::Caldav,
|
||||||
b"supported-calendar-component-set",
|
"supported-calendar-component-set",
|
||||||
) {
|
) {
|
||||||
consume_supported_components(xml, props)?;
|
consume_supported_components(xml, props)?;
|
||||||
} else if is(ns, local, NsKind::Caldav, b"calendar-data") {
|
} else if is(ns, local, NsKind::Caldav, "calendar-data") {
|
||||||
props.calendar_data = Some(read_text(xml)?);
|
props.calendar_data = Some(read_text(xml)?);
|
||||||
} else if is(ns, local, NsKind::Carddav, b"address-data") {
|
} else if is(ns, local, NsKind::Carddav, "address-data") {
|
||||||
props.address_data = Some(read_text(xml)?);
|
props.address_data = Some(read_text(xml)?);
|
||||||
} else {
|
} else {
|
||||||
skip_element(xml, local)?;
|
skip_element(xml, local)?;
|
||||||
@@ -380,7 +379,7 @@ fn consume_resourcetype<R: BufRead>(
|
|||||||
Step::EmptyElement { ns, local, .. } => {
|
Step::EmptyElement { ns, local, .. } => {
|
||||||
mark_resourcetype(ns, &local, props);
|
mark_resourcetype(ns, &local, props);
|
||||||
}
|
}
|
||||||
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, b"resourcetype") => break,
|
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, "resourcetype") => break,
|
||||||
Step::Eof => {
|
Step::Eof => {
|
||||||
return Err(ParseError::Xml("unexpected EOF in <resourcetype>".into()));
|
return Err(ParseError::Xml("unexpected EOF in <resourcetype>".into()));
|
||||||
}
|
}
|
||||||
@@ -390,12 +389,12 @@ fn consume_resourcetype<R: BufRead>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark_resourcetype(ns: NsKind, local: &[u8], props: &mut ResourceProps) {
|
fn mark_resourcetype(ns: NsKind, local: &str, props: &mut ResourceProps) {
|
||||||
if is(ns, local, NsKind::Dav, b"collection") {
|
if is(ns, local, NsKind::Dav, "collection") {
|
||||||
props.is_collection = true;
|
props.is_collection = true;
|
||||||
} else if is(ns, local, NsKind::Caldav, b"calendar") {
|
} else if is(ns, local, NsKind::Caldav, "calendar") {
|
||||||
props.is_calendar = true;
|
props.is_calendar = true;
|
||||||
} else if is(ns, local, NsKind::Carddav, b"addressbook") {
|
} else if is(ns, local, NsKind::Carddav, "addressbook") {
|
||||||
props.is_addressbook = true;
|
props.is_addressbook = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,8 +411,8 @@ fn consume_supported_components<R: BufRead>(
|
|||||||
ns,
|
ns,
|
||||||
local,
|
local,
|
||||||
ref attrs,
|
ref attrs,
|
||||||
} if is(ns, &local, NsKind::Caldav, b"comp") => {
|
} if is(ns, &local, NsKind::Caldav, "comp") => {
|
||||||
if let Some(name) = attr_lookup(attrs, b"name") {
|
if let Some(name) = attr_lookup(attrs, "name") {
|
||||||
props.supported_components.push(name);
|
props.supported_components.push(name);
|
||||||
}
|
}
|
||||||
skip_element(xml, &local)?;
|
skip_element(xml, &local)?;
|
||||||
@@ -422,8 +421,8 @@ fn consume_supported_components<R: BufRead>(
|
|||||||
ns,
|
ns,
|
||||||
local,
|
local,
|
||||||
ref attrs,
|
ref attrs,
|
||||||
} if is(ns, &local, NsKind::Caldav, b"comp") => {
|
} if is(ns, &local, NsKind::Caldav, "comp") => {
|
||||||
if let Some(name) = attr_lookup(attrs, b"name") {
|
if let Some(name) = attr_lookup(attrs, "name") {
|
||||||
props.supported_components.push(name);
|
props.supported_components.push(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,7 +431,7 @@ fn consume_supported_components<R: BufRead>(
|
|||||||
ns,
|
ns,
|
||||||
&local,
|
&local,
|
||||||
NsKind::Caldav,
|
NsKind::Caldav,
|
||||||
b"supported-calendar-component-set",
|
"supported-calendar-component-set",
|
||||||
) =>
|
) =>
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -448,13 +447,8 @@ fn consume_supported_components<R: BufRead>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attr_lookup(attrs: &[(Vec<u8>, Vec<u8>)], key: &[u8]) -> Option<String> {
|
fn attr_lookup(attrs: &[(String, String)], key: &str) -> Option<String> {
|
||||||
for (k, v) in attrs {
|
attrs.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
|
||||||
if k == key {
|
|
||||||
return Some(String::from_utf8_lossy(v).into_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_text<R: BufRead>(xml: &mut NsReader<R>) -> Result<String, ParseError> {
|
fn read_text<R: BufRead>(xml: &mut NsReader<R>) -> Result<String, ParseError> {
|
||||||
@@ -480,7 +474,7 @@ fn read_token<R: BufRead>(xml: &mut NsReader<R>) -> Result<String, ParseError> {
|
|||||||
|
|
||||||
fn read_first_href<R: BufRead>(
|
fn read_first_href<R: BufRead>(
|
||||||
xml: &mut NsReader<R>,
|
xml: &mut NsReader<R>,
|
||||||
closing_local: &[u8],
|
closing_local: &str,
|
||||||
) -> Result<Option<String>, ParseError> {
|
) -> Result<Option<String>, ParseError> {
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
let mut out: Option<String> = None;
|
let mut out: Option<String> = None;
|
||||||
@@ -488,7 +482,7 @@ fn read_first_href<R: BufRead>(
|
|||||||
let step = next_step(xml, &mut buf)?;
|
let step = next_step(xml, &mut buf)?;
|
||||||
match step {
|
match step {
|
||||||
Step::StartElement { ns, local, .. } => {
|
Step::StartElement { ns, local, .. } => {
|
||||||
if out.is_none() && is(ns, &local, NsKind::Dav, b"href") {
|
if out.is_none() && is(ns, &local, NsKind::Dav, "href") {
|
||||||
out = Some(read_token(xml)?);
|
out = Some(read_token(xml)?);
|
||||||
} else {
|
} else {
|
||||||
skip_element(xml, &local)?;
|
skip_element(xml, &local)?;
|
||||||
@@ -505,7 +499,7 @@ fn read_first_href<R: BufRead>(
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn skip_element<R: BufRead>(xml: &mut NsReader<R>, target: &[u8]) -> Result<(), ParseError> {
|
fn skip_element<R: BufRead>(xml: &mut NsReader<R>, target: &str) -> Result<(), ParseError> {
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
let mut depth: i32 = 1;
|
let mut depth: i32 = 1;
|
||||||
while depth > 0 {
|
while depth > 0 {
|
||||||
|
|||||||
@@ -435,31 +435,31 @@ fn parse_pox_response(body: &[u8]) -> Result<PoxOutcome, EwsError> {
|
|||||||
let (_, ev) = xml.read_resolved_event_into(&mut buf)?;
|
let (_, ev) = xml.read_resolved_event_into(&mut buf)?;
|
||||||
match ev {
|
match ev {
|
||||||
Event::Start(e) => {
|
Event::Start(e) => {
|
||||||
let local = e.local_name().as_ref().to_vec();
|
let local = e.local_name().as_ref().to_owned();
|
||||||
cur.clear();
|
cur.clear();
|
||||||
if local.eq_ignore_ascii_case(b"Protocol") {
|
if local.eq_ignore_ascii_case("Protocol") {
|
||||||
current_type = None;
|
current_type = None;
|
||||||
}
|
}
|
||||||
if local.eq_ignore_ascii_case(b"Type") {
|
if local.eq_ignore_ascii_case("Type") {
|
||||||
current = Some("type");
|
current = Some("type");
|
||||||
} else if local.eq_ignore_ascii_case(b"EwsUrl") {
|
} else if local.eq_ignore_ascii_case("EwsUrl") {
|
||||||
if matches!(current_type.as_deref(), Some("EXPR") | Some("EXCH")) {
|
if matches!(current_type.as_deref(), Some("EXPR") | Some("EXCH")) {
|
||||||
current = Some("ewsUrl");
|
current = Some("ewsUrl");
|
||||||
} else {
|
} else {
|
||||||
current = None;
|
current = None;
|
||||||
}
|
}
|
||||||
} else if local.eq_ignore_ascii_case(b"Action") {
|
} else if local.eq_ignore_ascii_case("Action") {
|
||||||
current = Some("action");
|
current = Some("action");
|
||||||
} else if local.eq_ignore_ascii_case(b"RedirectAddr") {
|
} else if local.eq_ignore_ascii_case("RedirectAddr") {
|
||||||
current = Some("redirectAddr");
|
current = Some("redirectAddr");
|
||||||
} else if local.eq_ignore_ascii_case(b"RedirectUrl") {
|
} else if local.eq_ignore_ascii_case("RedirectUrl") {
|
||||||
current = Some("redirectUrl");
|
current = Some("redirectUrl");
|
||||||
} else {
|
} else {
|
||||||
current = None;
|
current = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::End(e) => {
|
Event::End(e) => {
|
||||||
let local = e.local_name().as_ref().to_vec();
|
let local = e.local_name().as_ref().to_owned();
|
||||||
if let Some(field) = current.take() {
|
if let Some(field) = current.take() {
|
||||||
let text = std::mem::take(&mut cur);
|
let text = std::mem::take(&mut cur);
|
||||||
match field {
|
match field {
|
||||||
@@ -472,15 +472,15 @@ fn parse_pox_response(body: &[u8]) -> Result<PoxOutcome, EwsError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cur.clear();
|
cur.clear();
|
||||||
if local.eq_ignore_ascii_case(b"Protocol") {
|
if local.eq_ignore_ascii_case("Protocol") {
|
||||||
current_type = None;
|
current_type = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Text(t) => {
|
Event::Text(t) => {
|
||||||
cur.push_str(&t.decode().map(|c| c.into_owned()).unwrap_or_default());
|
cur.push_str(&t);
|
||||||
}
|
}
|
||||||
Event::CData(c) => {
|
Event::CData(c) => {
|
||||||
cur.push_str(&String::from_utf8_lossy(c.as_ref()));
|
cur.push_str(&c);
|
||||||
}
|
}
|
||||||
Event::GeneralRef(g) => {
|
Event::GeneralRef(g) => {
|
||||||
if let Some(c) = entity_to_char(&g) {
|
if let Some(c) = entity_to_char(&g) {
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ use std::sync::Mutex;
|
|||||||
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use base64::Engine;
|
|
||||||
use base64::engine::general_purpose::STANDARD;
|
|
||||||
use ureq::Agent;
|
use ureq::Agent;
|
||||||
use ureq::config::{Config, RedirectAuthHeaders};
|
use ureq::config::{Config, RedirectAuthHeaders};
|
||||||
use ureq::tls::{RootCerts, TlsConfig};
|
use ureq::tls::{RootCerts, TlsConfig};
|
||||||
@@ -168,14 +166,11 @@ impl EwsClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn auth_header(&self) -> String {
|
fn auth_header(&self) -> String {
|
||||||
let auth = self.inner.auth.lock().ok().map(|g| g.clone());
|
self.inner
|
||||||
match auth {
|
.auth
|
||||||
Some(Auth::Basic { user, password }) => {
|
.lock()
|
||||||
format!("Basic {}", STANDARD.encode(format!("{user}:{password}")))
|
.map(|auth| auth.header_value())
|
||||||
}
|
.unwrap_or_default()
|
||||||
Some(Auth::Bearer { token }) => format!("Bearer {token}"),
|
|
||||||
None => String::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn anchor_header(&self) -> Option<String> {
|
fn anchor_header(&self) -> Option<String> {
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use base64::Engine;
|
use encodify::base64::{Base64, Padding, URL_SAFE};
|
||||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use ureq::config::Config;
|
use ureq::config::Config;
|
||||||
use ureq::tls::{RootCerts, TlsConfig};
|
use ureq::tls::{RootCerts, TlsConfig};
|
||||||
@@ -19,6 +18,8 @@ pub const SCOPE_APP_ONLY: &str = "https://outlook.office365.com/.default";
|
|||||||
pub const SCOPE_DELEGATED: &str =
|
pub const SCOPE_DELEGATED: &str =
|
||||||
"https://outlook.office365.com/EWS.AccessAsUser.All offline_access";
|
"https://outlook.office365.com/EWS.AccessAsUser.All offline_access";
|
||||||
|
|
||||||
|
const JWT_SEGMENT: Base64 = URL_SAFE.with_padding(Padding::Optional).any_alphabet();
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AcquiredToken {
|
pub struct AcquiredToken {
|
||||||
pub access_token: String,
|
pub access_token: String,
|
||||||
@@ -81,11 +82,7 @@ pub fn decode_jwt_claims(token: &str) -> Option<JwtClaims> {
|
|||||||
let mut parts = token.split('.');
|
let mut parts = token.split('.');
|
||||||
let _header = parts.next()?;
|
let _header = parts.next()?;
|
||||||
let payload = parts.next()?;
|
let payload = parts.next()?;
|
||||||
let bytes = URL_SAFE_NO_PAD
|
let bytes = JWT_SEGMENT.decode(payload).ok()?;
|
||||||
.decode(payload)
|
|
||||||
.or_else(|_| base64::engine::general_purpose::STANDARD_NO_PAD.decode(payload))
|
|
||||||
.or_else(|_| base64::engine::general_purpose::STANDARD.decode(payload))
|
|
||||||
.ok()?;
|
|
||||||
let value: Value = serde_json::from_slice(&bytes).ok()?;
|
let value: Value = serde_json::from_slice(&bytes).ok()?;
|
||||||
Some(JwtClaims {
|
Some(JwtClaims {
|
||||||
tenant_id: value.get("tid").and_then(Value::as_str).map(str::to_owned),
|
tenant_id: value.get("tid").and_then(Value::as_str).map(str::to_owned),
|
||||||
@@ -329,7 +326,7 @@ fn urlencode(s: &str) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
use encodify::base64::{STANDARD, STANDARD_NO_PAD, URL_SAFE_NO_PAD};
|
||||||
|
|
||||||
fn make_jwt(tid: &str, upn: &str, exp: u64) -> String {
|
fn make_jwt(tid: &str, upn: &str, exp: u64) -> String {
|
||||||
let header = URL_SAFE_NO_PAD.encode(b"{\"alg\":\"none\"}");
|
let header = URL_SAFE_NO_PAD.encode(b"{\"alg\":\"none\"}");
|
||||||
@@ -347,6 +344,19 @@ mod tests {
|
|||||||
assert_eq!(claims.exp, Some(9999999999));
|
assert_eq!(claims.exp, Some(9999999999));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn jwt_payload_decodes_in_either_alphabet_with_or_without_padding() {
|
||||||
|
let claims = r#"{"tid":"t-3","upn":"??>>@xy","exp":1}"#;
|
||||||
|
for engine in [URL_SAFE_NO_PAD, URL_SAFE, STANDARD_NO_PAD, STANDARD] {
|
||||||
|
let token = format!("h.{}.s", engine.encode(claims));
|
||||||
|
let decoded = decode_jwt_claims(&token).unwrap();
|
||||||
|
assert_eq!(decoded.upn.as_deref(), Some("??>>@xy"), "{token}");
|
||||||
|
}
|
||||||
|
assert!(decode_jwt_claims("h.eyJ0aWQiOiJ0LTMifQ.s").is_some());
|
||||||
|
assert!(decode_jwt_claims("h.eyJ0aWQiOiJ0LTMifQ=.s").is_none());
|
||||||
|
assert!(decode_jwt_claims("h.eyJ0 aWQiOiJ0LTMifQ.s").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn malformed_token_returns_none() {
|
fn malformed_token_returns_none() {
|
||||||
assert!(decode_jwt_claims("garbage").is_none());
|
assert!(decode_jwt_claims("garbage").is_none());
|
||||||
|
|||||||
+307
-312
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,7 @@
|
|||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use base64::Engine;
|
use encodify::base64::{Base64, Padding, URL_SAFE};
|
||||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use ureq::config::Config;
|
use ureq::config::Config;
|
||||||
use ureq::tls::{RootCerts, TlsConfig};
|
use ureq::tls::{RootCerts, TlsConfig};
|
||||||
@@ -18,6 +17,8 @@ use crate::exchange_graph::error::GraphError;
|
|||||||
pub const SCOPES: &str =
|
pub const SCOPES: &str =
|
||||||
"offline_access User.Read Mail.Read MailboxSettings.Read Calendars.Read Contacts.Read";
|
"offline_access User.Read Mail.Read MailboxSettings.Read Calendars.Read Contacts.Read";
|
||||||
|
|
||||||
|
const JWT_SEGMENT: Base64 = URL_SAFE.with_padding(Padding::Optional).any_alphabet();
|
||||||
|
|
||||||
pub fn default_authority(tenant: &str) -> String {
|
pub fn default_authority(tenant: &str) -> String {
|
||||||
format!("https://login.microsoftonline.com/{tenant}")
|
format!("https://login.microsoftonline.com/{tenant}")
|
||||||
}
|
}
|
||||||
@@ -53,11 +54,7 @@ pub fn decode_jwt_claims(token: &str) -> Option<JwtClaims> {
|
|||||||
let mut parts = token.split('.');
|
let mut parts = token.split('.');
|
||||||
let _header = parts.next()?;
|
let _header = parts.next()?;
|
||||||
let payload = parts.next()?;
|
let payload = parts.next()?;
|
||||||
let bytes = URL_SAFE_NO_PAD
|
let bytes = JWT_SEGMENT.decode(payload).ok()?;
|
||||||
.decode(payload)
|
|
||||||
.or_else(|_| base64::engine::general_purpose::STANDARD_NO_PAD.decode(payload))
|
|
||||||
.or_else(|_| base64::engine::general_purpose::STANDARD.decode(payload))
|
|
||||||
.ok()?;
|
|
||||||
let value: Value = serde_json::from_slice(&bytes).ok()?;
|
let value: Value = serde_json::from_slice(&bytes).ok()?;
|
||||||
Some(JwtClaims {
|
Some(JwtClaims {
|
||||||
tenant_id: value.get("tid").and_then(Value::as_str).map(str::to_owned),
|
tenant_id: value.get("tid").and_then(Value::as_str).map(str::to_owned),
|
||||||
@@ -420,6 +417,7 @@ where
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use encodify::base64::{STANDARD, STANDARD_NO_PAD, URL_SAFE_NO_PAD};
|
||||||
|
|
||||||
fn make_jwt(tid: &str, upn: &str, exp: u64) -> String {
|
fn make_jwt(tid: &str, upn: &str, exp: u64) -> String {
|
||||||
let header = URL_SAFE_NO_PAD.encode(b"{\"alg\":\"none\"}");
|
let header = URL_SAFE_NO_PAD.encode(b"{\"alg\":\"none\"}");
|
||||||
@@ -437,6 +435,19 @@ mod tests {
|
|||||||
assert_eq!(claims.exp, Some(9999999999));
|
assert_eq!(claims.exp, Some(9999999999));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn jwt_payload_decodes_in_either_alphabet_with_or_without_padding() {
|
||||||
|
let claims = r#"{"tid":"t-3","upn":"??>>@xy","exp":1}"#;
|
||||||
|
for engine in [URL_SAFE_NO_PAD, URL_SAFE, STANDARD_NO_PAD, STANDARD] {
|
||||||
|
let token = format!("h.{}.s", engine.encode(claims));
|
||||||
|
let decoded = decode_jwt_claims(&token).unwrap();
|
||||||
|
assert_eq!(decoded.upn.as_deref(), Some("??>>@xy"), "{token}");
|
||||||
|
}
|
||||||
|
assert!(decode_jwt_claims("h.eyJ0aWQiOiJ0LTMifQ.s").is_some());
|
||||||
|
assert!(decode_jwt_claims("h.eyJ0aWQiOiJ0LTMifQ=.s").is_none());
|
||||||
|
assert!(decode_jwt_claims("h.eyJ0 aWQiOiJ0LTMifQ.s").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn malformed_token_returns_none() {
|
fn malformed_token_returns_none() {
|
||||||
assert!(decode_jwt_claims("garbage").is_none());
|
assert!(decode_jwt_claims("garbage").is_none());
|
||||||
|
|||||||
+14
-13
@@ -8,9 +8,6 @@ use std::collections::BTreeSet;
|
|||||||
use std::io::{BufReader, Read, Write};
|
use std::io::{BufReader, Read, Write};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use base64::Engine;
|
|
||||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
|
||||||
|
|
||||||
use super::command::{self, CommandBuilder};
|
use super::command::{self, CommandBuilder};
|
||||||
use super::error::{ImapError, NoError};
|
use super::error::{ImapError, NoError};
|
||||||
use super::response::{Response, Status, StatusLine, Untagged, parse_response};
|
use super::response::{Response, Status, StatusLine, Untagged, parse_response};
|
||||||
@@ -201,9 +198,8 @@ impl ImapClient {
|
|||||||
payload.extend_from_slice(authcid.as_bytes());
|
payload.extend_from_slice(authcid.as_bytes());
|
||||||
payload.push(0);
|
payload.push(0);
|
||||||
payload.extend_from_slice(password.as_bytes());
|
payload.extend_from_slice(password.as_bytes());
|
||||||
let encoded = BASE64.encode(&payload);
|
|
||||||
if self.has_capability("SASL-IR") {
|
if self.has_capability("SASL-IR") {
|
||||||
let cmd = command::authenticate_with_ir("PLAIN", &encoded);
|
let cmd = command::authenticate_with_ir("PLAIN", &payload);
|
||||||
return match self.run_collect(&cmd) {
|
return match self.run_collect(&cmd) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(ImapError::No(no)) if no.is_auth_failed() => {
|
Err(ImapError::No(no)) if no.is_auth_failed() => {
|
||||||
@@ -218,9 +214,7 @@ impl ImapClient {
|
|||||||
let resp = parse_response(&mut self.reader)?;
|
let resp = parse_response(&mut self.reader)?;
|
||||||
match resp {
|
match resp {
|
||||||
Response::Continuation(_) => {
|
Response::Continuation(_) => {
|
||||||
let mut line = encoded.clone().into_bytes();
|
self.write_all(&command::sasl_response(&payload))?;
|
||||||
line.extend_from_slice(b"\r\n");
|
|
||||||
self.write_all(&line)?;
|
|
||||||
}
|
}
|
||||||
Response::Tagged { tag: t, line } if t == tag => {
|
Response::Tagged { tag: t, line } if t == tag => {
|
||||||
self.update_capabilities_from_code(&line);
|
self.update_capabilities_from_code(&line);
|
||||||
@@ -260,9 +254,8 @@ impl ImapClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn drive_bearer_sasl(&mut self, mechanism: &str, payload: &[u8]) -> Result<(), ImapError> {
|
fn drive_bearer_sasl(&mut self, mechanism: &str, payload: &[u8]) -> Result<(), ImapError> {
|
||||||
let encoded = BASE64.encode(payload);
|
|
||||||
if self.has_capability("SASL-IR") {
|
if self.has_capability("SASL-IR") {
|
||||||
let cmd = command::authenticate_with_ir(mechanism, &encoded);
|
let cmd = command::authenticate_with_ir(mechanism, payload);
|
||||||
return match self.run_collect(&cmd) {
|
return match self.run_collect(&cmd) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(ImapError::No(no)) if no.is_auth_failed() => {
|
Err(ImapError::No(no)) if no.is_auth_failed() => {
|
||||||
@@ -281,9 +274,7 @@ impl ImapClient {
|
|||||||
if sent {
|
if sent {
|
||||||
self.write_all(b"\r\n")?;
|
self.write_all(b"\r\n")?;
|
||||||
} else {
|
} else {
|
||||||
let mut line = encoded.clone().into_bytes();
|
self.write_all(&command::sasl_response(payload))?;
|
||||||
line.extend_from_slice(b"\r\n");
|
|
||||||
self.write_all(&line)?;
|
|
||||||
sent = true;
|
sent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -664,6 +655,16 @@ mod tests {
|
|||||||
c.authenticate_plain("alice", "p@ss").unwrap();
|
c.authenticate_plain("alice", "p@ss").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authenticate_plain_accepts_dovecot_post_login_untagged_capability() {
|
||||||
|
let server = b"* CAPABILITY IMAP4rev1 SASL-IR MOVE IMAPSIEVE=sieve://127.0.0.1:4190 QUOTA\r\nA0001 OK [CAPABILITY IMAP4rev1 SASL-IR MOVE IMAPSIEVE=sieve://127.0.0.1:4190 QUOTA] Logged in\r\n";
|
||||||
|
let mut c = client_with(server);
|
||||||
|
c.capabilities.insert("SASL-IR".to_owned());
|
||||||
|
c.authenticate_plain("alice", "p@ss").unwrap();
|
||||||
|
assert!(c.has_capability("MOVE"));
|
||||||
|
assert!(c.has_capability("IMAPSIEVE=sieve://127.0.0.1:4190"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn authenticate_plain_continuation_path() {
|
fn authenticate_plain_continuation_path() {
|
||||||
let server = b"+ \r\nA0001 OK auth done\r\n";
|
let server = b"+ \r\nA0001 OK auth done\r\n";
|
||||||
|
|||||||
+39
-2
@@ -6,6 +6,11 @@
|
|||||||
|
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
|
use encodify::base64::STANDARD;
|
||||||
|
|
||||||
|
const AUTHENTICATE: &str = "AUTHENTICATE ";
|
||||||
|
const CRLF: &[u8] = b"\r\n";
|
||||||
|
|
||||||
pub struct CommandBuilder {
|
pub struct CommandBuilder {
|
||||||
next_tag: u32,
|
next_tag: u32,
|
||||||
}
|
}
|
||||||
@@ -84,8 +89,22 @@ pub fn authenticate(mechanism: &str) -> String {
|
|||||||
format!("AUTHENTICATE {mechanism}")
|
format!("AUTHENTICATE {mechanism}")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn authenticate_with_ir(mechanism: &str, initial_response: &str) -> String {
|
pub fn authenticate_with_ir(mechanism: &str, initial_response: &[u8]) -> String {
|
||||||
format!("AUTHENTICATE {mechanism} {initial_response}")
|
let mut out = String::with_capacity(
|
||||||
|
AUTHENTICATE.len() + mechanism.len() + 1 + STANDARD.encoded_len(initial_response.len()),
|
||||||
|
);
|
||||||
|
out.push_str(AUTHENTICATE);
|
||||||
|
out.push_str(mechanism);
|
||||||
|
out.push(' ');
|
||||||
|
STANDARD.encode_append(initial_response, &mut out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sasl_response(response: &[u8]) -> Vec<u8> {
|
||||||
|
let mut line = Vec::with_capacity(STANDARD.encoded_len(response.len()) + CRLF.len());
|
||||||
|
STANDARD.encode_append(response, &mut line);
|
||||||
|
line.extend_from_slice(CRLF);
|
||||||
|
line
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn capability() -> &'static str {
|
pub fn capability() -> &'static str {
|
||||||
@@ -282,6 +301,24 @@ mod tests {
|
|||||||
assert_eq!(b.next_tag(), "A0003");
|
assert_eq!(b.next_tag(), "A0003");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authenticate_with_ir_appends_the_base64_initial_response() {
|
||||||
|
assert_eq!(
|
||||||
|
authenticate_with_ir("PLAIN", b"\0foo\0bar"),
|
||||||
|
"AUTHENTICATE PLAIN AGZvbwBiYXI="
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
authenticate_with_ir("XOAUTH2", b""),
|
||||||
|
"AUTHENTICATE XOAUTH2 "
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sasl_response_is_base64_with_crlf() {
|
||||||
|
assert_eq!(sasl_response(b"\0foo\0bar"), b"AGZvbwBiYXI=\r\n");
|
||||||
|
assert_eq!(sasl_response(b""), b"\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_appends_crlf() {
|
fn build_appends_crlf() {
|
||||||
let mut b = CommandBuilder::new();
|
let mut b = CommandBuilder::new();
|
||||||
|
|||||||
+54
-176
@@ -4,10 +4,11 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use encodify::utf7::{self, Utf7};
|
||||||
|
|
||||||
use super::error::ImapError;
|
use super::error::ImapError;
|
||||||
|
|
||||||
const MODIFIED_UTF7_ALPHABET: &[u8] =
|
const RECEIVED_NAME: Utf7 = utf7::IMAP.lenient();
|
||||||
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,";
|
|
||||||
|
|
||||||
pub fn canonicalise_inbox(name: &str) -> String {
|
pub fn canonicalise_inbox(name: &str) -> String {
|
||||||
if name.eq_ignore_ascii_case("INBOX") {
|
if name.eq_ignore_ascii_case("INBOX") {
|
||||||
@@ -22,145 +23,17 @@ pub fn decode_mailbox_name(input: &str) -> Result<String, ImapError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_mailbox_name_with(input: &str, utf8_accept: bool) -> Result<String, ImapError> {
|
pub fn decode_mailbox_name_with(input: &str, utf8_accept: bool) -> Result<String, ImapError> {
|
||||||
if utf8_accept {
|
if utf8_accept || ends_in_open_shift(input) {
|
||||||
return Ok(input.to_owned());
|
return Ok(input.to_owned());
|
||||||
}
|
}
|
||||||
if input.is_ascii() && !input.contains('&') {
|
RECEIVED_NAME
|
||||||
return Ok(input.to_owned());
|
.decode(input)
|
||||||
}
|
.map_err(|e| ImapError::Parse(format!("modified UTF-7 mailbox name: {e}")))
|
||||||
let bytes = input.as_bytes();
|
|
||||||
let mut out = String::with_capacity(input.len());
|
|
||||||
let mut i = 0;
|
|
||||||
while i < bytes.len() {
|
|
||||||
if bytes[i] != b'&' {
|
|
||||||
let start = i;
|
|
||||||
while i < bytes.len() && bytes[i] != b'&' {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
out.push_str(&input[start..i]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if i + 1 < bytes.len() && bytes[i + 1] == b'-' {
|
|
||||||
out.push('&');
|
|
||||||
i += 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut end = i + 1;
|
|
||||||
while end < bytes.len() && bytes[end] != b'-' {
|
|
||||||
end += 1;
|
|
||||||
}
|
|
||||||
if end == bytes.len() {
|
|
||||||
return Err(ImapError::Parse(
|
|
||||||
"unterminated modified UTF-7 sequence".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let encoded = &bytes[i + 1..end];
|
|
||||||
let decoded = decode_b64_modified(encoded)?;
|
|
||||||
let u16s: Vec<u16> = decoded
|
|
||||||
.chunks(2)
|
|
||||||
.filter(|c| c.len() == 2)
|
|
||||||
.map(|c| u16::from_be_bytes([c[0], c[1]]))
|
|
||||||
.collect();
|
|
||||||
let s = String::from_utf16(&u16s).map_err(|e| ImapError::Parse(format!("utf-16: {e}")))?;
|
|
||||||
out.push_str(&s);
|
|
||||||
i = end + 1;
|
|
||||||
}
|
|
||||||
Ok(out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn alternate_mailbox_name(input: &str, utf8_accept: bool) -> Option<String> {
|
fn ends_in_open_shift(name: &str) -> bool {
|
||||||
let primary = encode_mailbox_name_with(input, utf8_accept);
|
name.rsplit_once('&')
|
||||||
let alternate = encode_mailbox_name_with(input, !utf8_accept);
|
.is_some_and(|(_, tail)| !tail.is_empty() && !tail.contains('-'))
|
||||||
if alternate == primary {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(alternate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn encode_mailbox_name(input: &str) -> String {
|
|
||||||
encode_mailbox_name_with(input, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn encode_mailbox_name_with(input: &str, utf8_accept: bool) -> String {
|
|
||||||
if utf8_accept {
|
|
||||||
return input.to_owned();
|
|
||||||
}
|
|
||||||
let mut out = String::with_capacity(input.len());
|
|
||||||
let mut pending: Vec<u16> = Vec::new();
|
|
||||||
let flush = |pending: &mut Vec<u16>, out: &mut String| {
|
|
||||||
if pending.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
out.push('&');
|
|
||||||
let mut raw = Vec::with_capacity(pending.len() * 2);
|
|
||||||
for w in pending.iter() {
|
|
||||||
raw.extend_from_slice(&w.to_be_bytes());
|
|
||||||
}
|
|
||||||
out.push_str(&encode_b64_modified(&raw));
|
|
||||||
out.push('-');
|
|
||||||
pending.clear();
|
|
||||||
};
|
|
||||||
for c in input.chars() {
|
|
||||||
let cp = c as u32;
|
|
||||||
if c == '&' {
|
|
||||||
flush(&mut pending, &mut out);
|
|
||||||
out.push_str("&-");
|
|
||||||
} else if (0x20..=0x7E).contains(&cp) {
|
|
||||||
flush(&mut pending, &mut out);
|
|
||||||
out.push(c);
|
|
||||||
} else {
|
|
||||||
let mut buf = [0u16; 2];
|
|
||||||
let units = c.encode_utf16(&mut buf);
|
|
||||||
pending.extend_from_slice(units);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flush(&mut pending, &mut out);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decode_b64_modified(bytes: &[u8]) -> Result<Vec<u8>, ImapError> {
|
|
||||||
let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
|
|
||||||
let mut acc: u32 = 0;
|
|
||||||
let mut bits: u32 = 0;
|
|
||||||
for &b in bytes {
|
|
||||||
let v = match MODIFIED_UTF7_ALPHABET.iter().position(|&x| x == b) {
|
|
||||||
Some(idx) => idx as u32,
|
|
||||||
None => {
|
|
||||||
return Err(ImapError::Parse(format!(
|
|
||||||
"invalid modified UTF-7 byte {b:#04x}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
acc = (acc << 6) | v;
|
|
||||||
bits += 6;
|
|
||||||
if bits >= 8 {
|
|
||||||
bits -= 8;
|
|
||||||
let byte = ((acc >> bits) & 0xFF) as u8;
|
|
||||||
out.push(byte);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode_b64_modified(bytes: &[u8]) -> String {
|
|
||||||
let mut out = String::with_capacity(bytes.len() * 4 / 3 + 1);
|
|
||||||
let mut acc: u32 = 0;
|
|
||||||
let mut bits: u32 = 0;
|
|
||||||
for &b in bytes {
|
|
||||||
acc = (acc << 8) | b as u32;
|
|
||||||
bits += 8;
|
|
||||||
while bits >= 6 {
|
|
||||||
bits -= 6;
|
|
||||||
let idx = ((acc >> bits) & 0x3F) as usize;
|
|
||||||
out.push(MODIFIED_UTF7_ALPHABET[idx] as char);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if bits > 0 {
|
|
||||||
let idx = ((acc << (6 - bits)) & 0x3F) as usize;
|
|
||||||
out.push(MODIFIED_UTF7_ALPHABET[idx] as char);
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -186,27 +59,52 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unterminated_sequence_errors() {
|
fn raw_ampersand_without_a_closing_dash_keeps_the_name_verbatim() {
|
||||||
let err = decode_mailbox_name("&ZeVnLIqe").unwrap_err();
|
for name in [
|
||||||
|
"R&D",
|
||||||
|
"AT&T",
|
||||||
|
"Tom&Jerry",
|
||||||
|
"A&-B&C",
|
||||||
|
"R&D Team",
|
||||||
|
"&ZeVnLIqe",
|
||||||
|
"Envoy&AOk",
|
||||||
|
] {
|
||||||
|
assert_eq!(decode_mailbox_name(name).unwrap(), name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trailing_lone_ampersand_is_literal() {
|
||||||
|
assert_eq!(decode_mailbox_name("Sales&").unwrap(), "Sales&");
|
||||||
|
assert_eq!(decode_mailbox_name("&AOk-s&").unwrap(), "és&");
|
||||||
|
assert_eq!(decode_mailbox_name("&").unwrap(), "&");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encoded_name_with_a_later_escaped_ampersand_decodes() {
|
||||||
|
assert_eq!(decode_mailbox_name("Envoy&AOk-s").unwrap(), "Envoyés");
|
||||||
|
assert_eq!(decode_mailbox_name("&AOk-/R&-D").unwrap(), "é/R&D");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn byte_outside_the_alphabet_inside_a_shift_errors() {
|
||||||
|
for name in ["&AOk.s-", "&AOk&-", "&AOké-"] {
|
||||||
|
let err = decode_mailbox_name(name).unwrap_err();
|
||||||
|
assert!(matches!(err, ImapError::Parse(_)), "{name}: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unpaired_surrogate_errors() {
|
||||||
|
let err = decode_mailbox_name("&2D0-").unwrap_err();
|
||||||
assert!(matches!(err, ImapError::Parse(_)));
|
assert!(matches!(err, ImapError::Parse(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ascii_passes_through_encode() {
|
fn encoded_printable_ascii_and_leftover_bits_are_accepted() {
|
||||||
assert_eq!(encode_mailbox_name("INBOX"), "INBOX");
|
assert_eq!(decode_mailbox_name("&AGE-").unwrap(), "a");
|
||||||
assert_eq!(encode_mailbox_name("Sent Items"), "Sent Items");
|
assert_eq!(decode_mailbox_name("&AOl-").unwrap(), "é");
|
||||||
}
|
assert_eq!(decode_mailbox_name("&AOk-&AOk-").unwrap(), "éé");
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn literal_amp_encodes_to_amp_dash() {
|
|
||||||
assert_eq!(encode_mailbox_name("R&D"), "R&-D");
|
|
||||||
assert_eq!(encode_mailbox_name("&"), "&-");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn non_ascii_encode_japanese() {
|
|
||||||
let encoded = encode_mailbox_name("~peter/mail/日本語/台北");
|
|
||||||
assert_eq!(encoded, "~peter/mail/&ZeVnLIqe-/&U,BTFw-");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -220,7 +118,7 @@ mod tests {
|
|||||||
"INBOX.Projects.Alpha",
|
"INBOX.Projects.Alpha",
|
||||||
"Junk E-mail",
|
"Junk E-mail",
|
||||||
] {
|
] {
|
||||||
let enc = encode_mailbox_name(s);
|
let enc = utf7::IMAP.encode(s);
|
||||||
let dec = decode_mailbox_name(&enc).unwrap();
|
let dec = decode_mailbox_name(&enc).unwrap();
|
||||||
assert_eq!(dec, s, "roundtrip failed for {s:?}, enc={enc:?}");
|
assert_eq!(dec, s, "roundtrip failed for {s:?}, enc={enc:?}");
|
||||||
}
|
}
|
||||||
@@ -247,34 +145,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn raw_utf8_name_round_trips_through_encode() {
|
fn raw_utf8_name_round_trips_through_encode() {
|
||||||
for s in ["Envoyés", "Gönderilmiş Postalar", "Çöp kutusu"] {
|
for s in ["Envoyés", "Gönderilmiş Postalar", "Çöp kutusu"] {
|
||||||
assert_eq!(decode_mailbox_name(&encode_mailbox_name(s)).unwrap(), s);
|
assert_eq!(decode_mailbox_name(&utf7::IMAP.encode(s)).unwrap(), s);
|
||||||
assert_eq!(decode_mailbox_name(s).unwrap(), s);
|
assert_eq!(decode_mailbox_name(s).unwrap(), s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alternate_encoding_offered_only_for_non_ascii_names() {
|
|
||||||
assert_eq!(
|
|
||||||
alternate_mailbox_name("Envoyés", false).as_deref(),
|
|
||||||
Some("Envoyés")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
alternate_mailbox_name("Envoyés", true).as_deref(),
|
|
||||||
Some("Envoy&AOk-s")
|
|
||||||
);
|
|
||||||
assert_eq!(alternate_mailbox_name("INBOX", false), None);
|
|
||||||
assert_eq!(alternate_mailbox_name("Sent Items", true), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn utf8_accept_skips_mutf7_decode_so_ampersand_passes_through() {
|
fn utf8_accept_skips_mutf7_decode_so_ampersand_passes_through() {
|
||||||
assert_eq!(decode_mailbox_name_with("R&D", true).unwrap(), "R&D");
|
assert_eq!(decode_mailbox_name_with("R&D", true).unwrap(), "R&D");
|
||||||
assert_eq!(decode_mailbox_name_with("日本語", true).unwrap(), "日本語");
|
assert_eq!(decode_mailbox_name_with("日本語", true).unwrap(), "日本語");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn utf8_accept_skips_mutf7_encode_so_input_passes_through() {
|
|
||||||
assert_eq!(encode_mailbox_name_with("日本語", true), "日本語");
|
|
||||||
assert_eq!(encode_mailbox_name_with("R&D", true), "R&D");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-26
@@ -190,17 +190,7 @@ impl<'r, R: BufRead> Parser<'r, R> {
|
|||||||
}
|
}
|
||||||
Ok(Untagged::StatusLine(line))
|
Ok(Untagged::StatusLine(line))
|
||||||
}
|
}
|
||||||
"CAPABILITY" => {
|
"CAPABILITY" => Ok(Untagged::Capability(self.read_space_separated()?)),
|
||||||
let mut caps = Vec::new();
|
|
||||||
while !self.at_end() {
|
|
||||||
self.skip_ws();
|
|
||||||
if self.at_end() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
caps.push(self.read_atom_string()?);
|
|
||||||
}
|
|
||||||
Ok(Untagged::Capability(caps))
|
|
||||||
}
|
|
||||||
"LIST" | "LSUB" => {
|
"LIST" | "LSUB" => {
|
||||||
self.skip_ws();
|
self.skip_ws();
|
||||||
self.expect(b'(')?;
|
self.expect(b'(')?;
|
||||||
@@ -300,17 +290,7 @@ impl<'r, R: BufRead> Parser<'r, R> {
|
|||||||
Ok(Untagged::Flags(flags))
|
Ok(Untagged::Flags(flags))
|
||||||
}
|
}
|
||||||
"NAMESPACE" => self.parse_namespace(),
|
"NAMESPACE" => self.parse_namespace(),
|
||||||
"ENABLED" => {
|
"ENABLED" => Ok(Untagged::Enabled(self.read_space_separated()?)),
|
||||||
let mut exts = Vec::new();
|
|
||||||
while !self.at_end() {
|
|
||||||
self.skip_ws();
|
|
||||||
if self.at_end() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
exts.push(self.read_atom_string()?);
|
|
||||||
}
|
|
||||||
Ok(Untagged::Enabled(exts))
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
if let Ok(num) = upper.parse::<u32>() {
|
if let Ok(num) = upper.parse::<u32>() {
|
||||||
self.skip_ws();
|
self.skip_ws();
|
||||||
@@ -651,6 +631,15 @@ impl<'r, R: BufRead> Parser<'r, R> {
|
|||||||
.map_err(|e| ImapError::Parse(format!("non-utf8 atom: {e}")))
|
.map_err(|e| ImapError::Parse(format!("non-utf8 atom: {e}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_space_separated(&mut self) -> Result<Vec<String>, ImapError> {
|
||||||
|
let end = self.buf.len().saturating_sub(self.tail_crlf_len());
|
||||||
|
let rest = self.buf.get(self.pos..end).unwrap_or_default();
|
||||||
|
self.pos = self.pos.max(end);
|
||||||
|
std::str::from_utf8(rest)
|
||||||
|
.map(|s| s.split_ascii_whitespace().map(str::to_owned).collect())
|
||||||
|
.map_err(|e| ImapError::Parse(format!("non-utf8 token: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
fn read_seq_set_token(&mut self) -> String {
|
fn read_seq_set_token(&mut self) -> String {
|
||||||
let start = self.pos;
|
let start = self.pos;
|
||||||
while let Some(b) = self.peek() {
|
while let Some(b) = self.peek() {
|
||||||
@@ -735,10 +724,7 @@ impl<'r, R: BufRead> Parser<'r, R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_atom_byte(b: u8) -> bool {
|
fn is_atom_byte(b: u8) -> bool {
|
||||||
matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'
|
b.is_ascii_graphic() && !matches!(b, b'(' | b')' | b'{' | b'%' | b'"' | b']')
|
||||||
| b'-' | b'.' | b'_' | b'!' | b'#' | b'$' | b'&'
|
|
||||||
| b'\'' | b'+' | b'/' | b';' | b'<' | b'=' | b'>' | b'?'
|
|
||||||
| b'@' | b'\\' | b'^' | b'`' | b'|' | b'~' | b'*')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_seq_set(s: &str) -> Vec<u32> {
|
fn parse_seq_set(s: &str) -> Vec<u32> {
|
||||||
@@ -833,6 +819,60 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn untagged_capability_accepts_uri_valued_tokens() {
|
||||||
|
let r = parse(
|
||||||
|
b"* CAPABILITY IMAP4rev1 SASL-IR SPECIAL-USE IMAPSIEVE=sieve://127.0.0.1:4190 QUOTA ACL RIGHTS=texk\r\n",
|
||||||
|
);
|
||||||
|
match r {
|
||||||
|
Response::Untagged(Untagged::Capability(caps)) => assert_eq!(
|
||||||
|
caps,
|
||||||
|
vec![
|
||||||
|
"IMAP4rev1",
|
||||||
|
"SASL-IR",
|
||||||
|
"SPECIAL-USE",
|
||||||
|
"IMAPSIEVE=sieve://127.0.0.1:4190",
|
||||||
|
"QUOTA",
|
||||||
|
"ACL",
|
||||||
|
"RIGHTS=texk",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
other => panic!("expected Capability, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn untagged_enabled_accepts_non_atom_tokens() {
|
||||||
|
let r = parse(b"* ENABLED UTF8=ACCEPT X-VENDOR=a:b,c\r\n");
|
||||||
|
match r {
|
||||||
|
Response::Untagged(Untagged::Enabled(exts)) => {
|
||||||
|
assert_eq!(exts, vec!["UTF8=ACCEPT", "X-VENDOR=a:b,c"]);
|
||||||
|
}
|
||||||
|
other => panic!("expected Enabled, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn atom_accepts_colon_comma_and_brackets() {
|
||||||
|
let r = parse(b"* LIST (\\HasNoChildren) \"/\" Work:2026,Q1[a}\r\n");
|
||||||
|
match r {
|
||||||
|
Response::Untagged(Untagged::List { name, .. }) => {
|
||||||
|
assert_eq!(name, "Work:2026,Q1[a}");
|
||||||
|
}
|
||||||
|
other => panic!("expected List, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn atom_stops_at_atom_specials() {
|
||||||
|
for b in b"(){%\"] \x7f\x00" {
|
||||||
|
assert!(!is_atom_byte(*b), "{b:#04x} must not be an atom byte");
|
||||||
|
}
|
||||||
|
for b in b":,[}=/.-_+" {
|
||||||
|
assert!(is_atom_byte(*b), "{:?} must be an atom byte", *b as char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quoted_mailbox_name_with_utf8_bytes_is_decoded_as_utf8() {
|
fn quoted_mailbox_name_with_utf8_bytes_is_decoded_as_utf8() {
|
||||||
let r = parse("* LIST (\\HasNoChildren) \"/\" \"Envoy\u{e9}s\"\r\n".as_bytes());
|
let r = parse("* LIST (\\HasNoChildren) \"/\" \"Envoy\u{e9}s\"\r\n".as_bytes());
|
||||||
|
|||||||
+6
-4
@@ -4,8 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use base64::Engine;
|
use encodify::base64::STANDARD;
|
||||||
use base64::engine::general_purpose::STANDARD;
|
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
|
|
||||||
use crate::jmap::error::JmapError;
|
use crate::jmap::error::JmapError;
|
||||||
@@ -14,6 +13,7 @@ pub const SENTINEL_KEY: &str = "@blob";
|
|||||||
|
|
||||||
const DEFAULT_MEDIA_TYPE: &str = "application/octet-stream";
|
const DEFAULT_MEDIA_TYPE: &str = "application/octet-stream";
|
||||||
const MEDIA_TYPE_KEYS: [&str; 2] = ["mediaType", "contentType"];
|
const MEDIA_TYPE_KEYS: [&str; 2] = ["mediaType", "contentType"];
|
||||||
|
const DATA_URI_OVERHEAD: usize = "data:;base64,".len();
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum InlineShape {
|
pub enum InlineShape {
|
||||||
@@ -103,11 +103,13 @@ fn data_uri(map: &Map<String, Value>, bytes: &[u8]) -> String {
|
|||||||
.iter()
|
.iter()
|
||||||
.find_map(|k| map.get(*k).and_then(Value::as_str).and_then(uri_media_type))
|
.find_map(|k| map.get(*k).and_then(Value::as_str).and_then(uri_media_type))
|
||||||
.unwrap_or_else(|| DEFAULT_MEDIA_TYPE.to_owned());
|
.unwrap_or_else(|| DEFAULT_MEDIA_TYPE.to_owned());
|
||||||
let mut uri = String::with_capacity(13 + media_type.len() + bytes.len().div_ceil(3) * 4);
|
let mut uri = String::with_capacity(
|
||||||
|
DATA_URI_OVERHEAD + media_type.len() + STANDARD.encoded_len(bytes.len()),
|
||||||
|
);
|
||||||
uri.push_str("data:");
|
uri.push_str("data:");
|
||||||
uri.push_str(&media_type);
|
uri.push_str(&media_type);
|
||||||
uri.push_str(";base64,");
|
uri.push_str(";base64,");
|
||||||
STANDARD.encode_string(bytes, &mut uri);
|
STANDARD.encode_append(bytes, &mut uri);
|
||||||
uri
|
uri
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-3
@@ -9,8 +9,7 @@ use std::sync::OnceLock;
|
|||||||
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
|
||||||
use std::time::{Duration, Instant, SystemTime};
|
use std::time::{Duration, Instant, SystemTime};
|
||||||
|
|
||||||
use base64::Engine;
|
use encodify::base64::STANDARD;
|
||||||
use base64::engine::general_purpose::STANDARD;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use ureq::Agent;
|
use ureq::Agent;
|
||||||
use ureq::config::{Config, RedirectAuthHeaders};
|
use ureq::config::{Config, RedirectAuthHeaders};
|
||||||
@@ -27,6 +26,8 @@ const MAX_BODY: u64 = 512 * 1024 * 1024;
|
|||||||
|
|
||||||
const LONG_RETRY_THRESHOLD: Duration = Duration::from_secs(10);
|
const LONG_RETRY_THRESHOLD: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
const BASIC: &str = "Basic ";
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Auth {
|
pub enum Auth {
|
||||||
Basic { user: String, password: String },
|
Basic { user: String, password: String },
|
||||||
@@ -37,7 +38,12 @@ impl Auth {
|
|||||||
pub fn header_value(&self) -> String {
|
pub fn header_value(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Auth::Basic { user, password } => {
|
Auth::Basic { user, password } => {
|
||||||
format!("Basic {}", STANDARD.encode(format!("{user}:{password}")))
|
let credentials = format!("{user}:{password}");
|
||||||
|
let mut header =
|
||||||
|
String::with_capacity(BASIC.len() + STANDARD.encoded_len(credentials.len()));
|
||||||
|
header.push_str(BASIC);
|
||||||
|
STANDARD.encode_append(credentials, &mut header);
|
||||||
|
header
|
||||||
}
|
}
|
||||||
Auth::Bearer { token } => format!("Bearer {token}"),
|
Auth::Bearer { token } => format!("Bearer {token}"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
use std::io::{BufRead, BufReader, Read, Write};
|
use std::io::{BufRead, BufReader, Read, Write};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use base64::Engine;
|
use encodify::base64::STANDARD;
|
||||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
|
||||||
|
|
||||||
use crate::imap::transport::{Connector, ImapStream};
|
use crate::imap::transport::{Connector, ImapStream};
|
||||||
use crate::logging::Logger;
|
use crate::logging::Logger;
|
||||||
@@ -201,17 +200,17 @@ impl SieveClient {
|
|||||||
payload.extend_from_slice(authcid.as_bytes());
|
payload.extend_from_slice(authcid.as_bytes());
|
||||||
payload.push(0);
|
payload.push(0);
|
||||||
payload.extend_from_slice(password.as_bytes());
|
payload.extend_from_slice(password.as_bytes());
|
||||||
let encoded = BASE64.encode(&payload);
|
let encoded = STANDARD.encode(&payload);
|
||||||
self.send_authenticate("PLAIN", &encoded)
|
self.send_authenticate("PLAIN", &encoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn authenticate_login(&mut self, authcid: &str, password: &str) -> Result<(), SieveError> {
|
pub fn authenticate_login(&mut self, authcid: &str, password: &str) -> Result<(), SieveError> {
|
||||||
self.fresh_post_auth_caps = false;
|
self.fresh_post_auth_caps = false;
|
||||||
self.write_all(command::authenticate("LOGIN").as_bytes())?;
|
self.write_all(command::authenticate("LOGIN").as_bytes())?;
|
||||||
let user_payload = BASE64.encode(authcid.as_bytes());
|
let user_payload = STANDARD.encode(authcid);
|
||||||
self.expect_continuation()?;
|
self.expect_continuation()?;
|
||||||
self.write_all(command::continuation_payload(&user_payload).as_bytes())?;
|
self.write_all(command::continuation_payload(&user_payload).as_bytes())?;
|
||||||
let pass_payload = BASE64.encode(password.as_bytes());
|
let pass_payload = STANDARD.encode(password);
|
||||||
self.expect_continuation()?;
|
self.expect_continuation()?;
|
||||||
self.write_all(command::continuation_payload(&pass_payload).as_bytes())?;
|
self.write_all(command::continuation_payload(&pass_payload).as_bytes())?;
|
||||||
let block = read_response(&mut self.reader)?;
|
let block = read_response(&mut self.reader)?;
|
||||||
@@ -237,7 +236,7 @@ impl SieveClient {
|
|||||||
payload.push_str(token);
|
payload.push_str(token);
|
||||||
payload.push('\x01');
|
payload.push('\x01');
|
||||||
payload.push('\x01');
|
payload.push('\x01');
|
||||||
let encoded = BASE64.encode(payload.as_bytes());
|
let encoded = STANDARD.encode(&payload);
|
||||||
self.send_authenticate("OAUTHBEARER", &encoded)
|
self.send_authenticate("OAUTHBEARER", &encoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use base64::Engine;
|
use encodify::base64::LENIENT;
|
||||||
use base64::engine::general_purpose::STANDARD;
|
|
||||||
|
|
||||||
use crate::db::blobs;
|
use crate::db::blobs;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
@@ -56,13 +55,12 @@ pub fn fetch_attachments(
|
|||||||
let resp = ctx.client.call(ctx.url, "GetAttachment", &body)?;
|
let resp = ctx.client.call(ctx.url, "GetAttachment", &body)?;
|
||||||
let inline = parse_get_attachment_inline(&resp.body)?;
|
let inline = parse_get_attachment_inline(&resp.body)?;
|
||||||
for att in inline {
|
for att in inline {
|
||||||
let cleaned = strip_ascii_whitespace(att.content_base64.as_bytes());
|
let bytes = LENIENT.decode(&att.content_base64).map_err(|e| {
|
||||||
if cleaned.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let bytes = STANDARD.decode(&cleaned).map_err(|e| {
|
|
||||||
EwsError::Malformed(format!("attachment {}: base64: {e}", att.attachment_id))
|
EwsError::Malformed(format!("attachment {}: base64: {e}", att.attachment_id))
|
||||||
})?;
|
})?;
|
||||||
|
if bytes.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let media_type = att
|
let media_type = att
|
||||||
.content_type
|
.content_type
|
||||||
.unwrap_or_else(|| "application/octet-stream".to_owned());
|
.unwrap_or_else(|| "application/octet-stream".to_owned());
|
||||||
@@ -79,25 +77,3 @@ pub fn fetch_attachments(
|
|||||||
pub fn intern_attachment(conn: &rusqlite::Connection, bytes: &[u8]) -> Result<i64, Error> {
|
pub fn intern_attachment(conn: &rusqlite::Connection, bytes: &[u8]) -> Result<i64, Error> {
|
||||||
blobs::intern_blob(conn, bytes).map_err(|e| Error::Partial(e.to_string()))
|
blobs::intern_blob(conn, bytes).map_err(|e| Error::Partial(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strip_ascii_whitespace(input: &[u8]) -> Vec<u8> {
|
|
||||||
let mut out = Vec::with_capacity(input.len());
|
|
||||||
for b in input {
|
|
||||||
if !matches!(b, b' ' | b'\t' | b'\n' | b'\r') {
|
|
||||||
out.push(*b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::strip_ascii_whitespace;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn strips_whitespace_efficiently() {
|
|
||||||
let input = b"AB CD\nEF\tGH\r\nIJ";
|
|
||||||
let cleaned = strip_ascii_whitespace(input);
|
|
||||||
assert_eq!(cleaned, b"ABCDEFGHIJ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ fn resolve_well_known_roles(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let messages =
|
let messages =
|
||||||
parse_response_messages(&resp.body, b"GetFolderResponseMessage").unwrap_or_default();
|
parse_response_messages(&resp.body, "GetFolderResponseMessage").unwrap_or_default();
|
||||||
let mut map: HashMap<String, &'static str> = HashMap::new();
|
let mut map: HashMap<String, &'static str> = HashMap::new();
|
||||||
for (msg, (_, role)) in messages.into_iter().zip(WELL_KNOWN_ROLES.iter()) {
|
for (msg, (_, role)) in messages.into_iter().zip(WELL_KNOWN_ROLES.iter()) {
|
||||||
if !msg.success {
|
if !msg.success {
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ pub fn get_items(
|
|||||||
for chunk in ids.chunks(batch) {
|
for chunk in ids.chunks(batch) {
|
||||||
let body = get_item_body(shape, chunk, version);
|
let body = get_item_body(shape, chunk, version);
|
||||||
match ctx.client.call(ctx.url, "GetItem", &body) {
|
match ctx.client.call(ctx.url, "GetItem", &body) {
|
||||||
Ok(resp) => match parse_response_messages(&resp.body, b"GetItemResponseMessage") {
|
Ok(resp) => match parse_response_messages(&resp.body, "GetItemResponseMessage") {
|
||||||
Ok(mut msgs) => all.append(&mut msgs),
|
Ok(mut msgs) => all.append(&mut msgs),
|
||||||
Err(e) if is_per_batch_fault(&e) => {
|
Err(e) if is_per_batch_fault(&e) => {
|
||||||
ctx.logger.warn(&format!(
|
ctx.logger.warn(&format!(
|
||||||
@@ -331,7 +331,7 @@ pub fn get_items(
|
|||||||
let body = get_item_body(shape, &chunk, version);
|
let body = get_item_body(shape, &chunk, version);
|
||||||
let n = chunk.len();
|
let n = chunk.len();
|
||||||
let result = match client.call(&url, "GetItem", &body) {
|
let result = match client.call(&url, "GetItem", &body) {
|
||||||
Ok(resp) => parse_response_messages(&resp.body, b"GetItemResponseMessage"),
|
Ok(resp) => parse_response_messages(&resp.body, "GetItemResponseMessage"),
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
};
|
};
|
||||||
(n, result)
|
(n, result)
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use base64::Engine;
|
use encodify::base64::LENIENT;
|
||||||
use base64::engine::general_purpose::STANDARD;
|
|
||||||
use rusqlite::{Connection, params};
|
use rusqlite::{Connection, params};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
@@ -164,18 +163,15 @@ fn apply_message(
|
|||||||
existing_local_id: Option<i64>,
|
existing_local_id: Option<i64>,
|
||||||
counts: &mut TypeCounts,
|
counts: &mut TypeCounts,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mime_b64 = match item.mime_content.as_ref() {
|
let Some(mime_b64) = item.mime_content.as_ref() else {
|
||||||
Some(s) => s.replace(['\n', '\r', ' ', '\t'], ""),
|
counts.skipped += 1;
|
||||||
None => {
|
ctx.logger.warn(&format!(
|
||||||
counts.skipped += 1;
|
"message {} has no MimeContent; skipping",
|
||||||
ctx.logger.warn(&format!(
|
item.id.id
|
||||||
"message {} has no MimeContent; skipping",
|
));
|
||||||
item.id.id
|
return Ok(());
|
||||||
));
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let bytes = match STANDARD.decode(mime_b64.as_bytes()) {
|
let bytes = match LENIENT.decode(mime_b64) {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
counts.failed += 1;
|
counts.failed += 1;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use encodify::base64::STANDARD;
|
||||||
use rusqlite::{Connection, Transaction, params};
|
use rusqlite::{Connection, Transaction, params};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
@@ -32,7 +33,6 @@ fn fetch_event_attachments(
|
|||||||
endpoints: &crate::exchange_graph::api::Endpoints,
|
endpoints: &crate::exchange_graph::api::Endpoints,
|
||||||
event_id: &str,
|
event_id: &str,
|
||||||
) -> Vec<EventAttachment> {
|
) -> Vec<EventAttachment> {
|
||||||
use base64::Engine;
|
|
||||||
let url = endpoints.event_attachments(event_id);
|
let url = endpoints.event_attachments(event_id);
|
||||||
let Ok(body) = client.get_json_with_prefer(&url, &[]) else {
|
let Ok(body) = client.get_json_with_prefer(&url, &[]) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@@ -50,7 +50,7 @@ fn fetch_event_attachments(
|
|||||||
let Some(encoded) = item.get("contentBytes").and_then(Value::as_str) else {
|
let Some(encoded) = item.get("contentBytes").and_then(Value::as_str) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(encoded) else {
|
let Ok(bytes) = STANDARD.decode(encoded) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if bytes.is_empty() {
|
if bytes.is_empty() {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ use crate::error::Error;
|
|||||||
use crate::imap::client::{ConnectMode, ImapClient};
|
use crate::imap::client::{ConnectMode, ImapClient};
|
||||||
use crate::imap::command;
|
use crate::imap::command;
|
||||||
use crate::imap::error::ImapError;
|
use crate::imap::error::ImapError;
|
||||||
use crate::imap::name::{alternate_mailbox_name, encode_mailbox_name_with};
|
|
||||||
use crate::imap::response::{NamespaceEntry, Untagged};
|
use crate::imap::response::{NamespaceEntry, Untagged};
|
||||||
use crate::imap::retry::{
|
use crate::imap::retry::{
|
||||||
BackoffState, Disposition, RetryPolicy, classify, is_negotiation_failure,
|
BackoffState, Disposition, RetryPolicy, classify, is_negotiation_failure,
|
||||||
@@ -114,28 +113,9 @@ pub(super) fn control_run_collect(
|
|||||||
pub(super) fn select_folder(
|
pub(super) fn select_folder(
|
||||||
client: &mut ImapClient,
|
client: &mut ImapClient,
|
||||||
ctx: &ControlCtx,
|
ctx: &ControlCtx,
|
||||||
folder: &str,
|
folder: &ResolvedFolder,
|
||||||
) -> Result<crate::imap::CollectedResponse, Error> {
|
) -> Result<crate::imap::CollectedResponse, Error> {
|
||||||
let utf8_accept = client.utf8_accept();
|
control_run_collect(client, ctx, &command::select(&folder.wire_name))
|
||||||
let wire_name = encode_mailbox_name_with(folder, utf8_accept);
|
|
||||||
let first = control_run_collect(client, ctx, &command::select(&wire_name));
|
|
||||||
let Err(Error::Partial(_)) = &first else {
|
|
||||||
return first;
|
|
||||||
};
|
|
||||||
let Some(alternate) = alternate_mailbox_name(folder, utf8_accept) else {
|
|
||||||
return first;
|
|
||||||
};
|
|
||||||
match control_run_collect(client, ctx, &command::select(&alternate)) {
|
|
||||||
Ok(r) => {
|
|
||||||
log_at(
|
|
||||||
ctx.logger,
|
|
||||||
LEVEL_DEFAULT,
|
|
||||||
&format!("folder {folder:?}: selected with the alternate mailbox-name encoding"),
|
|
||||||
);
|
|
||||||
Ok(r)
|
|
||||||
}
|
|
||||||
Err(_) => first,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn call_with_retry<F, T>(
|
pub(super) fn call_with_retry<F, T>(
|
||||||
@@ -740,7 +720,7 @@ fn reconcile_folder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp = select_folder(client, control_ctx, &folder.name)?;
|
let resp = select_folder(client, control_ctx, folder)?;
|
||||||
let mut uidvalidity: u32 = 0;
|
let mut uidvalidity: u32 = 0;
|
||||||
let mut uidnext: u32 = 0;
|
let mut uidnext: u32 = 0;
|
||||||
for u in &resp.untagged {
|
for u in &resp.untagged {
|
||||||
@@ -826,6 +806,7 @@ fn reconcile_folder(
|
|||||||
for batch in &batches {
|
for batch in &batches {
|
||||||
pool.submit(FetchJob {
|
pool.submit(FetchJob {
|
||||||
folder: folder.name.clone(),
|
folder: folder.name.clone(),
|
||||||
|
wire_name: folder.wire_name.clone(),
|
||||||
uidvalidity,
|
uidvalidity,
|
||||||
uids: batch.to_vec(),
|
uids: batch.to_vec(),
|
||||||
});
|
});
|
||||||
@@ -1149,7 +1130,7 @@ fn dry_run_summary(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for folder in folders {
|
for folder in folders {
|
||||||
let select_resp = match select_folder(client, control_ctx, &folder.name) {
|
let select_resp = match select_folder(client, control_ctx, folder) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_at(
|
log_at(
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub struct FolderStatus {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DiscoveredFolder {
|
pub struct DiscoveredFolder {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub wire_name: String,
|
||||||
pub delimiter: Option<char>,
|
pub delimiter: Option<char>,
|
||||||
pub attributes: Vec<String>,
|
pub attributes: Vec<String>,
|
||||||
pub subscribed: bool,
|
pub subscribed: bool,
|
||||||
@@ -32,6 +33,7 @@ pub struct DiscoveredFolder {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ResolvedFolder {
|
pub struct ResolvedFolder {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub wire_name: String,
|
||||||
pub leaf: String,
|
pub leaf: String,
|
||||||
pub parent_path: Option<String>,
|
pub parent_path: Option<String>,
|
||||||
pub delimiter: Option<char>,
|
pub delimiter: Option<char>,
|
||||||
@@ -85,6 +87,7 @@ pub fn collect_from_list(
|
|||||||
canonical.clone(),
|
canonical.clone(),
|
||||||
DiscoveredFolder {
|
DiscoveredFolder {
|
||||||
name: canonical,
|
name: canonical,
|
||||||
|
wire_name: name.clone(),
|
||||||
delimiter: *delimiter,
|
delimiter: *delimiter,
|
||||||
attributes: attributes.clone(),
|
attributes: attributes.clone(),
|
||||||
subscribed: false,
|
subscribed: false,
|
||||||
@@ -156,6 +159,7 @@ pub fn apply_filters(
|
|||||||
let (leaf, parent_path) = split_parent(&f.name, delim);
|
let (leaf, parent_path) = split_parent(&f.name, delim);
|
||||||
resolved.push(ResolvedFolder {
|
resolved.push(ResolvedFolder {
|
||||||
name: f.name,
|
name: f.name,
|
||||||
|
wire_name: f.wire_name,
|
||||||
leaf,
|
leaf,
|
||||||
parent_path,
|
parent_path,
|
||||||
delimiter: delim,
|
delimiter: delim,
|
||||||
@@ -437,6 +441,7 @@ mod tests {
|
|||||||
let mut r = vec![
|
let mut r = vec![
|
||||||
ResolvedFolder {
|
ResolvedFolder {
|
||||||
name: "Projects/Alpha".into(),
|
name: "Projects/Alpha".into(),
|
||||||
|
wire_name: "Projects/Alpha".into(),
|
||||||
leaf: "Alpha".into(),
|
leaf: "Alpha".into(),
|
||||||
parent_path: Some("Projects".into()),
|
parent_path: Some("Projects".into()),
|
||||||
delimiter: Some('/'),
|
delimiter: Some('/'),
|
||||||
@@ -446,6 +451,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
ResolvedFolder {
|
ResolvedFolder {
|
||||||
name: "INBOX".into(),
|
name: "INBOX".into(),
|
||||||
|
wire_name: "INBOX".into(),
|
||||||
leaf: "INBOX".into(),
|
leaf: "INBOX".into(),
|
||||||
parent_path: None,
|
parent_path: None,
|
||||||
delimiter: Some('/'),
|
delimiter: Some('/'),
|
||||||
@@ -455,6 +461,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
ResolvedFolder {
|
ResolvedFolder {
|
||||||
name: "Projects".into(),
|
name: "Projects".into(),
|
||||||
|
wire_name: "Projects".into(),
|
||||||
leaf: "Projects".into(),
|
leaf: "Projects".into(),
|
||||||
parent_path: None,
|
parent_path: None,
|
||||||
delimiter: Some('/'),
|
delimiter: Some('/'),
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ use crossbeam_channel::{Receiver, Sender, unbounded};
|
|||||||
use crate::imap::client::{ConnectMode, ImapClient};
|
use crate::imap::client::{ConnectMode, ImapClient};
|
||||||
use crate::imap::command;
|
use crate::imap::command;
|
||||||
use crate::imap::error::ImapError;
|
use crate::imap::error::ImapError;
|
||||||
use crate::imap::name::{alternate_mailbox_name, encode_mailbox_name_with};
|
|
||||||
use crate::imap::response::Untagged;
|
use crate::imap::response::Untagged;
|
||||||
use crate::imap::retry::{BackoffState, Disposition, RetryPolicy, classify};
|
use crate::imap::retry::{BackoffState, Disposition, RetryPolicy, classify};
|
||||||
use crate::imap::transport::Connector;
|
use crate::imap::transport::Connector;
|
||||||
@@ -26,6 +25,7 @@ pub const HARD_CAP: usize = 8;
|
|||||||
|
|
||||||
pub struct FetchJob {
|
pub struct FetchJob {
|
||||||
pub folder: String,
|
pub folder: String,
|
||||||
|
pub wire_name: String,
|
||||||
pub uidvalidity: u32,
|
pub uidvalidity: u32,
|
||||||
pub uids: Vec<u32>,
|
pub uids: Vec<u32>,
|
||||||
}
|
}
|
||||||
@@ -231,16 +231,7 @@ fn run_one_job(
|
|||||||
event_tx: &Sender<FetchEvent>,
|
event_tx: &Sender<FetchEvent>,
|
||||||
) -> Result<(), ImapError> {
|
) -> Result<(), ImapError> {
|
||||||
if current_folder.as_deref() != Some(job.folder.as_str()) {
|
if current_folder.as_deref() != Some(job.folder.as_str()) {
|
||||||
let utf8_accept = client.utf8_accept();
|
client.run_collect(&command::select(&job.wire_name))?;
|
||||||
let wire = encode_mailbox_name_with(&job.folder, utf8_accept);
|
|
||||||
if let Err(e) = client.run_collect(&command::select(&wire)) {
|
|
||||||
match alternate_mailbox_name(&job.folder, utf8_accept) {
|
|
||||||
Some(alternate) if matches!(e, ImapError::No(_)) => {
|
|
||||||
client.run_collect(&command::select(&alternate))?;
|
|
||||||
}
|
|
||||||
_ => return Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*current_folder = Some(job.folder.clone());
|
*current_folder = Some(job.folder.clone());
|
||||||
}
|
}
|
||||||
let set = command::format_uid_set(&job.uids, true);
|
let set = command::format_uid_set(&job.uids, true);
|
||||||
|
|||||||
@@ -897,6 +897,7 @@ fn opt_format_utc(value: &Option<time::OffsetDateTime>) -> Result<Option<String>
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::db::init;
|
use crate::db::init;
|
||||||
|
use encodify::base64::STANDARD;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
struct MapResolver {
|
struct MapResolver {
|
||||||
@@ -929,15 +930,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn decode_data_uri(value: &Value, media_type: &str) -> Vec<u8> {
|
fn decode_data_uri(value: &Value, media_type: &str) -> Vec<u8> {
|
||||||
use base64::Engine;
|
|
||||||
let uri = value.as_str().unwrap_or_else(|| panic!("{value} is a URI"));
|
let uri = value.as_str().unwrap_or_else(|| panic!("{value} is a URI"));
|
||||||
let prefix = format!("data:{media_type};base64,");
|
let prefix = format!("data:{media_type};base64,");
|
||||||
let payload = uri
|
let payload = uri
|
||||||
.strip_prefix(&prefix)
|
.strip_prefix(&prefix)
|
||||||
.unwrap_or_else(|| panic!("{uri} does not start with {prefix}"));
|
.unwrap_or_else(|| panic!("{uri} does not start with {prefix}"));
|
||||||
base64::engine::general_purpose::STANDARD
|
STANDARD.decode(payload).expect("base64 payload")
|
||||||
.decode(payload)
|
|
||||||
.expect("base64 payload")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_no_blob_id(value: &Value) {
|
fn assert_no_blob_id(value: &Value) {
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use base64::Engine;
|
use encodify::base64::STANDARD as B64;
|
||||||
use base64::engine::general_purpose::STANDARD as B64;
|
|
||||||
use ureq::Agent;
|
use ureq::Agent;
|
||||||
use ureq::http::{Method, Request};
|
use ureq::http::{Method, Request};
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ service managesieve-login {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protocol imap {
|
||||||
|
mail_plugins = $mail_plugins imap_sieve
|
||||||
|
}
|
||||||
|
|
||||||
protocol sieve {
|
protocol sieve {
|
||||||
managesieve_max_line_length = 1M
|
managesieve_max_line_length = 1M
|
||||||
managesieve_max_compile_errors = 5
|
managesieve_max_compile_errors = 5
|
||||||
@@ -96,6 +100,7 @@ info_log_path = /dev/stderr
|
|||||||
plugin {
|
plugin {
|
||||||
sieve = file:~/sieve;active=~/.dovecot.sieve
|
sieve = file:~/sieve;active=~/.dovecot.sieve
|
||||||
sieve_max_script_size = 1M
|
sieve_max_script_size = 1M
|
||||||
|
imapsieve_url = sieve://127.0.0.1:4190
|
||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
@@ -139,6 +144,7 @@ impl Dovecot {
|
|||||||
let host = container.get_host()?.to_string();
|
let host = container.get_host()?.to_string();
|
||||||
let imap = Endpoint::new(host.clone(), container.get_host_port_ipv4(IMAP_PORT.tcp())?);
|
let imap = Endpoint::new(host.clone(), container.get_host_port_ipv4(IMAP_PORT.tcp())?);
|
||||||
let sieve = Endpoint::new(host, container.get_host_port_ipv4(SIEVE_PORT.tcp())?);
|
let sieve = Endpoint::new(host, container.get_host_port_ipv4(SIEVE_PORT.tcp())?);
|
||||||
|
ImapSeed::connect(&imap.host, imap.port)?.logout()?;
|
||||||
|
|
||||||
let accounts: Vec<Account> = layouts::accounts()
|
let accounts: Vec<Account> = layouts::accounts()
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ use std::io::{BufRead, BufReader, Read, Write};
|
|||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use base64::Engine;
|
use encodify::base64::STANDARD as B64;
|
||||||
use base64::engine::general_purpose::STANDARD as B64;
|
|
||||||
|
|
||||||
use super::error::{ContainerError, ContainerResult};
|
use super::error::{ContainerError, ContainerResult};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use std::process::Command;
|
|||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use base64::Engine;
|
use encodify::base64::STANDARD;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use testcontainers::core::{IntoContainerPort, WaitFor};
|
use testcontainers::core::{IntoContainerPort, WaitFor};
|
||||||
use testcontainers::runners::SyncRunner;
|
use testcontainers::runners::SyncRunner;
|
||||||
@@ -74,7 +74,7 @@ impl Stalwart {
|
|||||||
.with_mapped_port(sieve_port, SIEVE_PORT.tcp())
|
.with_mapped_port(sieve_port, SIEVE_PORT.tcp())
|
||||||
.with_startup_timeout(Duration::from_secs(180));
|
.with_startup_timeout(Duration::from_secs(180));
|
||||||
|
|
||||||
let container = request.start()?;
|
let container = request.pull_image()?.start()?;
|
||||||
|
|
||||||
let me = Self {
|
let me = Self {
|
||||||
_container: container,
|
_container: container,
|
||||||
@@ -183,8 +183,7 @@ fn basic(user: &str, password: &str) -> String {
|
|||||||
raw.push_str(user);
|
raw.push_str(user);
|
||||||
raw.push(':');
|
raw.push(':');
|
||||||
raw.push_str(password);
|
raw.push_str(password);
|
||||||
format!(
|
let mut header = String::from("Basic ");
|
||||||
"Basic {}",
|
STANDARD.encode_append(raw, &mut header);
|
||||||
base64::engine::general_purpose::STANDARD.encode(raw.as_bytes())
|
header
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use base64::Engine;
|
use encodify::base64::STANDARD as B64;
|
||||||
use base64::engine::general_purpose::STANDARD as B64;
|
|
||||||
use testcontainers::core::{IntoContainerPort, WaitFor};
|
use testcontainers::core::{IntoContainerPort, WaitFor};
|
||||||
use testcontainers::runners::{SyncBuilder, SyncRunner};
|
use testcontainers::runners::{SyncBuilder, SyncRunner};
|
||||||
use testcontainers::{Container, GenericBuildableImage, GenericImage, ImageExt};
|
use testcontainers::{Container, GenericBuildableImage, GenericImage, ImageExt};
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ use integration::validate::{
|
|||||||
|
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use vandelay::error::Error;
|
use vandelay::error::Error;
|
||||||
|
use vandelay::imap::client::{ConnectMode, ImapClient};
|
||||||
|
use vandelay::imap::transport::Connector;
|
||||||
|
use vandelay::logging::Logger;
|
||||||
use vandelay::sync::import_imap::{ImapAuth, ImapImportConfig};
|
use vandelay::sync::import_imap::{ImapAuth, ImapImportConfig};
|
||||||
use vandelay::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig};
|
use vandelay::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig};
|
||||||
use vandelay::sync::{import_imap, import_managesieve};
|
use vandelay::sync::{import_imap, import_managesieve};
|
||||||
@@ -561,3 +564,28 @@ fn dovecot_non_ascii_mailbox_names_round_trip() {
|
|||||||
);
|
);
|
||||||
cleanup(&archive);
|
cleanup(&archive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires Docker"]
|
||||||
|
fn dovecot_post_login_capability_with_imapsieve_url_is_parsed() {
|
||||||
|
let d = Dovecot::start().expect("dovecot start");
|
||||||
|
let account = d.accounts.first().expect("account");
|
||||||
|
let connector = Connector::new(true).expect("connector");
|
||||||
|
let mut client = ImapClient::connect(
|
||||||
|
&connector,
|
||||||
|
&d.imap.host,
|
||||||
|
d.imap.port,
|
||||||
|
ConnectMode::StartTls,
|
||||||
|
Logger::from_flags(false, 0),
|
||||||
|
)
|
||||||
|
.expect("connect");
|
||||||
|
assert!(client.has_capability("SASL-IR"));
|
||||||
|
client
|
||||||
|
.authenticate_plain(&account.username, &account.password)
|
||||||
|
.expect("AUTHENTICATE PLAIN must survive the untagged post-login CAPABILITY");
|
||||||
|
assert!(
|
||||||
|
client.has_capability("IMAPSIEVE=sieve://127.0.0.1:4190"),
|
||||||
|
"post-login capabilities must carry the IMAPSIEVE URL: {:?}",
|
||||||
|
client.capabilities
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
+1
-2
@@ -1448,8 +1448,7 @@ fn dry_run_writes_nothing_but_emits_per_collection_counts() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dav_source_change_protection_fires_across_users_on_same_root() {
|
fn dav_source_change_protection_fires_across_users_on_same_root() {
|
||||||
use base64::Engine;
|
use encodify::base64::STANDARD;
|
||||||
use base64::engine::general_purpose::STANDARD;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use vandelay::logging::Logger;
|
use vandelay::logging::Logger;
|
||||||
use vandelay::sync::CommonConfig;
|
use vandelay::sync::CommonConfig;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use encodify::base64::{LENIENT, STANDARD};
|
||||||
use mockito::Matcher;
|
use mockito::Matcher;
|
||||||
use vandelay::exchange_ews::EwsClient;
|
use vandelay::exchange_ews::EwsClient;
|
||||||
use vandelay::exchange_ews::autodiscover::{DiscoverySource, discover};
|
use vandelay::exchange_ews::autodiscover::{DiscoverySource, discover};
|
||||||
@@ -220,7 +221,7 @@ fn get_item_mixed_success_and_per_item_error() {
|
|||||||
</m:GetItemResponseMessage>\
|
</m:GetItemResponseMessage>\
|
||||||
</m:ResponseMessages></m:GetItemResponse>"
|
</m:ResponseMessages></m:GetItemResponse>"
|
||||||
));
|
));
|
||||||
let r = parse_response_messages(body.as_bytes(), b"GetItemResponseMessage").unwrap();
|
let r = parse_response_messages(body.as_bytes(), "GetItemResponseMessage").unwrap();
|
||||||
assert_eq!(r.len(), 2);
|
assert_eq!(r.len(), 2);
|
||||||
assert!(r[0].success);
|
assert!(r[0].success);
|
||||||
assert!(r[0].inner_xml.contains("SGVsbG8="));
|
assert!(r[0].inner_xml.contains("SGVsbG8="));
|
||||||
@@ -300,8 +301,7 @@ fn http_401_surfaces_as_auth_error() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn mime_content_round_trips_through_base64_decode() {
|
fn mime_content_round_trips_through_base64_decode() {
|
||||||
let original = b"From: alice@x\r\nSubject: hi\r\n\r\nbody";
|
let original = b"From: alice@x\r\nSubject: hi\r\n\r\nbody";
|
||||||
use base64::Engine;
|
let encoded = STANDARD.encode(original);
|
||||||
let encoded = base64::engine::general_purpose::STANDARD.encode(original);
|
|
||||||
let body = envelope(&format!(
|
let body = envelope(&format!(
|
||||||
"<m:GetItemResponse{NS}><m:ResponseMessages>\
|
"<m:GetItemResponse{NS}><m:ResponseMessages>\
|
||||||
<m:GetItemResponseMessage ResponseClass=\"Success\">\
|
<m:GetItemResponseMessage ResponseClass=\"Success\">\
|
||||||
@@ -312,13 +312,10 @@ fn mime_content_round_trips_through_base64_decode() {
|
|||||||
</t:Message></m:Items></m:GetItemResponseMessage>\
|
</t:Message></m:Items></m:GetItemResponseMessage>\
|
||||||
</m:ResponseMessages></m:GetItemResponse>"
|
</m:ResponseMessages></m:GetItemResponse>"
|
||||||
));
|
));
|
||||||
let r = parse_response_messages(body.as_bytes(), b"GetItemResponseMessage").unwrap();
|
let r = parse_response_messages(body.as_bytes(), "GetItemResponseMessage").unwrap();
|
||||||
let item = vandelay::exchange_ews::parse::parse_message_item(&r[0].inner_xml).unwrap();
|
let item = vandelay::exchange_ews::parse::parse_message_item(&r[0].inner_xml).unwrap();
|
||||||
let s = item.mime_content.unwrap();
|
let s = item.mime_content.unwrap();
|
||||||
let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect();
|
let bytes = LENIENT.decode(&s).unwrap();
|
||||||
let bytes = base64::engine::general_purpose::STANDARD
|
|
||||||
.decode(cleaned.as_bytes())
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(bytes, original);
|
assert_eq!(bytes, original);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,7 +890,7 @@ fn warning_response_class_is_treated_as_success_in_mock() {
|
|||||||
<m:Items><t:Message><t:ItemId Id=\"W1\" ChangeKey=\"K\"/></t:Message></m:Items>\
|
<m:Items><t:Message><t:ItemId Id=\"W1\" ChangeKey=\"K\"/></t:Message></m:Items>\
|
||||||
</m:GetItemResponseMessage></m:ResponseMessages></m:GetItemResponse>"
|
</m:GetItemResponseMessage></m:ResponseMessages></m:GetItemResponse>"
|
||||||
));
|
));
|
||||||
let r = parse_response_messages(body.as_bytes(), b"GetItemResponseMessage").unwrap();
|
let r = parse_response_messages(body.as_bytes(), "GetItemResponseMessage").unwrap();
|
||||||
assert!(r[0].success);
|
assert!(r[0].success);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -933,7 +930,7 @@ fn get_folder_messages_preserve_position_when_one_errors() {
|
|||||||
</m:GetFolderResponseMessage>\
|
</m:GetFolderResponseMessage>\
|
||||||
</m:ResponseMessages></m:GetFolderResponse>"
|
</m:ResponseMessages></m:GetFolderResponse>"
|
||||||
));
|
));
|
||||||
let msgs = parse_response_messages(body.as_bytes(), b"GetFolderResponseMessage").unwrap();
|
let msgs = parse_response_messages(body.as_bytes(), "GetFolderResponseMessage").unwrap();
|
||||||
assert_eq!(msgs.len(), 3, "all three messages must be present");
|
assert_eq!(msgs.len(), 3, "all three messages must be present");
|
||||||
assert!(msgs[0].success);
|
assert!(msgs[0].success);
|
||||||
assert!(!msgs[1].success);
|
assert!(!msgs[1].success);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
use std::sync::Once;
|
use std::sync::Once;
|
||||||
|
|
||||||
use base64::Engine;
|
use encodify::base64::URL_SAFE_NO_PAD;
|
||||||
use mockito::{Matcher, Server};
|
use mockito::{Matcher, Server};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use vandelay::exchange_graph::api::{Endpoints, collect_all_ids, paged_collect};
|
use vandelay::exchange_graph::api::{Endpoints, collect_all_ids, paged_collect};
|
||||||
@@ -36,7 +36,6 @@ fn url_message_collection(server_url: &str, folder: &str, top: usize) -> String
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn make_jwt(exp: u64, upn: &str) -> String {
|
fn make_jwt(exp: u64, upn: &str) -> String {
|
||||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
|
||||||
let header = URL_SAFE_NO_PAD.encode(b"{\"alg\":\"none\"}");
|
let header = URL_SAFE_NO_PAD.encode(b"{\"alg\":\"none\"}");
|
||||||
let claims = format!(r#"{{"tid":"tenant-1","upn":"{upn}","exp":{exp}}}"#);
|
let claims = format!(r#"{{"tid":"tenant-1","upn":"{upn}","exp":{exp}}}"#);
|
||||||
let payload = URL_SAFE_NO_PAD.encode(claims.as_bytes());
|
let payload = URL_SAFE_NO_PAD.encode(claims.as_bytes());
|
||||||
|
|||||||
+111
-36
@@ -12,6 +12,7 @@ use std::sync::atomic::{AtomicU32, Ordering};
|
|||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use encodify::base64::STANDARD;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use vandelay::db;
|
use vandelay::db;
|
||||||
use vandelay::imap::client::{ConnectMode, ImapClient};
|
use vandelay::imap::client::{ConnectMode, ImapClient};
|
||||||
@@ -230,7 +231,6 @@ fn authenticate_oauthbearer_sasl_ir() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn authenticate_oauthbearer_continuation_payload_uses_gs2_header() {
|
fn authenticate_oauthbearer_continuation_payload_uses_gs2_header() {
|
||||||
use base64::Engine;
|
|
||||||
let server = MockImap::start(|conn| {
|
let server = MockImap::start(|conn| {
|
||||||
conn.write_line("* OK Hello")?;
|
conn.write_line("* OK Hello")?;
|
||||||
let (tag, _) = conn.read_command()?;
|
let (tag, _) = conn.read_command()?;
|
||||||
@@ -242,9 +242,7 @@ fn authenticate_oauthbearer_continuation_payload_uses_gs2_header() {
|
|||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
conn.reader.read_line(&mut line)?;
|
conn.reader.read_line(&mut line)?;
|
||||||
let payload = line.trim_end_matches(['\r', '\n']).to_owned();
|
let payload = line.trim_end_matches(['\r', '\n']).to_owned();
|
||||||
let decoded = base64::engine::general_purpose::STANDARD
|
let decoded = STANDARD.decode(&payload).expect("base64");
|
||||||
.decode(&payload)
|
|
||||||
.expect("base64");
|
|
||||||
let text = std::str::from_utf8(&decoded).unwrap_or("");
|
let text = std::str::from_utf8(&decoded).unwrap_or("");
|
||||||
assert!(
|
assert!(
|
||||||
text.starts_with("n,[email protected],"),
|
text.starts_with("n,[email protected],"),
|
||||||
@@ -489,26 +487,35 @@ fn write_fetch_message(
|
|||||||
conn.write_raw(b")\r\n")
|
conn.write_raw(b")\r\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serve_one_folder(
|
||||||
|
conn: &mut MockConn,
|
||||||
|
uidvalidity: u32,
|
||||||
|
uidnext: u32,
|
||||||
|
uids: &[u32],
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let (tag, cmd) = conn.read_command()?;
|
||||||
|
assert_eq!(cmd, "LIST \"\" \"*\"");
|
||||||
|
conn.write_line("* LIST () \"/\" \"INBOX\"")?;
|
||||||
|
conn.write_line(&format!("{tag} OK LIST done"))?;
|
||||||
|
let (tag, cmd) = conn.read_command()?;
|
||||||
|
assert_eq!(cmd, "LSUB \"\" \"*\"");
|
||||||
|
conn.write_line(&format!("{tag} OK LSUB done"))?;
|
||||||
|
let (tag, cmd) = conn.read_command()?;
|
||||||
|
assert_eq!(cmd, "SELECT \"INBOX\"");
|
||||||
|
write_select(conn, &tag, uidvalidity, uidnext, uids.len() as u32)?;
|
||||||
|
let (tag, cmd) = conn.read_command()?;
|
||||||
|
assert_eq!(cmd, "UID SEARCH ALL");
|
||||||
|
let uid_strs: Vec<String> = uids.iter().map(|u| u.to_string()).collect();
|
||||||
|
conn.write_line(&format!("* SEARCH {}", uid_strs.join(" ")))?;
|
||||||
|
conn.write_line(&format!("{tag} OK SEARCH done"))?;
|
||||||
|
drain_until_close(conn);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn control_script_one_folder(uidvalidity: u32, uidnext: u32, uids: &'static [u32]) -> Script {
|
fn control_script_one_folder(uidvalidity: u32, uidnext: u32, uids: &'static [u32]) -> Script {
|
||||||
Box::new(move |conn: &mut MockConn| -> std::io::Result<()> {
|
Box::new(move |conn: &mut MockConn| -> std::io::Result<()> {
|
||||||
auth_preamble(conn, "IMAP4rev2 LITERAL+ AUTH=PLAIN")?;
|
auth_preamble(conn, "IMAP4rev2 LITERAL+ AUTH=PLAIN")?;
|
||||||
let (tag, cmd) = conn.read_command()?;
|
serve_one_folder(conn, uidvalidity, uidnext, uids)
|
||||||
assert_eq!(cmd, "LIST \"\" \"*\"");
|
|
||||||
conn.write_line("* LIST () \"/\" \"INBOX\"")?;
|
|
||||||
conn.write_line(&format!("{tag} OK LIST done"))?;
|
|
||||||
let (tag, cmd) = conn.read_command()?;
|
|
||||||
assert_eq!(cmd, "LSUB \"\" \"*\"");
|
|
||||||
conn.write_line(&format!("{tag} OK LSUB done"))?;
|
|
||||||
let (tag, cmd) = conn.read_command()?;
|
|
||||||
assert_eq!(cmd, "SELECT \"INBOX\"");
|
|
||||||
write_select(conn, &tag, uidvalidity, uidnext, uids.len() as u32)?;
|
|
||||||
let (tag, cmd) = conn.read_command()?;
|
|
||||||
assert_eq!(cmd, "UID SEARCH ALL");
|
|
||||||
let uid_strs: Vec<String> = uids.iter().map(|u| u.to_string()).collect();
|
|
||||||
conn.write_line(&format!("* SEARCH {}", uid_strs.join(" ")))?;
|
|
||||||
conn.write_line(&format!("{tag} OK SEARCH done"))?;
|
|
||||||
drain_until_close(conn);
|
|
||||||
Ok(())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,6 +592,63 @@ fn coordinator_imports_one_folder_one_message() {
|
|||||||
assert_eq!(folder_role(&conn, "INBOX"), Some("inbox".to_owned()));
|
assert_eq!(folder_role(&conn, "INBOX"), Some("inbox".to_owned()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DOVECOT_PRE_LOGIN_CAPS: &str =
|
||||||
|
"IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE LITERAL+ AUTH=PLAIN AUTH=LOGIN";
|
||||||
|
const DOVECOT_POST_LOGIN_CAPS: &str = "IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT \
|
||||||
|
UIDPLUS LITERAL+ NOTIFY IMAPSIEVE=sieve://127.0.0.1:4190 \
|
||||||
|
QUOTA ACL RIGHTS=texk";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coordinator_accepts_dovecot_post_login_capability_with_imapsieve_url() {
|
||||||
|
let control: Script = Box::new(|conn: &mut MockConn| -> std::io::Result<()> {
|
||||||
|
conn.write_line(&format!(
|
||||||
|
"* OK [CAPABILITY {DOVECOT_PRE_LOGIN_CAPS}] Dovecot (Debian) ready."
|
||||||
|
))?;
|
||||||
|
let (tag, cmd) = conn.read_command()?;
|
||||||
|
assert_eq!(cmd, "CAPABILITY");
|
||||||
|
write_capability(conn, DOVECOT_PRE_LOGIN_CAPS)?;
|
||||||
|
conn.write_line(&format!(
|
||||||
|
"{tag} OK Pre-login capabilities listed, post-login capabilities have more."
|
||||||
|
))?;
|
||||||
|
let (tag, cmd) = conn.read_command()?;
|
||||||
|
assert!(
|
||||||
|
cmd.starts_with("AUTHENTICATE PLAIN "),
|
||||||
|
"expected SASL-IR form, got {cmd}"
|
||||||
|
);
|
||||||
|
write_capability(conn, DOVECOT_POST_LOGIN_CAPS)?;
|
||||||
|
conn.write_line(&format!(
|
||||||
|
"{tag} OK [CAPABILITY {DOVECOT_POST_LOGIN_CAPS}] Logged in"
|
||||||
|
))?;
|
||||||
|
let (tag, cmd) = conn.read_command()?;
|
||||||
|
assert_eq!(cmd, "CAPABILITY");
|
||||||
|
write_capability(conn, DOVECOT_POST_LOGIN_CAPS)?;
|
||||||
|
conn.write_line(&format!("{tag} OK Capability completed."))?;
|
||||||
|
serve_one_folder(conn, 12345, 2, &[1])
|
||||||
|
});
|
||||||
|
let server = MockImap::start_scripts(vec![
|
||||||
|
control,
|
||||||
|
worker_fetch_script(
|
||||||
|
"IMAP4rev1 SASL-IR LITERAL+ AUTH=PLAIN IMAPSIEVE=sieve://127.0.0.1:4190",
|
||||||
|
"INBOX",
|
||||||
|
12345,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
vec![(1, 1, MSG_BODY)],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
let archive = tempfile("dovecot-imapsieve");
|
||||||
|
let summary = run_import(&server, "alice", archive.clone(), |_| {}).expect("import");
|
||||||
|
let email = summary
|
||||||
|
.per_type
|
||||||
|
.iter()
|
||||||
|
.find(|(k, _)| *k == "email")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(email.1.created, 1, "summary={summary:?}");
|
||||||
|
let conn = Connection::open(&archive).unwrap();
|
||||||
|
db::init::apply_schema(&conn).unwrap();
|
||||||
|
assert_eq!(count(&conn, "emails"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn coordinator_uses_esearch_when_advertised() {
|
fn coordinator_uses_esearch_when_advertised() {
|
||||||
let control: Script = Box::new(|conn: &mut MockConn| -> std::io::Result<()> {
|
let control: Script = Box::new(|conn: &mut MockConn| -> std::io::Result<()> {
|
||||||
@@ -1871,22 +1935,33 @@ fn mutf7_server_gets_the_folder_name_back_as_mutf7() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn utf8_name_from_a_server_that_never_enabled_utf8_falls_back_on_select() {
|
fn utf8_name_from_a_server_that_never_enabled_utf8_is_selected_as_listed() {
|
||||||
let control: Script = Box::new(|conn: &mut MockConn| -> std::io::Result<()> {
|
assert_name_selected_as_listed(FRENCH_SENT_UTF8, FRENCH_SENT_UTF8, "utf8_no_enable");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn raw_ampersand_name_is_selected_as_listed_not_reencoded() {
|
||||||
|
assert_name_selected_as_listed("R&D", "R&D", "raw_ampersand");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modified_utf7_name_is_selected_as_listed() {
|
||||||
|
assert_name_selected_as_listed(FRENCH_SENT_MUTF7, FRENCH_SENT_UTF8, "mutf7_as_listed");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_name_selected_as_listed(listed: &'static str, stored: &str, archive_name: &str) {
|
||||||
|
let control: Script = Box::new(move |conn: &mut MockConn| -> std::io::Result<()> {
|
||||||
auth_preamble(conn, "IMAP4rev2 LITERAL+ AUTH=PLAIN")?;
|
auth_preamble(conn, "IMAP4rev2 LITERAL+ AUTH=PLAIN")?;
|
||||||
let (tag, cmd) = conn.read_command()?;
|
let (tag, cmd) = conn.read_command()?;
|
||||||
assert_eq!(cmd, "LIST \"\" \"*\"");
|
assert_eq!(cmd, "LIST \"\" \"*\"");
|
||||||
conn.write_line(&format!("* LIST () \"/\" \"{FRENCH_SENT_UTF8}\""))?;
|
conn.write_line(&format!("* LIST () \"/\" \"{listed}\""))?;
|
||||||
conn.write_line(&format!("{tag} OK LIST done"))?;
|
conn.write_line(&format!("{tag} OK LIST done"))?;
|
||||||
let (tag, _) = conn.read_command()?;
|
let (tag, _) = conn.read_command()?;
|
||||||
conn.write_line(&format!("{tag} OK LSUB done"))?;
|
conn.write_line(&format!("{tag} OK LSUB done"))?;
|
||||||
let (tag, name) = read_select_mailbox(conn)?;
|
let (tag, name) = read_select_mailbox(conn)?;
|
||||||
assert_eq!(name, FRENCH_SENT_MUTF7);
|
|
||||||
conn.write_line(&format!("{tag} NO [NONEXISTENT] Mailbox does not exist."))?;
|
|
||||||
let (tag, name) = read_select_mailbox(conn)?;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
name, FRENCH_SENT_UTF8,
|
name, listed,
|
||||||
"a refused modified UTF-7 name must be retried as UTF-8"
|
"the name must go back exactly as the server listed it"
|
||||||
);
|
);
|
||||||
write_select(conn, &tag, 902, 2, 1)?;
|
write_select(conn, &tag, 902, 2, 1)?;
|
||||||
let (tag, cmd) = conn.read_command()?;
|
let (tag, cmd) = conn.read_command()?;
|
||||||
@@ -1896,13 +1971,13 @@ fn utf8_name_from_a_server_that_never_enabled_utf8_falls_back_on_select() {
|
|||||||
drain_until_close(conn);
|
drain_until_close(conn);
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
let worker: Script = Box::new(|conn: &mut MockConn| -> std::io::Result<()> {
|
let worker: Script = Box::new(move |conn: &mut MockConn| -> std::io::Result<()> {
|
||||||
auth_preamble(conn, "IMAP4rev2 LITERAL+ AUTH=PLAIN")?;
|
auth_preamble(conn, "IMAP4rev2 LITERAL+ AUTH=PLAIN")?;
|
||||||
let (tag, name) = read_select_mailbox(conn)?;
|
let (tag, name) = read_select_mailbox(conn)?;
|
||||||
assert_eq!(name, FRENCH_SENT_MUTF7);
|
assert_eq!(
|
||||||
conn.write_line(&format!("{tag} NO [NONEXISTENT] Mailbox does not exist."))?;
|
name, listed,
|
||||||
let (tag, name) = read_select_mailbox(conn)?;
|
"the fetch worker must agree with the coordinator"
|
||||||
assert_eq!(name, FRENCH_SENT_UTF8);
|
);
|
||||||
write_select(conn, &tag, 902, 2, 1)?;
|
write_select(conn, &tag, 902, 2, 1)?;
|
||||||
let (tag, _) = conn.read_command()?;
|
let (tag, _) = conn.read_command()?;
|
||||||
write_fetch_message(conn, 1, 1, MSG_BODY)?;
|
write_fetch_message(conn, 1, 1, MSG_BODY)?;
|
||||||
@@ -1912,7 +1987,7 @@ fn utf8_name_from_a_server_that_never_enabled_utf8_falls_back_on_select() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let server = MockImap::start_scripts(vec![control, worker]);
|
let server = MockImap::start_scripts(vec![control, worker]);
|
||||||
let archive = tempfile("utf8_no_enable");
|
let archive = tempfile(archive_name);
|
||||||
let summary = run_import(&server, "alice", archive.clone(), |_| {}).expect("import");
|
let summary = run_import(&server, "alice", archive.clone(), |_| {}).expect("import");
|
||||||
let email = summary
|
let email = summary
|
||||||
.per_type
|
.per_type
|
||||||
@@ -1925,7 +2000,7 @@ fn utf8_name_from_a_server_that_never_enabled_utf8_falls_back_on_select() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
conn.query_row::<String, _, _>("SELECT name FROM mailboxes", [], |r| r.get(0))
|
conn.query_row::<String, _, _>("SELECT name FROM mailboxes", [], |r| r.get(0))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
FRENCH_SENT_UTF8
|
stored
|
||||||
);
|
);
|
||||||
let _ = std::fs::remove_file(&archive);
|
let _ = std::fs::remove_file(&archive);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ impl Admin {
|
|||||||
"encryptionAtRest": { "@type": "Disabled" },
|
"encryptionAtRest": { "@type": "Disabled" },
|
||||||
"permissions": { "@type": "Inherit" },
|
"permissions": { "@type": "Inherit" },
|
||||||
"roles": { "@type": role },
|
"roles": { "@type": role },
|
||||||
"locale": "en_US"
|
"locale": "en-US"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let response =
|
let response =
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use base64::Engine;
|
use encodify::base64::STANDARD;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use ureq::Agent;
|
use ureq::Agent;
|
||||||
use ureq::config::RedirectAuthHeaders;
|
use ureq::config::RedirectAuthHeaders;
|
||||||
@@ -43,10 +43,9 @@ fn basic(user: &str, password: &str) -> String {
|
|||||||
raw.push_str(user);
|
raw.push_str(user);
|
||||||
raw.push(':');
|
raw.push(':');
|
||||||
raw.push_str(password);
|
raw.push_str(password);
|
||||||
format!(
|
let mut header = String::from("Basic ");
|
||||||
"Basic {}",
|
STANDARD.encode_append(raw, &mut header);
|
||||||
base64::engine::general_purpose::STANDARD.encode(raw.as_bytes())
|
header
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Jmap {
|
impl Jmap {
|
||||||
|
|||||||
+11
-5
@@ -9,6 +9,7 @@ mod seeder;
|
|||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use encodify::base64::STANDARD;
|
||||||
use integration::stalwart::shared as shared_stalwart;
|
use integration::stalwart::shared as shared_stalwart;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use serde_json::{Map, Value, json};
|
use serde_json::{Map, Value, json};
|
||||||
@@ -693,7 +694,6 @@ const ISSUE30_CARD_UID: &str = "vandelay-issue30-card";
|
|||||||
const ISSUE30_EVENT_UID: &str = "vandelay-issue30-event";
|
const ISSUE30_EVENT_UID: &str = "vandelay-issue30-event";
|
||||||
|
|
||||||
fn data_uri_bytes(resource: &Value, uri_key: &str, expect_media_type: &str) -> Vec<u8> {
|
fn data_uri_bytes(resource: &Value, uri_key: &str, expect_media_type: &str) -> Vec<u8> {
|
||||||
use base64::Engine;
|
|
||||||
let uri = resource
|
let uri = resource
|
||||||
.get(uri_key)
|
.get(uri_key)
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
@@ -706,9 +706,7 @@ fn data_uri_bytes(resource: &Value, uri_key: &str, expect_media_type: &str) -> V
|
|||||||
let payload = uri
|
let payload = uri
|
||||||
.strip_prefix(&prefix)
|
.strip_prefix(&prefix)
|
||||||
.unwrap_or_else(|| panic!("{uri_key} is not a {prefix}... data URI: {resource}"));
|
.unwrap_or_else(|| panic!("{uri_key} is not a {prefix}... data URI: {resource}"));
|
||||||
base64::engine::general_purpose::STANDARD
|
STANDARD.decode(payload).expect("base64 payload")
|
||||||
.decode(payload)
|
|
||||||
.expect("base64 payload")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1245,6 +1243,10 @@ fn apply_jmap_settings(
|
|||||||
.is_some(),
|
.is_some(),
|
||||||
"x:Jmap/set not applied: {resp}"
|
"x:Jmap/set not applied: {resp}"
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
resp.pointer("/methodResponses/1/1/created/r").is_some(),
|
||||||
|
"ReloadSettings failed, so the x:Jmap/set values are not in effect: {resp}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1257,8 +1259,13 @@ fn live_blob_quota_429_triggers_retry_after_then_succeeds() {
|
|||||||
let fx = seeder::provision(base_url()).expect("provision");
|
let fx = seeder::provision(base_url()).expect("provision");
|
||||||
let acc = fx.account("test1").expect("test1");
|
let acc = fx.account("test1").expect("test1");
|
||||||
|
|
||||||
|
let blob_size = 8 * 1024 * 1024;
|
||||||
let mut updates = serde_json::Map::new();
|
let mut updates = serde_json::Map::new();
|
||||||
updates.insert("uploadTtl".to_owned(), serde_json::json!(5_000));
|
updates.insert("uploadTtl".to_owned(), serde_json::json!(5_000));
|
||||||
|
updates.insert(
|
||||||
|
"uploadQuota".to_owned(),
|
||||||
|
serde_json::json!(blob_size * 5 / 2),
|
||||||
|
);
|
||||||
let _ttl_guard = JmapSettingsGuard::override_settings(updates);
|
let _ttl_guard = JmapSettingsGuard::override_settings(updates);
|
||||||
|
|
||||||
let client = HttpClient::new(basic("test1"), RetryPolicy::new(20), true);
|
let client = HttpClient::new(basic("test1"), RetryPolicy::new(20), true);
|
||||||
@@ -1266,7 +1273,6 @@ fn live_blob_quota_429_triggers_retry_after_then_succeeds() {
|
|||||||
let limits = session.core_limits().expect("core limits");
|
let limits = session.core_limits().expect("core limits");
|
||||||
client.set_limits(&limits);
|
client.set_limits(&limits);
|
||||||
|
|
||||||
let blob_size = 8 * 1024 * 1024;
|
|
||||||
let mut blob = vec![0u8; blob_size];
|
let mut blob = vec![0u8; blob_size];
|
||||||
let max_uploads = 8u32;
|
let max_uploads = 8u32;
|
||||||
let mut accepted = 0u32;
|
let mut accepted = 0u32;
|
||||||
|
|||||||
Reference in New Issue
Block a user