diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e694c3..d51adc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/). +## [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 ### Added diff --git a/Cargo.lock b/Cargo.lock index 69ea5c0..4c86ec9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,7 +157,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] @@ -174,9 +174,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" dependencies = [ "aws-lc-sys", "zeroize", @@ -184,9 +184,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.44.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" dependencies = [ "cc", "cmake", @@ -258,9 +258,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "blake3" @@ -283,7 +283,7 @@ checksum = "dbe8358268799ebb3e4df23cb9d47f4c72bbc4f5247e2fa6a1bf7b6c0baea220" dependencies = [ "async-stream", "base64 0.22.1", - "bitflags 2.13.1", + "bitflags 2.13.2", "bollard-buildkit-proto", "bollard-stubs", "bytes", @@ -301,7 +301,7 @@ dependencies = [ "log", "num", "pin-project-lite", - "rand 0.10.2", + "rand 0.10.3", "rustls", "rustls-native-certs", "rustls-pki-types", @@ -371,9 +371,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "calcard" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b779382e675380a1ff8a4873acee5ba60158be7a00458fb98e6b85aad1f2ee" +checksum = "c601473ec15a875626bce73db1a1f0fc81e9c949ca25f1463b714947c0a70a1f" dependencies = [ "ahash", "chrono", @@ -389,9 +389,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.4" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" dependencies = [ "find-msvc-tools", "jobserver", @@ -401,15 +401,15 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", "cpufeatures", @@ -442,9 +442,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -452,9 +452,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -464,21 +464,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "cmake" @@ -540,7 +540,7 @@ dependencies = [ "cookie", "document-features", "idna", - "indexmap 2.14.0", + "indexmap 2.14.2", "log", "serde", "serde_derive", @@ -567,42 +567,42 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "darling" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ "darling_core", "darling_macro", @@ -610,26 +610,26 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] name = "darling_macro" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -680,7 +680,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] @@ -721,6 +721,16 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "equivalent" version = "1.0.2" @@ -772,24 +782,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" dependencies = [ "portable-atomic", - "rand 0.10.2", + "rand 0.10.3", "web-time", ] [[package]] name = "find-msvc-tools" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -875,7 +886,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] @@ -954,7 +965,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.14.0", + "indexmap 2.14.2", "slab", "tokio", "tokio-util", @@ -991,7 +1002,7 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd1246c0e5493286aeb2dde35b1f4eb9c4ce00e628641210a5e553fc001a1f26" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.2", "proc-macro2", "quote", "syn 2.0.119", @@ -999,9 +1010,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" dependencies = [ "hashbrown 0.17.1", ] @@ -1014,9 +1025,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] name = "hex" @@ -1080,9 +1091,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -1116,9 +1127,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.9" +version = "0.27.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" dependencies = [ "http", "hyper", @@ -1144,15 +1155,16 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" dependencies = [ "bytes", "futures-channel", "futures-util", "http", "http-body", + "httparse", "hyper", "libc", "pin-project-lite", @@ -1324,9 +1336,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -1357,9 +1369,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" dependencies = [ "defmt", "jiff-core", @@ -1374,18 +1386,19 @@ dependencies = [ [[package]] name = "jiff-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" dependencies = [ "defmt", + "log", ] [[package]] name = "jiff-static" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" dependencies = [ "jiff-core", "proc-macro2", @@ -1480,9 +1493,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.104" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ "cfg-if", "futures-util", @@ -1541,15 +1554,15 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "mail-builder" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c942e8a4b83f9351236c1e531ea9fa0237913d63c7fc36818430e0128a1ddf3" +checksum = "ccbc27cb02a58a311a404769a22f5abf63fe3173b290d80be7c534f9c7023151" [[package]] name = "mail-parser" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3a9080c1fb8190e232df37a10aa1b3d6b08be084e537069913f025e0ce86c5" +checksum = "4ec00bda90c6e645a54506c630c2820cd6b1890cfd2b0a169b50f74b2b8c7c86" dependencies = [ "hashify", ] @@ -1574,9 +1587,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", @@ -1584,9 +1597,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "wasi", @@ -1836,9 +1849,9 @@ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" dependencies = [ "portable-atomic", ] @@ -1910,9 +1923,9 @@ dependencies = [ [[package]] name = "psl" -version = "2.1.226" +version = "2.1.238" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc88482eea924ca3a2f56a547454169af58deef35567965eb4fc2392a834841" +checksum = "2e6aa2cd4e8e062e78ac5d7df6206e2f8e39b624f658efd6f34f9aa1928ac5da" dependencies = [ "psl-types", ] @@ -1925,9 +1938,9 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] name = "quick-xml" -version = "0.41.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" dependencies = [ "memchr", ] @@ -1965,9 +1978,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -2005,7 +2018,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -2025,7 +2038,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] @@ -2087,7 +2100,7 @@ version = "0.40.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -2113,11 +2126,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys", @@ -2126,9 +2139,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "aws-lc-rs", "log", @@ -2163,9 +2176,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" dependencies = [ "core-foundation", "core-foundation-sys", @@ -2184,9 +2197,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier-android" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" [[package]] name = "rustls-webpki" @@ -2266,7 +2279,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation", "core-foundation-sys", "libc", @@ -2316,7 +2329,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] @@ -2340,7 +2353,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] @@ -2357,16 +2370,16 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.22.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.14.0", + "indexmap 2.14.2", "jiff", "schemars 0.9.0", "schemars 1.2.2", @@ -2378,14 +2391,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.22.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -2440,9 +2453,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "siphasher" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" [[package]] name = "slab" @@ -2452,9 +2465,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" [[package]] name = "socket2" @@ -2532,9 +2545,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -2549,13 +2562,13 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "synstructure" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -2604,22 +2617,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.20" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.20" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] @@ -2664,18 +2677,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" @@ -2702,14 +2706,14 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" dependencies = [ "rustls", "tokio", @@ -2788,7 +2792,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.14.0", + "indexmap 2.14.2", "pin-project-lite", "slab", "sync_wrapper", @@ -2850,9 +2854,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "untrusted" @@ -2862,9 +2866,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.4.0" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" dependencies = [ "base64 0.23.1", "cookie_store", @@ -2882,9 +2886,9 @@ dependencies = [ [[package]] name = "ureq-proto" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613" +checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be" dependencies = [ "base64 0.23.1", "http", @@ -2925,26 +2929,26 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.26.0" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" dependencies = [ "sha1_smol", ] [[package]] name = "vandelay" -version = "1.0.10" +version = "1.0.11" dependencies = [ - "base64 0.23.1", "blake3", "calcard", "chrono", "chrono-tz", "clap", "crossbeam-channel", + "encodify", "flate2", - "indexmap 2.14.0", + "indexmap 2.14.2", "mail-parser", "mockito", "num_cpus", @@ -3012,9 +3016,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.127" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", "once_cell", @@ -3025,9 +3029,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.127" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3035,22 +3039,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.127" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.127" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] @@ -3281,30 +3285,30 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.56" +version = "0.8.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.56" +version = "0.8.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" dependencies = [ "proc-macro2", "quote", @@ -3322,13 +3326,13 @@ dependencies = [ [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", "synstructure", ] @@ -3368,9 +3372,15 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "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]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index 80a4d9b..c3b8088 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "vandelay" description = "JMAP account migration utility" -version = "1.0.10" +version = "1.0.11" authors = ["Stalwart Labs LLC "] license = "Apache-2.0 OR MIT" repository = "https://github.com/stalwartlabs/vandelay" @@ -26,7 +26,7 @@ bundled-sqlite = ["rusqlite/bundled"] [dependencies] serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0" -indexmap = { version = "2.14.0", features = ["serde"] } +indexmap = { version = "2.14", features = ["serde"] } rusqlite = { version = "0.40", features = ["blob"] } blake3 = "1.8.5" 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"] } crossbeam-channel = "0.5.15" num_cpus = "1.17.0" -base64 = "0.23" +encodify = "1.0" mail-parser = "0.11.3" calcard = "0.3" rustls = { version = "0.23.40", default-features = false, features = ["aws-lc-rs", "std", "tls12", "logging"] } rustls-platform-verifier = "0.7" regex = "1.13" flate2 = "1.1.9" -quick-xml = "0.41" +quick-xml = "0.42" chrono = { version = "0.4", default-features = false, features = ["std"] } chrono-tz = { version = "0.10", default-features = false } psl = "2.1.226" diff --git a/src/dav/href.rs b/src/dav/href.rs index 8ad1b23..6a1c357 100644 --- a/src/dav/href.rs +++ b/src/dav/href.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT */ +use encodify::hex::decode_pair; use url::Url; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -111,9 +112,9 @@ fn percent_decode(s: &str) -> String { while i < bytes.len() { if bytes[i] == b'%' && 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; continue; } @@ -126,15 +127,6 @@ fn percent_decode(s: &str) -> String { } } -fn hex_val(b: u8) -> Option { - 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 { let mut out = String::with_capacity(s.len()); for byte in s.bytes() { diff --git a/src/dav/parse.rs b/src/dav/parse.rs index 6d9c006..198c976 100644 --- a/src/dav/parse.rs +++ b/src/dav/parse.rs @@ -78,13 +78,13 @@ fn classify_ns(ns: &ResolveResult<'_>) -> NsKind { match ns { ResolveResult::Bound(prefix) => { let p = prefix.as_ref(); - if p == NS_DAV.as_bytes() { + if p == NS_DAV { NsKind::Dav - } else if p == NS_CALDAV.as_bytes() { + } else if p == NS_CALDAV { NsKind::Caldav - } else if p == NS_CARDDAV.as_bytes() { + } else if p == NS_CARDDAV { NsKind::Carddav - } else if p == NS_APPLE_ICAL.as_bytes() { + } else if p == NS_APPLE_ICAL { NsKind::Apple } else { 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) } @@ -102,17 +102,17 @@ fn is(ns: NsKind, local: &[u8], expected_ns: NsKind, target: &[u8]) -> bool { enum Step { StartElement { ns: NsKind, - local: Vec, - attrs: Vec<(Vec, Vec)>, + local: String, + attrs: Vec<(String, String)>, }, EmptyElement { ns: NsKind, - local: Vec, - attrs: Vec<(Vec, Vec)>, + local: String, + attrs: Vec<(String, String)>, }, EndElement { ns: NsKind, - local: Vec, + local: String, }, Text(String), CData(String), @@ -125,7 +125,7 @@ fn resolve_entity(g: &BytesRef) -> Option { if let Ok(Some(c)) = g.resolve_char_ref() { return Some(c); } - match g.decode().ok()?.as_ref() { + match g.as_ref() { "amp" => Some('&'), "lt" => Some('<'), "gt" => Some('>'), @@ -141,11 +141,12 @@ fn next_step(xml: &mut NsReader, buf: &mut Vec) -> Result { - let local = e.local_name().as_ref().to_vec(); - let mut attrs: Vec<(Vec, Vec)> = Vec::new(); - for a in e.attributes().flatten() { - attrs.push((a.key.as_ref().to_vec(), a.value.as_ref().to_vec())); - } + let local = e.local_name().as_ref().to_owned(); + let attrs = e + .attributes() + .flatten() + .map(|a| (a.key.as_ref().to_owned(), a.value.into_owned())) + .collect(); Step::StartElement { ns: ns_kind, local, @@ -153,11 +154,12 @@ fn next_step(xml: &mut NsReader, buf: &mut Vec) -> Result { - let local = e.local_name().as_ref().to_vec(); - let mut attrs: Vec<(Vec, Vec)> = Vec::new(); - for a in e.attributes().flatten() { - attrs.push((a.key.as_ref().to_vec(), a.value.as_ref().to_vec())); - } + let local = e.local_name().as_ref().to_owned(); + let attrs = e + .attributes() + .flatten() + .map(|a| (a.key.as_ref().to_owned(), a.value.into_owned())) + .collect(); Step::EmptyElement { ns: ns_kind, local, @@ -166,13 +168,10 @@ fn next_step(xml: &mut NsReader, buf: &mut Vec) -> Result Step::EndElement { ns: ns_kind, - local: e.local_name().as_ref().to_vec(), + local: e.local_name().as_ref().to_owned(), }, - Event::Text(t) => { - let s = t.decode().map_err(|e| ParseError::Xml(e.to_string()))?; - Step::Text(s.into_owned()) - } - Event::CData(cd) => Step::CData(String::from_utf8_lossy(cd.as_ref()).into_owned()), + Event::Text(t) => Step::Text(t.into_inner().into_owned()), + Event::CData(cd) => Step::CData(cd.into_inner().into_owned()), Event::GeneralRef(g) => match resolve_entity(&g) { Some(c) => Step::Entity(c), None => Step::Other, @@ -194,7 +193,7 @@ pub fn parse_multistatus( loop { let step = next_step(&mut xml, &mut buf)?; 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)? && seen.insert(r.href.as_str().to_owned()) { @@ -222,15 +221,15 @@ fn parse_response( let step = next_step(xml, &mut buf)?; match step { Step::StartElement { ns, local, .. } => { - if is(ns, &local, NsKind::Dav, b"href") { + if is(ns, &local, NsKind::Dav, "href") { let text = read_token(xml)?; if href.is_none() { 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)?; 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)?; match block_status { Some(s) if (200..300).contains(&s) => merge_props(&mut props, block_props), @@ -241,7 +240,7 @@ fn parse_response( 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 ".into())), _ => {} } @@ -270,16 +269,16 @@ fn parse_propstat( let step = next_step(xml, &mut buf)?; match step { Step::StartElement { ns, local, .. } => { - if is(ns, &local, NsKind::Dav, b"status") { + if is(ns, &local, NsKind::Dav, "status") { let text = read_token(xml)?; 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)?; } else { 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 ".into())), _ => {} } @@ -299,7 +298,7 @@ fn parse_prop_block( handle_prop_element(xml, props, ns, &local)?; } 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 ".into())), _ => {} } @@ -311,53 +310,53 @@ fn handle_prop_element( xml: &mut NsReader, props: &mut ResourceProps, ns: NsKind, - local: &[u8], + local: &str, ) -> Result<(), ParseError> { - if is(ns, local, NsKind::Dav, b"resourcetype") { + if is(ns, local, NsKind::Dav, "resourcetype") { 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)?); - } else if is(ns, local, NsKind::Dav, b"current-user-principal") { - props.current_user_principal = read_first_href(xml, b"current-user-principal")?; - } else if is(ns, local, NsKind::Caldav, b"calendar-home-set") { - props.calendar_home_set = read_first_href(xml, b"calendar-home-set")?; - } else if is(ns, local, NsKind::Carddav, b"addressbook-home-set") { - props.addressbook_home_set = read_first_href(xml, b"addressbook-home-set")?; - } else if is(ns, local, NsKind::Dav, b"getetag") { + } else if is(ns, local, NsKind::Dav, "current-user-principal") { + props.current_user_principal = read_first_href(xml, "current-user-principal")?; + } else if is(ns, local, NsKind::Caldav, "calendar-home-set") { + props.calendar_home_set = read_first_href(xml, "calendar-home-set")?; + } else if is(ns, local, NsKind::Carddav, "addressbook-home-set") { + props.addressbook_home_set = read_first_href(xml, "addressbook-home-set")?; + } else if is(ns, local, NsKind::Dav, "getetag") { let raw = read_token(xml)?; if !raw.is_empty() { 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)?); - } else if is(ns, local, NsKind::Dav, b"getlastmodified") { + } else if is(ns, local, NsKind::Dav, "getlastmodified") { 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)?); - } else if is(ns, local, NsKind::Dav, b"getcontentlength") { + } else if is(ns, local, NsKind::Dav, "getcontentlength") { let raw = read_token(xml)?; props.content_length = raw.parse::().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)?); - } 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)?); - } 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)?); - } else if is(ns, local, NsKind::Apple, b"calendar-order") { + } else if is(ns, local, NsKind::Apple, "calendar-order") { let raw = read_token(xml)?; props.calendar_order = raw.parse::().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)?); } else if is( ns, local, NsKind::Caldav, - b"supported-calendar-component-set", + "supported-calendar-component-set", ) { 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)?); - } 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)?); } else { skip_element(xml, local)?; @@ -380,7 +379,7 @@ fn consume_resourcetype( Step::EmptyElement { ns, local, .. } => { 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 => { return Err(ParseError::Xml("unexpected EOF in ".into())); } @@ -390,12 +389,12 @@ fn consume_resourcetype( Ok(()) } -fn mark_resourcetype(ns: NsKind, local: &[u8], props: &mut ResourceProps) { - if is(ns, local, NsKind::Dav, b"collection") { +fn mark_resourcetype(ns: NsKind, local: &str, props: &mut ResourceProps) { + if is(ns, local, NsKind::Dav, "collection") { 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; - } else if is(ns, local, NsKind::Carddav, b"addressbook") { + } else if is(ns, local, NsKind::Carddav, "addressbook") { props.is_addressbook = true; } } @@ -412,8 +411,8 @@ fn consume_supported_components( ns, local, ref attrs, - } if is(ns, &local, NsKind::Caldav, b"comp") => { - if let Some(name) = attr_lookup(attrs, b"name") { + } if is(ns, &local, NsKind::Caldav, "comp") => { + if let Some(name) = attr_lookup(attrs, "name") { props.supported_components.push(name); } skip_element(xml, &local)?; @@ -422,8 +421,8 @@ fn consume_supported_components( ns, local, ref attrs, - } if is(ns, &local, NsKind::Caldav, b"comp") => { - if let Some(name) = attr_lookup(attrs, b"name") { + } if is(ns, &local, NsKind::Caldav, "comp") => { + if let Some(name) = attr_lookup(attrs, "name") { props.supported_components.push(name); } } @@ -432,7 +431,7 @@ fn consume_supported_components( ns, &local, NsKind::Caldav, - b"supported-calendar-component-set", + "supported-calendar-component-set", ) => { break; @@ -448,13 +447,8 @@ fn consume_supported_components( Ok(()) } -fn attr_lookup(attrs: &[(Vec, Vec)], key: &[u8]) -> Option { - for (k, v) in attrs { - if k == key { - return Some(String::from_utf8_lossy(v).into_owned()); - } - } - None +fn attr_lookup(attrs: &[(String, String)], key: &str) -> Option { + attrs.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone()) } fn read_text(xml: &mut NsReader) -> Result { @@ -480,7 +474,7 @@ fn read_token(xml: &mut NsReader) -> Result { fn read_first_href( xml: &mut NsReader, - closing_local: &[u8], + closing_local: &str, ) -> Result, ParseError> { let mut buf = Vec::new(); let mut out: Option = None; @@ -488,7 +482,7 @@ fn read_first_href( let step = next_step(xml, &mut buf)?; match step { 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)?); } else { skip_element(xml, &local)?; @@ -505,7 +499,7 @@ fn read_first_href( Ok(out) } -fn skip_element(xml: &mut NsReader, target: &[u8]) -> Result<(), ParseError> { +fn skip_element(xml: &mut NsReader, target: &str) -> Result<(), ParseError> { let mut buf = Vec::new(); let mut depth: i32 = 1; while depth > 0 { diff --git a/src/exchange_ews/autodiscover.rs b/src/exchange_ews/autodiscover.rs index 42e224a..cb28b4f 100644 --- a/src/exchange_ews/autodiscover.rs +++ b/src/exchange_ews/autodiscover.rs @@ -435,31 +435,31 @@ fn parse_pox_response(body: &[u8]) -> Result { let (_, ev) = xml.read_resolved_event_into(&mut buf)?; match ev { Event::Start(e) => { - let local = e.local_name().as_ref().to_vec(); + let local = e.local_name().as_ref().to_owned(); cur.clear(); - if local.eq_ignore_ascii_case(b"Protocol") { + if local.eq_ignore_ascii_case("Protocol") { current_type = None; } - if local.eq_ignore_ascii_case(b"Type") { + if local.eq_ignore_ascii_case("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")) { current = Some("ewsUrl"); } else { current = None; } - } else if local.eq_ignore_ascii_case(b"Action") { + } else if local.eq_ignore_ascii_case("Action") { current = Some("action"); - } else if local.eq_ignore_ascii_case(b"RedirectAddr") { + } else if local.eq_ignore_ascii_case("RedirectAddr") { current = Some("redirectAddr"); - } else if local.eq_ignore_ascii_case(b"RedirectUrl") { + } else if local.eq_ignore_ascii_case("RedirectUrl") { current = Some("redirectUrl"); } else { current = None; } } 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() { let text = std::mem::take(&mut cur); match field { @@ -472,15 +472,15 @@ fn parse_pox_response(body: &[u8]) -> Result { } } cur.clear(); - if local.eq_ignore_ascii_case(b"Protocol") { + if local.eq_ignore_ascii_case("Protocol") { current_type = None; } } Event::Text(t) => { - cur.push_str(&t.decode().map(|c| c.into_owned()).unwrap_or_default()); + cur.push_str(&t); } Event::CData(c) => { - cur.push_str(&String::from_utf8_lossy(c.as_ref())); + cur.push_str(&c); } Event::GeneralRef(g) => { if let Some(c) = entity_to_char(&g) { diff --git a/src/exchange_ews/client.rs b/src/exchange_ews/client.rs index 6e69fc8..b8fef91 100644 --- a/src/exchange_ews/client.rs +++ b/src/exchange_ews/client.rs @@ -9,8 +9,6 @@ use std::sync::Mutex; use std::sync::atomic::{AtomicU8, AtomicU64, Ordering}; use std::time::{Duration, Instant}; -use base64::Engine; -use base64::engine::general_purpose::STANDARD; use ureq::Agent; use ureq::config::{Config, RedirectAuthHeaders}; use ureq::tls::{RootCerts, TlsConfig}; @@ -168,14 +166,11 @@ impl EwsClient { } fn auth_header(&self) -> String { - let auth = self.inner.auth.lock().ok().map(|g| g.clone()); - match auth { - Some(Auth::Basic { user, password }) => { - format!("Basic {}", STANDARD.encode(format!("{user}:{password}"))) - } - Some(Auth::Bearer { token }) => format!("Bearer {token}"), - None => String::new(), - } + self.inner + .auth + .lock() + .map(|auth| auth.header_value()) + .unwrap_or_default() } fn anchor_header(&self) -> Option { diff --git a/src/exchange_ews/oauth.rs b/src/exchange_ews/oauth.rs index a537dc0..e00e935 100644 --- a/src/exchange_ews/oauth.rs +++ b/src/exchange_ews/oauth.rs @@ -7,8 +7,7 @@ use std::io::{self, Write}; use std::time::Duration; -use base64::Engine; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use encodify::base64::{Base64, Padding, URL_SAFE}; use serde_json::Value; use ureq::config::Config; 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 = "https://outlook.office365.com/EWS.AccessAsUser.All offline_access"; +const JWT_SEGMENT: Base64 = URL_SAFE.with_padding(Padding::Optional).any_alphabet(); + #[derive(Debug, Clone)] pub struct AcquiredToken { pub access_token: String, @@ -81,11 +82,7 @@ pub fn decode_jwt_claims(token: &str) -> Option { let mut parts = token.split('.'); let _header = parts.next()?; let payload = parts.next()?; - let bytes = URL_SAFE_NO_PAD - .decode(payload) - .or_else(|_| base64::engine::general_purpose::STANDARD_NO_PAD.decode(payload)) - .or_else(|_| base64::engine::general_purpose::STANDARD.decode(payload)) - .ok()?; + let bytes = JWT_SEGMENT.decode(payload).ok()?; let value: Value = serde_json::from_slice(&bytes).ok()?; Some(JwtClaims { 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)] mod tests { 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 { let header = URL_SAFE_NO_PAD.encode(b"{\"alg\":\"none\"}"); @@ -347,6 +344,19 @@ mod tests { 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] fn malformed_token_returns_none() { assert!(decode_jwt_claims("garbage").is_none()); diff --git a/src/exchange_ews/parse.rs b/src/exchange_ews/parse.rs index 358c847..0024b54 100644 --- a/src/exchange_ews/parse.rs +++ b/src/exchange_ews/parse.rs @@ -26,9 +26,9 @@ fn classify(ns: &ResolveResult<'_>) -> Ns { match ns { ResolveResult::Bound(prefix) => { let p = prefix.as_ref(); - if p == NS_MESSAGES.as_bytes() { + if p == NS_MESSAGES { Ns::Messages - } else if p == NS_TYPES.as_bytes() { + } else if p == NS_TYPES { Ns::Types } else { Ns::Other @@ -38,18 +38,18 @@ fn classify(ns: &ResolveResult<'_>) -> Ns { } } -fn is(ns: Ns, local: &[u8], expected_ns: Ns, target: &[u8]) -> bool { +fn is(ns: Ns, local: &str, expected_ns: Ns, target: &str) -> bool { ns == expected_ns && local.eq_ignore_ascii_case(target) } -fn attr_value(e: &BytesStart<'_>, name: &[u8]) -> Option { +fn attr_value(e: &BytesStart<'_>, name: &str) -> Option { for a in e.attributes().flatten() { let key = a.key.local_name(); if key.as_ref().eq_ignore_ascii_case(name) { return Some( a.normalized_value(XmlVersion::Implicit1_0) .map(|c| c.into_owned()) - .unwrap_or_else(|_| String::from_utf8_lossy(a.value.as_ref()).into_owned()), + .unwrap_or_else(|_| a.value.into_owned()), ); } } @@ -63,10 +63,10 @@ fn capture_id_attrs(e: &BytesStart<'_>, id_out: &mut String, ck_out: &mut String let v = a .normalized_value(XmlVersion::Implicit1_0) .map(|c| c.into_owned()) - .unwrap_or_else(|_| String::from_utf8_lossy(a.value.as_ref()).into_owned()); - if kb.eq_ignore_ascii_case(b"Id") { + .unwrap_or_else(|_| a.value.into_owned()); + if kb.eq_ignore_ascii_case("Id") { *id_out = v; - } else if kb.eq_ignore_ascii_case(b"ChangeKey") { + } else if kb.eq_ignore_ascii_case("ChangeKey") { *ck_out = v; } } @@ -94,16 +94,16 @@ fn read_version_attrs(e: &BytesStart<'_>, out: &mut ServerVersionInfo) { for a in e.attributes().flatten() { let key = a.key.local_name(); let kb = key.as_ref(); - let v = String::from_utf8_lossy(a.value.as_ref()).into_owned(); - if kb.eq_ignore_ascii_case(b"MajorVersion") { + let v = a.value.into_owned(); + if kb.eq_ignore_ascii_case("MajorVersion") { out.major_version = v.parse().ok(); - } else if kb.eq_ignore_ascii_case(b"MinorVersion") { + } else if kb.eq_ignore_ascii_case("MinorVersion") { out.minor_version = v.parse().ok(); - } else if kb.eq_ignore_ascii_case(b"MajorBuildNumber") { + } else if kb.eq_ignore_ascii_case("MajorBuildNumber") { out.major_build = v.parse().ok(); - } else if kb.eq_ignore_ascii_case(b"MinorBuildNumber") { + } else if kb.eq_ignore_ascii_case("MinorBuildNumber") { out.minor_build = v.parse().ok(); - } else if kb.eq_ignore_ascii_case(b"Version") { + } else if kb.eq_ignore_ascii_case("Version") { out.version = Some(v); } } @@ -140,13 +140,13 @@ pub fn read_envelope_summary(bytes: &[u8]) -> Result { let ns_kind = classify(&ns); match ev { Event::Start(ref e) | Event::Empty(ref e) => { - let local = e.local_name().as_ref().to_vec(); - if is(ns_kind, &local, Ns::Types, b"ServerVersionInfo") { + let local = e.local_name().as_ref().to_owned(); + if is(ns_kind, &local, Ns::Types, "ServerVersionInfo") { read_version_attrs(e, &mut version); - } else if local.eq_ignore_ascii_case(b"Fault") { + } else if local.eq_ignore_ascii_case("Fault") { let fault = parse_fault(&mut xml)?; return Ok(EnvelopeKind::Fault { version, fault }); - } else if local.eq_ignore_ascii_case(b"Body") { + } else if local.eq_ignore_ascii_case("Body") { seen_body = true; } } @@ -177,24 +177,24 @@ fn parse_fault(xml: &mut NsReader) -> Result match ev { Event::Start(e) => { depth += 1; - let local = e.local_name().as_ref().to_vec(); - if local.eq_ignore_ascii_case(b"faultcode") { + let local = e.local_name().as_ref().to_owned(); + if local.eq_ignore_ascii_case("faultcode") { text_target = Some("faultcode"); - } else if local.eq_ignore_ascii_case(b"faultstring") { + } else if local.eq_ignore_ascii_case("faultstring") { text_target = Some("faultstring"); - } else if local.eq_ignore_ascii_case(b"ResponseCode") { + } else if local.eq_ignore_ascii_case("ResponseCode") { text_target = Some("responseCode"); - } else if local.eq_ignore_ascii_case(b"Value") && ns_kind == Ns::Types { - last_value_name = attr_value(&e, b"Name"); + } else if local.eq_ignore_ascii_case("Value") && ns_kind == Ns::Types { + last_value_name = attr_value(&e, "Name"); text_target = Some("messageXmlValue"); } else { text_target = None; } } Event::Empty(e) => { - let local = e.local_name().as_ref().to_vec(); - if is(ns_kind, &local, Ns::Types, b"Value") { - last_value_name = attr_value(&e, b"Name"); + let local = e.local_name().as_ref().to_owned(); + if is(ns_kind, &local, Ns::Types, "Value") { + last_value_name = attr_value(&e, "Name"); } } Event::End(_) => { @@ -208,7 +208,7 @@ fn parse_fault(xml: &mut NsReader) -> Result } } Event::Text(t) => { - let text = t.decode().map(|c| c.into_owned()).unwrap_or_default(); + let text = t.into_inner().into_owned(); match text_target { Some("faultcode") => fault_code = text, Some("faultstring") => fault_string.push_str(&text), @@ -222,7 +222,7 @@ fn parse_fault(xml: &mut NsReader) -> Result } } Event::CData(c) if text_target == Some("faultstring") => { - fault_string.push_str(&String::from_utf8_lossy(c.as_ref())); + fault_string.push_str(&c); } Event::GeneralRef(ref g) if text_target == Some("faultstring") => { if let Some(c) = entity_to_char(g) { @@ -291,16 +291,16 @@ pub enum FolderElement { } impl FolderElement { - fn from_local(local: &[u8]) -> Option { - if local.eq_ignore_ascii_case(b"Folder") { + fn from_local(local: &str) -> Option { + if local.eq_ignore_ascii_case("Folder") { Some(FolderElement::Folder) - } else if local.eq_ignore_ascii_case(b"CalendarFolder") { + } else if local.eq_ignore_ascii_case("CalendarFolder") { Some(FolderElement::CalendarFolder) - } else if local.eq_ignore_ascii_case(b"ContactsFolder") { + } else if local.eq_ignore_ascii_case("ContactsFolder") { Some(FolderElement::ContactsFolder) - } else if local.eq_ignore_ascii_case(b"TasksFolder") { + } else if local.eq_ignore_ascii_case("TasksFolder") { Some(FolderElement::TasksFolder) - } else if local.eq_ignore_ascii_case(b"SearchFolder") { + } else if local.eq_ignore_ascii_case("SearchFolder") { Some(FolderElement::SearchFolder) } else { None @@ -319,12 +319,12 @@ pub fn parse_find_folder_response(body: &[u8]) -> Result { - let local = e.local_name().as_ref().to_vec(); - if is(ns_kind, &local, Ns::Messages, b"RootFolder") { - if let Some(v) = attr_value(e, b"TotalItemsInView") { + let local = e.local_name().as_ref().to_owned(); + if is(ns_kind, &local, Ns::Messages, "RootFolder") { + if let Some(v) = attr_value(e, "TotalItemsInView") { out.total_in_view = v.trim().parse().ok(); } - if let Some(v) = attr_value(e, b"IncludesLastItemInRange") { + if let Some(v) = attr_value(e, "IncludesLastItemInRange") { out.more = !matches!(v.trim(), "true" | "1"); } } else if ns_kind == Ns::Types @@ -352,7 +352,7 @@ pub fn parse_folder_inner(inner_xml: &str) -> Result, EwsErr match ev { Event::Start(_) | Event::Empty(_) => { let local = match &ev { - Event::Start(e) | Event::Empty(e) => e.local_name().as_ref().to_vec(), + Event::Start(e) | Event::Empty(e) => e.local_name().as_ref().to_owned(), _ => continue, }; if ns_kind == Ns::Types @@ -379,7 +379,7 @@ pub fn parse_get_folder_response(body: &[u8]) -> Result, EwsErr match ev { Event::Start(_) | Event::Empty(_) => { let local = match &ev { - Event::Start(e) | Event::Empty(e) => e.local_name().as_ref().to_vec(), + Event::Start(e) | Event::Empty(e) => e.local_name().as_ref().to_owned(), _ => continue, }; if ns_kind == Ns::Types @@ -419,31 +419,31 @@ fn parse_folder_element( if !is_empty { depth += 1; } - let local = e.local_name().as_ref().to_vec(); + let local = e.local_name().as_ref().to_owned(); if ns_kind == Ns::Types { - if local.eq_ignore_ascii_case(b"FolderId") { + if local.eq_ignore_ascii_case("FolderId") { capture_id_attrs( e, &mut entry.folder_id.id, &mut entry.folder_id.change_key, ); - } else if local.eq_ignore_ascii_case(b"ParentFolderId") { + } else if local.eq_ignore_ascii_case("ParentFolderId") { let mut pid = String::new(); let mut ck = String::new(); capture_id_attrs(e, &mut pid, &mut ck); if !pid.is_empty() { entry.parent_id = Some(pid); } - } else if local.eq_ignore_ascii_case(b"DisplayName") { + } else if local.eq_ignore_ascii_case("DisplayName") { current = Some("displayName"); cur.clear(); - } else if local.eq_ignore_ascii_case(b"FolderClass") { + } else if local.eq_ignore_ascii_case("FolderClass") { current = Some("folderClass"); cur.clear(); - } else if local.eq_ignore_ascii_case(b"TotalCount") { + } else if local.eq_ignore_ascii_case("TotalCount") { current = Some("totalCount"); cur.clear(); - } else if local.eq_ignore_ascii_case(b"ChildFolderCount") { + } else if local.eq_ignore_ascii_case("ChildFolderCount") { current = Some("childCount"); cur.clear(); } else { @@ -474,10 +474,10 @@ fn parse_folder_element( } } Event::Text(t) => { - cur.push_str(&t.decode().map(|c| c.into_owned()).unwrap_or_default()); + cur.push_str(&t); } Event::CData(c) => { - cur.push_str(&String::from_utf8_lossy(c.as_ref())); + cur.push_str(&c); } Event::GeneralRef(ref g) => { if let Some(c) = entity_to_char(g) { @@ -516,22 +516,22 @@ pub fn parse_find_item_response(body: &[u8]) -> Result { - let local = e.local_name().as_ref().to_vec(); - if is(ns_kind, &local, Ns::Messages, b"RootFolder") { + let local = e.local_name().as_ref().to_owned(); + if is(ns_kind, &local, Ns::Messages, "RootFolder") { in_root = true; - if let Some(v) = attr_value(e, b"TotalItemsInView") { + if let Some(v) = attr_value(e, "TotalItemsInView") { out.total_in_view = v.trim().parse().ok(); } - if let Some(v) = attr_value(e, b"IncludesLastItemInRange") { + if let Some(v) = attr_value(e, "IncludesLastItemInRange") { out.more = !matches!(v.trim(), "true" | "1"); } } else if in_root && ns_kind == Ns::Types { if is_item_element(&local) { out.items.push(ItemEntry { - element: String::from_utf8_lossy(&local).into_owned(), + element: local.clone(), id: ItemId::default(), }); - } else if local.eq_ignore_ascii_case(b"ItemId") + } else if local.eq_ignore_ascii_case("ItemId") && let Some(last) = out.items.last_mut() { capture_id_attrs(e, &mut last.id.id, &mut last.id.change_key); @@ -539,8 +539,8 @@ pub fn parse_find_item_response(body: &[u8]) -> Result { - let local = e.local_name().as_ref().to_vec(); - if local.eq_ignore_ascii_case(b"RootFolder") { + let local = e.local_name().as_ref().to_owned(); + if local.eq_ignore_ascii_case("RootFolder") { in_root = false; } } @@ -555,7 +555,7 @@ pub(crate) fn entity_to_char(g: &BytesRef) -> Option { if let Ok(Some(c)) = g.resolve_char_ref() { return Some(c); } - match g.decode().ok()?.as_ref() { + match g.as_ref() { "amp" => Some('&'), "lt" => Some('<'), "gt" => Some('>'), @@ -565,18 +565,18 @@ pub(crate) fn entity_to_char(g: &BytesRef) -> Option { } } -fn is_item_element(local: &[u8]) -> bool { +fn is_item_element(local: &str) -> bool { matches!( - local.to_ascii_lowercase().as_slice(), - b"message" - | b"calendaritem" - | b"contact" - | b"distributionlist" - | b"meetingrequest" - | b"meetingresponse" - | b"meetingmessage" - | b"meetingcancellation" - | b"item" + local.to_ascii_lowercase().as_str(), + "message" + | "calendaritem" + | "contact" + | "distributionlist" + | "meetingrequest" + | "meetingresponse" + | "meetingmessage" + | "meetingcancellation" + | "item" ) } @@ -590,7 +590,7 @@ pub struct ResponseMessage { pub fn parse_response_messages( body: &[u8], - response_message_local: &[u8], + response_message_local: &str, ) -> Result, EwsError> { let mut xml = NsReader::from_reader(body); xml.config_mut().trim_text(false); @@ -602,10 +602,10 @@ pub fn parse_response_messages( let ns_kind = classify(&ns); match ev { Event::Start(e) => { - let local = e.local_name().as_ref().to_vec(); + let local = e.local_name().as_ref().to_owned(); if is(ns_kind, &local, Ns::Messages, response_message_local) { let response_class = - attr_value(&e, b"ResponseClass").unwrap_or_else(|| "Success".to_owned()); + attr_value(&e, "ResponseClass").unwrap_or_else(|| "Success".to_owned()); let msg = parse_one_response_message(&mut xml, response_class)?; out.push(msg); } @@ -636,15 +636,15 @@ fn parse_one_response_message( match ev { Event::Start(e) => { depth += 1; - let local = e.local_name().as_ref().to_vec(); - if !in_capture && is(ns_kind, &local, Ns::Messages, b"ResponseCode") { + let local = e.local_name().as_ref().to_owned(); + if !in_capture && is(ns_kind, &local, Ns::Messages, "ResponseCode") { current = Some("responseCode"); - } else if !in_capture && is(ns_kind, &local, Ns::Messages, b"MessageText") { + } else if !in_capture && is(ns_kind, &local, Ns::Messages, "MessageText") { current = Some("messageText"); } else if !in_capture - && (is(ns_kind, &local, Ns::Messages, b"Items") - || is(ns_kind, &local, Ns::Messages, b"Attachments") - || is(ns_kind, &local, Ns::Messages, b"Folders")) + && (is(ns_kind, &local, Ns::Messages, "Items") + || is(ns_kind, &local, Ns::Messages, "Attachments") + || is(ns_kind, &local, Ns::Messages, "Folders")) { capture_depth = 1; current = None; @@ -656,11 +656,11 @@ fn parse_one_response_message( } } Event::Empty(e) => { - let local = e.local_name().as_ref().to_vec(); + let local = e.local_name().as_ref().to_owned(); if in_capture { write_empty_xml(&mut inner, &e); - } else if is(ns_kind, &local, Ns::Messages, b"ResponseCode") - || is(ns_kind, &local, Ns::Messages, b"MessageText") + } else if is(ns_kind, &local, Ns::Messages, "ResponseCode") + || is(ns_kind, &local, Ns::Messages, "MessageText") { } } @@ -683,7 +683,7 @@ fn parse_one_response_message( } } Event::Text(t) => { - let text = t.decode().map(|c| c.into_owned()).unwrap_or_default(); + let text = t.into_inner().into_owned(); if in_capture { write_text_xml(&mut inner, &text); } else { @@ -695,7 +695,7 @@ fn parse_one_response_message( } } Event::CData(c) => { - let text = String::from_utf8_lossy(c.as_ref()).into_owned(); + let text = c.into_inner().into_owned(); if in_capture { write_text_xml(&mut inner, &text); } @@ -703,9 +703,7 @@ fn parse_one_response_message( Event::GeneralRef(ref g) => { if in_capture { inner.push('&'); - if let Ok(name) = g.decode() { - inner.push_str(&name); - } + inner.push_str(g.as_ref()); inner.push(';'); } } @@ -732,12 +730,12 @@ fn wrap_inner_with_namespaces(inner: &str) -> String { fn write_start_xml(out: &mut String, e: &BytesStart<'_>) { out.push('<'); - out.push_str(&String::from_utf8_lossy(e.name().as_ref())); + out.push_str(e.name().as_ref()); for a in e.attributes().flatten() { out.push(' '); - out.push_str(&String::from_utf8_lossy(a.key.as_ref())); + out.push_str(a.key.as_ref()); out.push_str("=\""); - let val = String::from_utf8_lossy(a.value.as_ref()); + let val = a.value; for ch in val.chars() { match ch { '&' => out.push_str("&"), @@ -754,12 +752,12 @@ fn write_start_xml(out: &mut String, e: &BytesStart<'_>) { fn write_empty_xml(out: &mut String, e: &BytesStart<'_>) { out.push('<'); - out.push_str(&String::from_utf8_lossy(e.name().as_ref())); + out.push_str(e.name().as_ref()); for a in e.attributes().flatten() { out.push(' '); - out.push_str(&String::from_utf8_lossy(a.key.as_ref())); + out.push_str(a.key.as_ref()); out.push_str("=\""); - let val = String::from_utf8_lossy(a.value.as_ref()); + let val = a.value; for ch in val.chars() { match ch { '&' => out.push_str("&"), @@ -776,7 +774,7 @@ fn write_empty_xml(out: &mut String, e: &BytesStart<'_>) { fn write_end_xml_event(out: &mut String, e: &quick_xml::events::BytesEnd<'_>) { out.push_str("'); } @@ -824,47 +822,47 @@ pub fn parse_sync_folder_items_response(body: &[u8]) -> Result { - let local = e.local_name().as_ref().to_vec(); + let local = e.local_name().as_ref().to_owned(); let is_empty = matches!(ev, Event::Empty(_)); - if is(ns_kind, &local, Ns::Messages, b"SyncState") { + if is(ns_kind, &local, Ns::Messages, "SyncState") { current_text = Some("syncState"); - } else if is(ns_kind, &local, Ns::Messages, b"IncludesLastItemInRange") { + } else if is(ns_kind, &local, Ns::Messages, "IncludesLastItemInRange") { current_text = Some("includesLast"); - } else if is(ns_kind, &local, Ns::Messages, b"Changes") { + } else if is(ns_kind, &local, Ns::Messages, "Changes") { in_changes = true; } else if in_changes && ns_kind == Ns::Types { - if local.eq_ignore_ascii_case(b"Create") { + if local.eq_ignore_ascii_case("Create") { current_change = Some(SyncChange::Create { id: ItemId::default(), element: String::new(), }); - } else if local.eq_ignore_ascii_case(b"Update") { + } else if local.eq_ignore_ascii_case("Update") { current_change = Some(SyncChange::Update { id: ItemId::default(), element: String::new(), }); - } else if local.eq_ignore_ascii_case(b"Delete") { + } else if local.eq_ignore_ascii_case("Delete") { current_change = Some(SyncChange::Delete { id: ItemId::default(), }); - } else if local.eq_ignore_ascii_case(b"ReadFlagChange") { + } else if local.eq_ignore_ascii_case("ReadFlagChange") { current_change = Some(SyncChange::ReadFlagChange { id: ItemId::default(), is_read: false, }); pending_is_read = None; - } else if local.eq_ignore_ascii_case(b"ItemId") { + } else if local.eq_ignore_ascii_case("ItemId") { if let Some(change) = current_change.as_mut() { let (id, ck) = item_id_mut(change); capture_id_attrs(e, id, ck); } - } else if local.eq_ignore_ascii_case(b"IsRead") { + } else if local.eq_ignore_ascii_case("IsRead") { reading_is_read = true; } else if is_item_element(&local) { match current_change.as_mut() { Some(SyncChange::Create { element, .. }) | Some(SyncChange::Update { element, .. }) => { - *element = String::from_utf8_lossy(&local).into_owned(); + *element = local.clone(); } _ => {} } @@ -875,10 +873,10 @@ pub fn parse_sync_folder_items_response(body: &[u8]) -> Result { - let local = e.local_name().as_ref().to_vec(); + let local = e.local_name().as_ref().to_owned(); let lower = local.to_ascii_lowercase(); - match lower.as_slice() { - b"create" | b"update" | b"delete" | b"readflagchange" => { + match lower.as_str() { + "create" | "update" | "delete" | "readflagchange" => { if let Some(mut change) = current_change.take() { if let SyncChange::ReadFlagChange { is_read, .. } = &mut change && let Some(v) = pending_is_read.take() @@ -888,14 +886,14 @@ pub fn parse_sync_folder_items_response(body: &[u8]) -> Result reading_is_read = false, - b"changes" => in_changes = false, + "isread" => reading_is_read = false, + "changes" => in_changes = false, _ => {} } current_text = None; } Event::Text(t) => { - let text = t.decode().map(|c| c.into_owned()).unwrap_or_default(); + let text = t.into_inner().into_owned(); if reading_is_read { pending_is_read = Some(matches!(text.trim(), "true" | "1")); } @@ -906,7 +904,7 @@ pub fn parse_sync_folder_items_response(body: &[u8]) -> Result { - let text = String::from_utf8_lossy(c.as_ref()).into_owned(); + let text = c.into_inner().into_owned(); if current_text == Some("syncState") { sync_state = text; } @@ -964,48 +962,48 @@ pub fn parse_message_item(inner_xml: &str) -> Result { let ns_kind = classify(&ns); match ev { Event::Start(ref e) | Event::Empty(ref e) => { - let local = e.local_name().as_ref().to_vec(); + let local = e.local_name().as_ref().to_owned(); let is_empty = matches!(ev, Event::Empty(_)); if item.element.is_empty() && is_item_element(&local) { - item.element = String::from_utf8_lossy(&local).into_owned(); + item.element = local.clone(); } else if ns_kind == Ns::Types { cur.clear(); - if local.eq_ignore_ascii_case(b"ItemId") { + if local.eq_ignore_ascii_case("ItemId") { capture_id_attrs(e, &mut item.id.id, &mut item.id.change_key); - } else if local.eq_ignore_ascii_case(b"ParentFolderId") { + } else if local.eq_ignore_ascii_case("ParentFolderId") { let mut pid = String::new(); let mut ck = String::new(); capture_id_attrs(e, &mut pid, &mut ck); if !pid.is_empty() { item.parent_folder_id = Some(pid); } - } else if local.eq_ignore_ascii_case(b"MimeContent") { - mime_charset = attr_value(e, b"CharacterSet"); + } else if local.eq_ignore_ascii_case("MimeContent") { + mime_charset = attr_value(e, "CharacterSet"); current = Some("mimeContent"); - } else if local.eq_ignore_ascii_case(b"Subject") { + } else if local.eq_ignore_ascii_case("Subject") { current = Some("subject"); - } else if local.eq_ignore_ascii_case(b"DateTimeReceived") { + } else if local.eq_ignore_ascii_case("DateTimeReceived") { current = Some("received"); - } else if local.eq_ignore_ascii_case(b"IsRead") { + } else if local.eq_ignore_ascii_case("IsRead") { current = Some("isRead"); - } else if local.eq_ignore_ascii_case(b"IsDraft") { + } else if local.eq_ignore_ascii_case("IsDraft") { current = Some("isDraft"); - } else if local.eq_ignore_ascii_case(b"IsReadReceiptRequested") { + } else if local.eq_ignore_ascii_case("IsReadReceiptRequested") { current = Some("readReceipt"); - } else if local.eq_ignore_ascii_case(b"Categories") { + } else if local.eq_ignore_ascii_case("Categories") { category_collecting = true; - } else if category_collecting && local.eq_ignore_ascii_case(b"String") { + } else if category_collecting && local.eq_ignore_ascii_case("String") { current = Some("category"); - } else if local.eq_ignore_ascii_case(b"Flag") { + } else if local.eq_ignore_ascii_case("Flag") { in_flag = true; - } else if in_flag && local.eq_ignore_ascii_case(b"FlagStatus") { + } else if in_flag && local.eq_ignore_ascii_case("FlagStatus") { current = Some("flagStatus"); - } else if local.eq_ignore_ascii_case(b"ExtendedFieldURI") { - if let Some(tag) = attr_value(e, b"PropertyTag") { + } else if local.eq_ignore_ascii_case("ExtendedFieldURI") { + if let Some(tag) = attr_value(e, "PropertyTag") { let t = tag.trim().to_ascii_lowercase(); in_flagstatus_ext = t == "0x1090" || t == "4240"; } - } else if in_flagstatus_ext && local.eq_ignore_ascii_case(b"Value") { + } else if in_flagstatus_ext && local.eq_ignore_ascii_case("Value") { current = Some("flagStatusExt"); } else { current = None; @@ -1016,7 +1014,7 @@ pub fn parse_message_item(inner_xml: &str) -> Result { } } Event::End(e) => { - let local = e.local_name().as_ref().to_vec(); + let local = e.local_name().as_ref().to_owned(); let lower = local.to_ascii_lowercase(); if let Some(field) = current.take() { let text = std::mem::take(&mut cur); @@ -1042,19 +1040,19 @@ pub fn parse_message_item(inner_xml: &str) -> Result { } } cur.clear(); - if lower == b"categories" { + if lower == "categories" { category_collecting = false; - } else if lower == b"flag" { + } else if lower == "flag" { in_flag = false; - } else if lower == b"extendedproperty" { + } else if lower == "extendedproperty" { in_flagstatus_ext = false; } } Event::Text(ref t) => { - cur.push_str(&t.decode().map(|c| c.into_owned()).unwrap_or_default()); + cur.push_str(t); } Event::CData(ref c) => { - cur.push_str(&String::from_utf8_lossy(c.as_ref())); + cur.push_str(c); } Event::GeneralRef(ref g) => { if let Some(c) = entity_to_char(g) { @@ -1198,7 +1196,7 @@ pub fn parse_calendar_item(inner_xml: &str) -> Result let mut deleted_stack: Vec = Vec::new(); let mut in_modified = false; let mut in_deleted = false; - let mut recurrence_path: Vec> = Vec::new(); + let mut recurrence_path: Vec = Vec::new(); let mut recurrence_text: Option<&'static str> = None; let mut recurrence = RawRecurrence::default(); let mut pending = PendingRecurrence::default(); @@ -1210,10 +1208,10 @@ pub fn parse_calendar_item(inner_xml: &str) -> Result let ns_kind = classify(&ns); match ev { Event::Start(ref e) | Event::Empty(ref e) => { - let local = e.local_name().as_ref().to_vec(); + let local = e.local_name().as_ref().to_owned(); let is_empty = matches!(ev, Event::Empty(_)); if item.element.is_empty() && is_item_element(&local) { - item.element = String::from_utf8_lossy(&local).into_owned(); + item.element = local.clone(); continue; } if ns_kind != Ns::Types { @@ -1221,39 +1219,39 @@ pub fn parse_calendar_item(inner_xml: &str) -> Result } if !recurrence_path.is_empty() { recurrence_path.push(local.clone()); - if local.eq_ignore_ascii_case(b"Interval") { + if local.eq_ignore_ascii_case("Interval") { recurrence_text = Some("interval"); - } else if local.eq_ignore_ascii_case(b"DaysOfWeek") { + } else if local.eq_ignore_ascii_case("DaysOfWeek") { recurrence_text = Some("daysOfWeek"); - } else if local.eq_ignore_ascii_case(b"DayOfMonth") { + } else if local.eq_ignore_ascii_case("DayOfMonth") { recurrence_text = Some("dayOfMonth"); - } else if local.eq_ignore_ascii_case(b"DayOfWeekIndex") { + } else if local.eq_ignore_ascii_case("DayOfWeekIndex") { recurrence_text = Some("dayOfWeekIndex"); - } else if local.eq_ignore_ascii_case(b"Month") { + } else if local.eq_ignore_ascii_case("Month") { recurrence_text = Some("month"); - } else if local.eq_ignore_ascii_case(b"StartDate") { + } else if local.eq_ignore_ascii_case("StartDate") { recurrence_text = Some("startDate"); - } else if local.eq_ignore_ascii_case(b"EndDate") { + } else if local.eq_ignore_ascii_case("EndDate") { recurrence_text = Some("endDate"); - } else if local.eq_ignore_ascii_case(b"NumberOfOccurrences") { + } else if local.eq_ignore_ascii_case("NumberOfOccurrences") { recurrence_text = Some("numberOfOccurrences"); - } else if local.eq_ignore_ascii_case(b"DailyRecurrence") { + } else if local.eq_ignore_ascii_case("DailyRecurrence") { pending.pattern_choice = Some("Daily"); - } else if local.eq_ignore_ascii_case(b"WeeklyRecurrence") { + } else if local.eq_ignore_ascii_case("WeeklyRecurrence") { pending.pattern_choice = Some("Weekly"); - } else if local.eq_ignore_ascii_case(b"AbsoluteMonthlyRecurrence") { + } else if local.eq_ignore_ascii_case("AbsoluteMonthlyRecurrence") { pending.pattern_choice = Some("AbsoluteMonthly"); - } else if local.eq_ignore_ascii_case(b"RelativeMonthlyRecurrence") { + } else if local.eq_ignore_ascii_case("RelativeMonthlyRecurrence") { pending.pattern_choice = Some("RelativeMonthly"); - } else if local.eq_ignore_ascii_case(b"AbsoluteYearlyRecurrence") { + } else if local.eq_ignore_ascii_case("AbsoluteYearlyRecurrence") { pending.pattern_choice = Some("AbsoluteYearly"); - } else if local.eq_ignore_ascii_case(b"RelativeYearlyRecurrence") { + } else if local.eq_ignore_ascii_case("RelativeYearlyRecurrence") { pending.pattern_choice = Some("RelativeYearly"); - } else if local.eq_ignore_ascii_case(b"NoEndRecurrence") { + } else if local.eq_ignore_ascii_case("NoEndRecurrence") { pending.range_choice = Some("NoEnd"); - } else if local.eq_ignore_ascii_case(b"EndDateRecurrence") { + } else if local.eq_ignore_ascii_case("EndDateRecurrence") { pending.range_choice = Some("EndDate"); - } else if local.eq_ignore_ascii_case(b"NumberedRecurrence") { + } else if local.eq_ignore_ascii_case("NumberedRecurrence") { pending.range_choice = Some("Numbered"); } else { recurrence_text = None; @@ -1265,7 +1263,7 @@ pub fn parse_calendar_item(inner_xml: &str) -> Result continue; } cur.clear(); - if local.eq_ignore_ascii_case(b"ItemId") { + if local.eq_ignore_ascii_case("ItemId") { if in_modified { if let Some(occ) = occurrence_stack.last_mut() { capture_id_attrs(e, &mut occ.item_id.id, &mut occ.item_id.change_key); @@ -1277,127 +1275,126 @@ pub fn parse_calendar_item(inner_xml: &str) -> Result } else { capture_id_attrs(e, &mut item.id.id, &mut item.id.change_key); } - } else if local.eq_ignore_ascii_case(b"ParentFolderId") { + } else if local.eq_ignore_ascii_case("ParentFolderId") { let mut pid = String::new(); let mut ck = String::new(); capture_id_attrs(e, &mut pid, &mut ck); if !pid.is_empty() { item.parent_folder_id = Some(pid); } - } else if local.eq_ignore_ascii_case(b"UID") { + } else if local.eq_ignore_ascii_case("UID") { text_target = Some("uid"); - } else if local.eq_ignore_ascii_case(b"Subject") { + } else if local.eq_ignore_ascii_case("Subject") { text_target = Some("subject"); - } else if local.eq_ignore_ascii_case(b"Start") { + } else if local.eq_ignore_ascii_case("Start") { if in_modified || in_deleted { text_target = Some("occStart"); } else { text_target = Some("start"); } - } else if local.eq_ignore_ascii_case(b"End") { + } else if local.eq_ignore_ascii_case("End") { if in_modified || in_deleted { text_target = Some("occEnd"); } else { text_target = Some("end"); } - } else if local.eq_ignore_ascii_case(b"OriginalStart") { + } else if local.eq_ignore_ascii_case("OriginalStart") { if in_modified || in_deleted { text_target = Some("occOrig"); } else { text_target = Some("originalStart"); } - } else if local.eq_ignore_ascii_case(b"IsAllDayEvent") { + } else if local.eq_ignore_ascii_case("IsAllDayEvent") { text_target = Some("isAllDay"); - } else if local.eq_ignore_ascii_case(b"LegacyFreeBusyStatus") { + } else if local.eq_ignore_ascii_case("LegacyFreeBusyStatus") { text_target = Some("freeBusy"); - } else if local.eq_ignore_ascii_case(b"Location") { + } else if local.eq_ignore_ascii_case("Location") { text_target = Some("location"); - } else if local.eq_ignore_ascii_case(b"CalendarItemType") { + } else if local.eq_ignore_ascii_case("CalendarItemType") { text_target = Some("calendarItemType"); - } else if local.eq_ignore_ascii_case(b"RecurrenceId") { + } else if local.eq_ignore_ascii_case("RecurrenceId") { text_target = Some("recurrenceId"); - } else if local.eq_ignore_ascii_case(b"StartTimeZone") { - if let Some(v) = attr_value(e, b"Id") { + } else if local.eq_ignore_ascii_case("StartTimeZone") { + if let Some(v) = attr_value(e, "Id") { item.start_tz = Some(v); } - } else if local.eq_ignore_ascii_case(b"EndTimeZone") { - if let Some(v) = attr_value(e, b"Id") { + } else if local.eq_ignore_ascii_case("EndTimeZone") { + if let Some(v) = attr_value(e, "Id") { item.end_tz = Some(v); } - } else if local.eq_ignore_ascii_case(b"Recurrence") { + } else if local.eq_ignore_ascii_case("Recurrence") { recurrence_path.push(local.clone()); - } else if local.eq_ignore_ascii_case(b"ModifiedOccurrences") { + } else if local.eq_ignore_ascii_case("ModifiedOccurrences") { in_modified = true; - } else if local.eq_ignore_ascii_case(b"DeletedOccurrences") { + } else if local.eq_ignore_ascii_case("DeletedOccurrences") { in_deleted = true; - } else if local.eq_ignore_ascii_case(b"Occurrence") { + } else if local.eq_ignore_ascii_case("Occurrence") { let mut occ = RawOccurrence::default(); capture_id_attrs(e, &mut occ.item_id.id, &mut occ.item_id.change_key); occurrence_stack.push(occ); - } else if local.eq_ignore_ascii_case(b"DeletedOccurrence") { + } else if local.eq_ignore_ascii_case("DeletedOccurrence") { deleted_stack.push(RawOccurrence::default()); - } else if local.eq_ignore_ascii_case(b"Organizer") { + } else if local.eq_ignore_ascii_case("Organizer") { in_organizer = true; - } else if local.eq_ignore_ascii_case(b"RequiredAttendees") { + } else if local.eq_ignore_ascii_case("RequiredAttendees") { attendee_kind = Some("required"); - } else if local.eq_ignore_ascii_case(b"OptionalAttendees") { + } else if local.eq_ignore_ascii_case("OptionalAttendees") { attendee_kind = Some("optional"); - } else if local.eq_ignore_ascii_case(b"Resources") { + } else if local.eq_ignore_ascii_case("Resources") { attendee_kind = Some("resource"); - } else if local.eq_ignore_ascii_case(b"ReminderIsSet") { + } else if local.eq_ignore_ascii_case("ReminderIsSet") { text_target = Some("reminderIsSet"); - } else if local.eq_ignore_ascii_case(b"ReminderMinutesBeforeStart") { + } else if local.eq_ignore_ascii_case("ReminderMinutesBeforeStart") { text_target = Some("reminderMinutes"); - } else if local.eq_ignore_ascii_case(b"IsOnlineMeeting") { + } else if local.eq_ignore_ascii_case("IsOnlineMeeting") { text_target = Some("isOnlineMeeting"); - } else if local.eq_ignore_ascii_case(b"JoinOnlineMeetingUrl") { + } else if local.eq_ignore_ascii_case("JoinOnlineMeetingUrl") { text_target = Some("joinUrl"); - } else if local.eq_ignore_ascii_case(b"NetShowUrl") { + } else if local.eq_ignore_ascii_case("NetShowUrl") { text_target = Some("netShowUrl"); - } else if local.eq_ignore_ascii_case(b"MeetingWorkspaceUrl") { + } else if local.eq_ignore_ascii_case("MeetingWorkspaceUrl") { text_target = Some("workspaceUrl"); - } else if local.eq_ignore_ascii_case(b"Attendee") && attendee_kind.is_some() { + } else if local.eq_ignore_ascii_case("Attendee") && attendee_kind.is_some() { current_attendee = Some(RawAttendee::default()); - } else if local.eq_ignore_ascii_case(b"Mailbox") { + } else if local.eq_ignore_ascii_case("Mailbox") { in_mailbox = true; - } else if local.eq_ignore_ascii_case(b"Name") && in_mailbox { + } else if local.eq_ignore_ascii_case("Name") && in_mailbox { text_target = Some(if in_organizer { "organizerName" } else { "attendeeName" }); - } else if local.eq_ignore_ascii_case(b"EmailAddress") && in_mailbox { + } else if local.eq_ignore_ascii_case("EmailAddress") && in_mailbox { text_target = Some(if in_organizer { "organizerEmail" } else { "attendeeEmail" }); - } else if local.eq_ignore_ascii_case(b"RoutingType") && in_mailbox { + } else if local.eq_ignore_ascii_case("RoutingType") && in_mailbox { text_target = Some(if in_organizer { "organizerRouting" } else { "attendeeRouting" }); - } else if local.eq_ignore_ascii_case(b"ResponseType") && current_attendee.is_some() - { + } else if local.eq_ignore_ascii_case("ResponseType") && current_attendee.is_some() { text_target = Some("attendeeResponse"); - } else if local.eq_ignore_ascii_case(b"Categories") { + } else if local.eq_ignore_ascii_case("Categories") { category_collecting = true; - } else if category_collecting && local.eq_ignore_ascii_case(b"String") { + } else if category_collecting && local.eq_ignore_ascii_case("String") { text_target = Some("category"); - } else if local.eq_ignore_ascii_case(b"DateTimeCreated") { + } else if local.eq_ignore_ascii_case("DateTimeCreated") { text_target = Some("created"); - } else if local.eq_ignore_ascii_case(b"LastModifiedTime") { + } else if local.eq_ignore_ascii_case("LastModifiedTime") { text_target = Some("lastModified"); - } else if local.eq_ignore_ascii_case(b"Body") { - let body_type = attr_value(e, b"BodyType").unwrap_or_default(); + } else if local.eq_ignore_ascii_case("Body") { + let body_type = attr_value(e, "BodyType").unwrap_or_default(); text_target = Some(if body_type.eq_ignore_ascii_case("HTML") { "bodyHtml" } else { "bodyText" }); - } else if local.eq_ignore_ascii_case(b"FileAttachment") - || local.eq_ignore_ascii_case(b"ItemAttachment") + } else if local.eq_ignore_ascii_case("FileAttachment") + || local.eq_ignore_ascii_case("ItemAttachment") { let att = parse_attachment_ref(&mut xml, &local)?; item.attachments.push(att); @@ -1409,7 +1406,7 @@ pub fn parse_calendar_item(inner_xml: &str) -> Result } } Event::End(e) => { - let local = e.local_name().as_ref().to_vec(); + let local = e.local_name().as_ref().to_owned(); let lower = local.to_ascii_lowercase(); if !recurrence_path.is_empty() { recurrence_path.pop(); @@ -1501,20 +1498,20 @@ pub fn parse_calendar_item(inner_xml: &str) -> Result } cur.clear(); text_target = None; - match lower.as_slice() { - b"modifiedoccurrences" => in_modified = false, - b"deletedoccurrences" => in_deleted = false, - b"occurrence" => { + match lower.as_str() { + "modifiedoccurrences" => in_modified = false, + "deletedoccurrences" => in_deleted = false, + "occurrence" => { if let Some(occ) = occurrence_stack.pop() { item.modified_occurrences.push(occ); } } - b"deletedoccurrence" => { + "deletedoccurrence" => { if let Some(occ) = deleted_stack.pop() { item.deleted_occurrences.push(occ); } } - b"attendee" => { + "attendee" => { if let Some(att) = current_attendee.take() { match attendee_kind { Some("required") => item.required_attendees.push(att), @@ -1524,17 +1521,15 @@ pub fn parse_calendar_item(inner_xml: &str) -> Result } } } - b"requiredattendees" | b"optionalattendees" | b"resources" => { - attendee_kind = None - } - b"organizer" => in_organizer = false, - b"mailbox" => in_mailbox = false, - b"categories" => category_collecting = false, + "requiredattendees" | "optionalattendees" | "resources" => attendee_kind = None, + "organizer" => in_organizer = false, + "mailbox" => in_mailbox = false, + "categories" => category_collecting = false, _ => {} } } Event::Text(ref t) => { - let text = t.decode().map(|c| c.into_owned()).unwrap_or_default(); + let text = t.as_ref().to_owned(); if !recurrence_path.is_empty() { match recurrence_text { Some("interval") => pending.interval = text.trim().parse().unwrap_or(1), @@ -1563,7 +1558,7 @@ pub fn parse_calendar_item(inner_xml: &str) -> Result } Event::CData(ref c) => { if recurrence_path.is_empty() { - cur.push_str(&String::from_utf8_lossy(c.as_ref())); + cur.push_str(c); } } Event::GeneralRef(ref g) => { @@ -1718,14 +1713,14 @@ pub fn parse_contact_item(inner_xml: &str) -> Result { let ns_kind = classify(&ns); match ev { Event::Start(ref e) | Event::Empty(ref e) => { - let local = e.local_name().as_ref().to_vec(); + let local = e.local_name().as_ref().to_owned(); let is_empty = matches!(ev, Event::Empty(_)); if !seen_root_contact - && (local.eq_ignore_ascii_case(b"Contact") - || local.eq_ignore_ascii_case(b"DistributionList")) + && (local.eq_ignore_ascii_case("Contact") + || local.eq_ignore_ascii_case("DistributionList")) { seen_root_contact = true; - if local.eq_ignore_ascii_case(b"DistributionList") { + if local.eq_ignore_ascii_case("DistributionList") { item.is_group = true; } continue; @@ -1734,91 +1729,91 @@ pub fn parse_contact_item(inner_xml: &str) -> Result { continue; } cur.clear(); - if local.eq_ignore_ascii_case(b"ItemId") { + if local.eq_ignore_ascii_case("ItemId") { capture_id_attrs(e, &mut item.id.id, &mut item.id.change_key); - } else if local.eq_ignore_ascii_case(b"ParentFolderId") { + } else if local.eq_ignore_ascii_case("ParentFolderId") { let mut pid = String::new(); let mut ck = String::new(); capture_id_attrs(e, &mut pid, &mut ck); if !pid.is_empty() { item.parent_folder_id = Some(pid); } - } else if local.eq_ignore_ascii_case(b"DisplayName") { + } else if local.eq_ignore_ascii_case("DisplayName") { text_target = Some("displayName"); - } else if local.eq_ignore_ascii_case(b"GivenName") { + } else if local.eq_ignore_ascii_case("GivenName") { text_target = Some("givenName"); - } else if local.eq_ignore_ascii_case(b"MiddleName") { + } else if local.eq_ignore_ascii_case("MiddleName") { text_target = Some("middleName"); - } else if local.eq_ignore_ascii_case(b"Surname") { + } else if local.eq_ignore_ascii_case("Surname") { text_target = Some("surname"); - } else if local.eq_ignore_ascii_case(b"Initials") { + } else if local.eq_ignore_ascii_case("Initials") { text_target = Some("initials"); - } else if local.eq_ignore_ascii_case(b"Nickname") { + } else if local.eq_ignore_ascii_case("Nickname") { text_target = Some("nickname"); - } else if local.eq_ignore_ascii_case(b"CompanyName") { + } else if local.eq_ignore_ascii_case("CompanyName") { text_target = Some("companyName"); - } else if local.eq_ignore_ascii_case(b"Department") { + } else if local.eq_ignore_ascii_case("Department") { text_target = Some("department"); - } else if local.eq_ignore_ascii_case(b"JobTitle") { + } else if local.eq_ignore_ascii_case("JobTitle") { text_target = Some("jobTitle"); - } else if local.eq_ignore_ascii_case(b"Generation") { + } else if local.eq_ignore_ascii_case("Generation") { text_target = Some("generation"); - } else if local.eq_ignore_ascii_case(b"OfficeLocation") { + } else if local.eq_ignore_ascii_case("OfficeLocation") { text_target = Some("officeLocation"); - } else if local.eq_ignore_ascii_case(b"BusinessHomePage") { + } else if local.eq_ignore_ascii_case("BusinessHomePage") { text_target = Some("url"); - } else if local.eq_ignore_ascii_case(b"Birthday") { + } else if local.eq_ignore_ascii_case("Birthday") { text_target = Some("birthday"); - } else if local.eq_ignore_ascii_case(b"WeddingAnniversary") { + } else if local.eq_ignore_ascii_case("WeddingAnniversary") { text_target = Some("weddingAnniversary"); - } else if local.eq_ignore_ascii_case(b"Manager") { + } else if local.eq_ignore_ascii_case("Manager") { text_target = Some("manager"); - } else if local.eq_ignore_ascii_case(b"SpouseName") { + } else if local.eq_ignore_ascii_case("SpouseName") { text_target = Some("spouse"); - } else if local.eq_ignore_ascii_case(b"AssistantName") { + } else if local.eq_ignore_ascii_case("AssistantName") { text_target = Some("assistant"); - } else if local.eq_ignore_ascii_case(b"Profession") { + } else if local.eq_ignore_ascii_case("Profession") { text_target = Some("profession"); - } else if local.eq_ignore_ascii_case(b"PostalAddressIndex") { + } else if local.eq_ignore_ascii_case("PostalAddressIndex") { text_target = Some("postalAddressIndex"); - } else if local.eq_ignore_ascii_case(b"Members") { + } else if local.eq_ignore_ascii_case("Members") { in_members = true; - } else if in_members && local.eq_ignore_ascii_case(b"Member") { + } else if in_members && local.eq_ignore_ascii_case("Member") { current_member = Some(RawGroupMember::default()); - } else if in_members && local.eq_ignore_ascii_case(b"Mailbox") { + } else if in_members && local.eq_ignore_ascii_case("Mailbox") { member_mailbox = true; - } else if member_mailbox && local.eq_ignore_ascii_case(b"Name") { + } else if member_mailbox && local.eq_ignore_ascii_case("Name") { text_target = Some("memberName"); - } else if member_mailbox && local.eq_ignore_ascii_case(b"EmailAddress") { + } else if member_mailbox && local.eq_ignore_ascii_case("EmailAddress") { text_target = Some("memberEmail"); - } else if local.eq_ignore_ascii_case(b"Body") { + } else if local.eq_ignore_ascii_case("Body") { text_target = Some("notes"); - } else if local.eq_ignore_ascii_case(b"DateTimeCreated") { + } else if local.eq_ignore_ascii_case("DateTimeCreated") { text_target = Some("created"); - } else if local.eq_ignore_ascii_case(b"LastModifiedTime") { + } else if local.eq_ignore_ascii_case("LastModifiedTime") { text_target = Some("lastModified"); - } else if local.eq_ignore_ascii_case(b"Categories") { + } else if local.eq_ignore_ascii_case("Categories") { category_collecting = true; - } else if category_collecting && local.eq_ignore_ascii_case(b"String") { + } else if category_collecting && local.eq_ignore_ascii_case("String") { text_target = Some("category"); - } else if local.eq_ignore_ascii_case(b"Children") { + } else if local.eq_ignore_ascii_case("Children") { children_collecting = true; - } else if children_collecting && local.eq_ignore_ascii_case(b"String") { + } else if children_collecting && local.eq_ignore_ascii_case("String") { text_target = Some("child"); - } else if local.eq_ignore_ascii_case(b"Companies") { + } else if local.eq_ignore_ascii_case("Companies") { companies_collecting = true; - } else if companies_collecting && local.eq_ignore_ascii_case(b"String") { + } else if companies_collecting && local.eq_ignore_ascii_case("String") { text_target = Some("company"); - } else if local.eq_ignore_ascii_case(b"EmailAddresses") { + } else if local.eq_ignore_ascii_case("EmailAddresses") { entry_container = Some("email"); - } else if local.eq_ignore_ascii_case(b"PhoneNumbers") { + } else if local.eq_ignore_ascii_case("PhoneNumbers") { entry_container = Some("phone"); - } else if local.eq_ignore_ascii_case(b"ImAddresses") { + } else if local.eq_ignore_ascii_case("ImAddresses") { entry_container = Some("im"); - } else if local.eq_ignore_ascii_case(b"PhysicalAddresses") { + } else if local.eq_ignore_ascii_case("PhysicalAddresses") { entry_container = Some("address"); - } else if local.eq_ignore_ascii_case(b"Entry") { - entry_key = attr_value(e, b"Key"); + } else if local.eq_ignore_ascii_case("Entry") { + entry_key = attr_value(e, "Key"); if entry_container == Some("address") { current_address = Some(RawContactAddress { key: entry_key.clone().unwrap_or_default(), @@ -1828,21 +1823,21 @@ pub fn parse_contact_item(inner_xml: &str) -> Result { text_target = Some("entryValue"); } } else if current_address.is_some() { - if local.eq_ignore_ascii_case(b"Street") { + if local.eq_ignore_ascii_case("Street") { address_text = Some("street"); - } else if local.eq_ignore_ascii_case(b"City") { + } else if local.eq_ignore_ascii_case("City") { address_text = Some("city"); - } else if local.eq_ignore_ascii_case(b"State") { + } else if local.eq_ignore_ascii_case("State") { address_text = Some("state"); - } else if local.eq_ignore_ascii_case(b"CountryOrRegion") { + } else if local.eq_ignore_ascii_case("CountryOrRegion") { address_text = Some("country"); - } else if local.eq_ignore_ascii_case(b"PostalCode") { + } else if local.eq_ignore_ascii_case("PostalCode") { address_text = Some("postal"); } else { address_text = None; } - } else if local.eq_ignore_ascii_case(b"FileAttachment") - || local.eq_ignore_ascii_case(b"ItemAttachment") + } else if local.eq_ignore_ascii_case("FileAttachment") + || local.eq_ignore_ascii_case("ItemAttachment") { let att = parse_attachment_ref(&mut xml, &local)?; item.attachments.push(att); @@ -1855,7 +1850,7 @@ pub fn parse_contact_item(inner_xml: &str) -> Result { } } Event::End(e) => { - let local = e.local_name().as_ref().to_vec(); + let local = e.local_name().as_ref().to_owned(); let lower = local.to_ascii_lowercase(); if let Some(at) = address_text { if let Some(addr) = current_address.as_mut() { @@ -1922,22 +1917,22 @@ pub fn parse_contact_item(inner_xml: &str) -> Result { cur.clear(); text_target = None; address_text = None; - match lower.as_slice() { - b"categories" => category_collecting = false, - b"children" => children_collecting = false, - b"companies" => companies_collecting = false, - b"emailaddresses" | b"phonenumbers" | b"imaddresses" | b"physicaladdresses" => { + match lower.as_str() { + "categories" => category_collecting = false, + "children" => children_collecting = false, + "companies" => companies_collecting = false, + "emailaddresses" | "phonenumbers" | "imaddresses" | "physicaladdresses" => { entry_container = None } - b"entry" => { + "entry" => { if let Some(addr) = current_address.take() { item.addresses.push(addr); } entry_key = None; } - b"members" => in_members = false, - b"mailbox" => member_mailbox = false, - b"member" => { + "members" => in_members = false, + "mailbox" => member_mailbox = false, + "member" => { if let Some(m) = current_member.take() && (m.email.is_some() || m.name.is_some()) { @@ -1948,10 +1943,10 @@ pub fn parse_contact_item(inner_xml: &str) -> Result { } } Event::Text(ref t) => { - cur.push_str(&t.decode().map(|c| c.into_owned()).unwrap_or_default()); + cur.push_str(t); } Event::CData(ref c) => { - cur.push_str(&String::from_utf8_lossy(c.as_ref())); + cur.push_str(c); } Event::GeneralRef(ref g) => { if let Some(c) = entity_to_char(g) { @@ -1967,10 +1962,10 @@ pub fn parse_contact_item(inner_xml: &str) -> Result { fn parse_attachment_ref( xml: &mut NsReader, - element_local: &[u8], + element_local: &str, ) -> Result { let mut att = RawAttachmentRef { - is_item_attachment: element_local.eq_ignore_ascii_case(b"itemattachment"), + is_item_attachment: element_local.eq_ignore_ascii_case("itemattachment"), ..RawAttachmentRef::default() }; let mut buf = Vec::new(); @@ -1987,18 +1982,18 @@ fn parse_attachment_ref( if !is_empty { depth += 1; } - let local = e.local_name().as_ref().to_vec(); + let local = e.local_name().as_ref().to_owned(); if ns_kind == Ns::Types { cur.clear(); - if local.eq_ignore_ascii_case(b"AttachmentId") { - if let Some(v) = attr_value(e, b"Id") { + if local.eq_ignore_ascii_case("AttachmentId") { + if let Some(v) = attr_value(e, "Id") { att.attachment_id = v; } - } else if local.eq_ignore_ascii_case(b"Name") { + } else if local.eq_ignore_ascii_case("Name") { current = Some("name"); - } else if local.eq_ignore_ascii_case(b"ContentType") { + } else if local.eq_ignore_ascii_case("ContentType") { current = Some("contentType"); - } else if local.eq_ignore_ascii_case(b"IsContactPhoto") { + } else if local.eq_ignore_ascii_case("IsContactPhoto") { current = Some("isContactPhoto"); } else { current = None; @@ -2030,10 +2025,10 @@ fn parse_attachment_ref( } } Event::Text(ref t) => { - cur.push_str(&t.decode().map(|c| c.into_owned()).unwrap_or_default()); + cur.push_str(t); } Event::CData(ref c) => { - cur.push_str(&String::from_utf8_lossy(c.as_ref())); + cur.push_str(c); } Event::GeneralRef(ref g) => { if let Some(c) = entity_to_char(g) { @@ -2069,27 +2064,27 @@ pub fn parse_get_attachment_inline(body: &[u8]) -> Result { - let local = e.local_name().as_ref().to_vec(); + let local = e.local_name().as_ref().to_owned(); let is_empty = matches!(ev, Event::Empty(_)); if ns_kind == Ns::Types - && (local.eq_ignore_ascii_case(b"FileAttachment") - || local.eq_ignore_ascii_case(b"ItemAttachment")) + && (local.eq_ignore_ascii_case("FileAttachment") + || local.eq_ignore_ascii_case("ItemAttachment")) { current = Some(GetAttachmentInline::default()); } else if let Some(cur) = current.as_mut() && ns_kind == Ns::Types { - if local.eq_ignore_ascii_case(b"AttachmentId") { - if let Some(v) = attr_value(e, b"Id") { + if local.eq_ignore_ascii_case("AttachmentId") { + if let Some(v) = attr_value(e, "Id") { cur.attachment_id = v; } - } else if local.eq_ignore_ascii_case(b"Name") { + } else if local.eq_ignore_ascii_case("Name") { text_target = Some("name"); - } else if local.eq_ignore_ascii_case(b"ContentType") { + } else if local.eq_ignore_ascii_case("ContentType") { text_target = Some("contentType"); - } else if local.eq_ignore_ascii_case(b"IsContactPhoto") { + } else if local.eq_ignore_ascii_case("IsContactPhoto") { text_target = Some("isContactPhoto"); - } else if local.eq_ignore_ascii_case(b"Content") { + } else if local.eq_ignore_ascii_case("Content") { text_target = Some("content"); } else { text_target = None; @@ -2100,9 +2095,9 @@ pub fn parse_get_attachment_inline(body: &[u8]) -> Result { - let local = e.local_name().as_ref().to_vec(); - if (local.eq_ignore_ascii_case(b"FileAttachment") - || local.eq_ignore_ascii_case(b"ItemAttachment")) + let local = e.local_name().as_ref().to_owned(); + if (local.eq_ignore_ascii_case("FileAttachment") + || local.eq_ignore_ascii_case("ItemAttachment")) && let Some(att) = current.take() { out.push(att); @@ -2111,8 +2106,8 @@ pub fn parse_get_attachment_inline(body: &[u8]) -> Result { let text = match ev { - Event::Text(ref t) => t.decode().map(|c| c.into_owned()).unwrap_or_default(), - Event::CData(ref c) => String::from_utf8_lossy(c.as_ref()).into_owned(), + Event::Text(ref t) => t.as_ref().to_owned(), + Event::CData(ref c) => c.as_ref().to_owned(), _ => unreachable!(), }; @@ -2261,7 +2256,7 @@ mod tests { \ " ); - 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!(r[0].success); assert!(r[0].inner_xml.contains("{message_xml}\ " ); - let r = parse_response_messages(body.as_bytes(), b"GetItemResponseMessage").unwrap(); + let r = parse_response_messages(body.as_bytes(), "GetItemResponseMessage").unwrap(); parse_message_item(&r[0].inner_xml).unwrap() } @@ -2323,7 +2318,7 @@ mod tests { false\ " ); - let r = parse_response_messages(body.as_bytes(), b"GetItemResponseMessage").unwrap(); + let r = parse_response_messages(body.as_bytes(), "GetItemResponseMessage").unwrap(); let p = parse_message_item(&r[0].inner_xml).unwrap(); assert_eq!( p.flag_status.as_deref(), @@ -2628,7 +2623,7 @@ mod tests { AT&T\ " ); - let msgs = parse_response_messages(body.as_bytes(), b"GetItemResponseMessage").unwrap(); + let msgs = parse_response_messages(body.as_bytes(), "GetItemResponseMessage").unwrap(); assert!( msgs[0].inner_xml.contains("AT&T") || msgs[0].inner_xml.contains("AT&T"), "capture must preserve the entity for the per-item parser: {}", @@ -2684,7 +2679,7 @@ mod tests { \ " ); - let msgs = parse_response_messages(body.as_bytes(), b"GetItemResponseMessage").unwrap(); + let msgs = parse_response_messages(body.as_bytes(), "GetItemResponseMessage").unwrap(); assert_eq!(msgs.len(), 1); let item = parse_message_item(&msgs[0].inner_xml).unwrap(); assert_eq!(item.id.id, "NP1"); @@ -2758,7 +2753,7 @@ mod tests { \ " ); - 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(), 1); assert!(r[0].success, "Warning should be success-equivalent"); } @@ -2776,7 +2771,7 @@ mod tests { ErrorAccessDenied\ " ); - 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(), 2); assert!(msgs[0].success); let f = parse_folder_inner(&msgs[0].inner_xml).unwrap().unwrap(); diff --git a/src/exchange_graph/oauth.rs b/src/exchange_graph/oauth.rs index c4b4ec5..b240111 100644 --- a/src/exchange_graph/oauth.rs +++ b/src/exchange_graph/oauth.rs @@ -7,8 +7,7 @@ use std::io::{self, Write}; use std::time::{Duration, Instant}; -use base64::Engine; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use encodify::base64::{Base64, Padding, URL_SAFE}; use serde_json::Value; use ureq::config::Config; use ureq::tls::{RootCerts, TlsConfig}; @@ -18,6 +17,8 @@ use crate::exchange_graph::error::GraphError; pub const SCOPES: &str = "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 { format!("https://login.microsoftonline.com/{tenant}") } @@ -53,11 +54,7 @@ pub fn decode_jwt_claims(token: &str) -> Option { let mut parts = token.split('.'); let _header = parts.next()?; let payload = parts.next()?; - let bytes = URL_SAFE_NO_PAD - .decode(payload) - .or_else(|_| base64::engine::general_purpose::STANDARD_NO_PAD.decode(payload)) - .or_else(|_| base64::engine::general_purpose::STANDARD.decode(payload)) - .ok()?; + let bytes = JWT_SEGMENT.decode(payload).ok()?; let value: Value = serde_json::from_slice(&bytes).ok()?; Some(JwtClaims { tenant_id: value.get("tid").and_then(Value::as_str).map(str::to_owned), @@ -420,6 +417,7 @@ where #[cfg(test)] mod tests { use super::*; + use encodify::base64::{STANDARD, STANDARD_NO_PAD, URL_SAFE_NO_PAD}; fn make_jwt(tid: &str, upn: &str, exp: u64) -> String { let header = URL_SAFE_NO_PAD.encode(b"{\"alg\":\"none\"}"); @@ -437,6 +435,19 @@ mod tests { 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] fn malformed_token_returns_none() { assert!(decode_jwt_claims("garbage").is_none()); diff --git a/src/imap/client.rs b/src/imap/client.rs index eb374cd..066ce26 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -8,9 +8,6 @@ use std::collections::BTreeSet; use std::io::{BufReader, Read, Write}; use std::time::Instant; -use base64::Engine; -use base64::engine::general_purpose::STANDARD as BASE64; - use super::command::{self, CommandBuilder}; use super::error::{ImapError, NoError}; use super::response::{Response, Status, StatusLine, Untagged, parse_response}; @@ -201,9 +198,8 @@ impl ImapClient { payload.extend_from_slice(authcid.as_bytes()); payload.push(0); payload.extend_from_slice(password.as_bytes()); - let encoded = BASE64.encode(&payload); 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) { Ok(_) => Ok(()), Err(ImapError::No(no)) if no.is_auth_failed() => { @@ -218,9 +214,7 @@ impl ImapClient { let resp = parse_response(&mut self.reader)?; match resp { Response::Continuation(_) => { - let mut line = encoded.clone().into_bytes(); - line.extend_from_slice(b"\r\n"); - self.write_all(&line)?; + self.write_all(&command::sasl_response(&payload))?; } Response::Tagged { tag: t, line } if t == tag => { self.update_capabilities_from_code(&line); @@ -260,9 +254,8 @@ impl ImapClient { } fn drive_bearer_sasl(&mut self, mechanism: &str, payload: &[u8]) -> Result<(), ImapError> { - let encoded = BASE64.encode(payload); 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) { Ok(_) => Ok(()), Err(ImapError::No(no)) if no.is_auth_failed() => { @@ -281,9 +274,7 @@ impl ImapClient { if sent { self.write_all(b"\r\n")?; } else { - let mut line = encoded.clone().into_bytes(); - line.extend_from_slice(b"\r\n"); - self.write_all(&line)?; + self.write_all(&command::sasl_response(payload))?; sent = true; } } @@ -664,6 +655,16 @@ mod tests { 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] fn authenticate_plain_continuation_path() { let server = b"+ \r\nA0001 OK auth done\r\n"; diff --git a/src/imap/command.rs b/src/imap/command.rs index 0056bff..611bdd8 100644 --- a/src/imap/command.rs +++ b/src/imap/command.rs @@ -6,6 +6,11 @@ use std::fmt::Write as _; +use encodify::base64::STANDARD; + +const AUTHENTICATE: &str = "AUTHENTICATE "; +const CRLF: &[u8] = b"\r\n"; + pub struct CommandBuilder { next_tag: u32, } @@ -84,8 +89,22 @@ pub fn authenticate(mechanism: &str) -> String { format!("AUTHENTICATE {mechanism}") } -pub fn authenticate_with_ir(mechanism: &str, initial_response: &str) -> String { - format!("AUTHENTICATE {mechanism} {initial_response}") +pub fn authenticate_with_ir(mechanism: &str, initial_response: &[u8]) -> String { + 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 { + 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 { @@ -282,6 +301,24 @@ mod tests { 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] fn build_appends_crlf() { let mut b = CommandBuilder::new(); diff --git a/src/imap/name.rs b/src/imap/name.rs index cea7e82..66b8e38 100644 --- a/src/imap/name.rs +++ b/src/imap/name.rs @@ -4,10 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT */ +use encodify::utf7::{self, Utf7}; + use super::error::ImapError; -const MODIFIED_UTF7_ALPHABET: &[u8] = - b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,"; +const RECEIVED_NAME: Utf7 = utf7::IMAP.lenient(); pub fn canonicalise_inbox(name: &str) -> String { if name.eq_ignore_ascii_case("INBOX") { @@ -22,145 +23,17 @@ pub fn decode_mailbox_name(input: &str) -> Result { } pub fn decode_mailbox_name_with(input: &str, utf8_accept: bool) -> Result { - if utf8_accept { + if utf8_accept || ends_in_open_shift(input) { return Ok(input.to_owned()); } - if input.is_ascii() && !input.contains('&') { - return Ok(input.to_owned()); - } - 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 = 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) + RECEIVED_NAME + .decode(input) + .map_err(|e| ImapError::Parse(format!("modified UTF-7 mailbox name: {e}"))) } -pub fn alternate_mailbox_name(input: &str, utf8_accept: bool) -> Option { - let primary = encode_mailbox_name_with(input, utf8_accept); - let alternate = encode_mailbox_name_with(input, !utf8_accept); - 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 = Vec::new(); - let flush = |pending: &mut Vec, 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, 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 +fn ends_in_open_shift(name: &str) -> bool { + name.rsplit_once('&') + .is_some_and(|(_, tail)| !tail.is_empty() && !tail.contains('-')) } #[cfg(test)] @@ -186,27 +59,52 @@ mod tests { } #[test] - fn unterminated_sequence_errors() { - let err = decode_mailbox_name("&ZeVnLIqe").unwrap_err(); + fn raw_ampersand_without_a_closing_dash_keeps_the_name_verbatim() { + 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(_))); } #[test] - fn ascii_passes_through_encode() { - assert_eq!(encode_mailbox_name("INBOX"), "INBOX"); - assert_eq!(encode_mailbox_name("Sent Items"), "Sent Items"); - } - - #[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-"); + fn encoded_printable_ascii_and_leftover_bits_are_accepted() { + assert_eq!(decode_mailbox_name("&AGE-").unwrap(), "a"); + assert_eq!(decode_mailbox_name("&AOl-").unwrap(), "é"); + assert_eq!(decode_mailbox_name("&AOk-&AOk-").unwrap(), "éé"); } #[test] @@ -220,7 +118,7 @@ mod tests { "INBOX.Projects.Alpha", "Junk E-mail", ] { - let enc = encode_mailbox_name(s); + let enc = utf7::IMAP.encode(s); let dec = decode_mailbox_name(&enc).unwrap(); assert_eq!(dec, s, "roundtrip failed for {s:?}, enc={enc:?}"); } @@ -247,34 +145,14 @@ mod tests { #[test] fn raw_utf8_name_round_trips_through_encode() { 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); } } - #[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] 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("日本語", 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"); - } } diff --git a/src/imap/response.rs b/src/imap/response.rs index 898f553..1bbc097 100644 --- a/src/imap/response.rs +++ b/src/imap/response.rs @@ -190,17 +190,7 @@ impl<'r, R: BufRead> Parser<'r, R> { } Ok(Untagged::StatusLine(line)) } - "CAPABILITY" => { - 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)) - } + "CAPABILITY" => Ok(Untagged::Capability(self.read_space_separated()?)), "LIST" | "LSUB" => { self.skip_ws(); self.expect(b'(')?; @@ -300,17 +290,7 @@ impl<'r, R: BufRead> Parser<'r, R> { Ok(Untagged::Flags(flags)) } "NAMESPACE" => self.parse_namespace(), - "ENABLED" => { - 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)) - } + "ENABLED" => Ok(Untagged::Enabled(self.read_space_separated()?)), _ => { if let Ok(num) = upper.parse::() { self.skip_ws(); @@ -651,6 +631,15 @@ impl<'r, R: BufRead> Parser<'r, R> { .map_err(|e| ImapError::Parse(format!("non-utf8 atom: {e}"))) } + fn read_space_separated(&mut self) -> Result, 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 { let start = self.pos; while let Some(b) = self.peek() { @@ -735,10 +724,7 @@ impl<'r, R: BufRead> Parser<'r, R> { } fn is_atom_byte(b: u8) -> bool { - matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' - | b'-' | b'.' | b'_' | b'!' | b'#' | b'$' | b'&' - | b'\'' | b'+' | b'/' | b';' | b'<' | b'=' | b'>' | b'?' - | b'@' | b'\\' | b'^' | b'`' | b'|' | b'~' | b'*') + b.is_ascii_graphic() && !matches!(b, b'(' | b')' | b'{' | b'%' | b'"' | b']') } fn parse_seq_set(s: &str) -> Vec { @@ -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] fn quoted_mailbox_name_with_utf8_bytes_is_decoded_as_utf8() { let r = parse("* LIST (\\HasNoChildren) \"/\" \"Envoy\u{e9}s\"\r\n".as_bytes()); diff --git a/src/jmap/blob.rs b/src/jmap/blob.rs index acc04fb..807ce70 100644 --- a/src/jmap/blob.rs +++ b/src/jmap/blob.rs @@ -4,8 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT */ -use base64::Engine; -use base64::engine::general_purpose::STANDARD; +use encodify::base64::STANDARD; use serde_json::{Map, Value}; use crate::jmap::error::JmapError; @@ -14,6 +13,7 @@ pub const SENTINEL_KEY: &str = "@blob"; const DEFAULT_MEDIA_TYPE: &str = "application/octet-stream"; const MEDIA_TYPE_KEYS: [&str; 2] = ["mediaType", "contentType"]; +const DATA_URI_OVERHEAD: usize = "data:;base64,".len(); #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum InlineShape { @@ -103,11 +103,13 @@ fn data_uri(map: &Map, bytes: &[u8]) -> String { .iter() .find_map(|k| map.get(*k).and_then(Value::as_str).and_then(uri_media_type)) .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(&media_type); uri.push_str(";base64,"); - STANDARD.encode_string(bytes, &mut uri); + STANDARD.encode_append(bytes, &mut uri); uri } diff --git a/src/jmap/http.rs b/src/jmap/http.rs index 68eec72..77c8ed8 100644 --- a/src/jmap/http.rs +++ b/src/jmap/http.rs @@ -9,8 +9,7 @@ use std::sync::OnceLock; use std::sync::atomic::{AtomicU8, AtomicU64, Ordering}; use std::time::{Duration, Instant, SystemTime}; -use base64::Engine; -use base64::engine::general_purpose::STANDARD; +use encodify::base64::STANDARD; use serde_json::Value; use ureq::Agent; 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 BASIC: &str = "Basic "; + #[derive(Debug, Clone)] pub enum Auth { Basic { user: String, password: String }, @@ -37,7 +38,12 @@ impl Auth { pub fn header_value(&self) -> String { match self { 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}"), } diff --git a/src/managesieve/client.rs b/src/managesieve/client.rs index c78a749..c75ee23 100644 --- a/src/managesieve/client.rs +++ b/src/managesieve/client.rs @@ -7,8 +7,7 @@ use std::io::{BufRead, BufReader, Read, Write}; use std::time::Instant; -use base64::Engine; -use base64::engine::general_purpose::STANDARD as BASE64; +use encodify::base64::STANDARD; use crate::imap::transport::{Connector, ImapStream}; use crate::logging::Logger; @@ -201,17 +200,17 @@ impl SieveClient { payload.extend_from_slice(authcid.as_bytes()); payload.push(0); payload.extend_from_slice(password.as_bytes()); - let encoded = BASE64.encode(&payload); + let encoded = STANDARD.encode(&payload); self.send_authenticate("PLAIN", &encoded) } pub fn authenticate_login(&mut self, authcid: &str, password: &str) -> Result<(), SieveError> { self.fresh_post_auth_caps = false; 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.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.write_all(command::continuation_payload(&pass_payload).as_bytes())?; let block = read_response(&mut self.reader)?; @@ -237,7 +236,7 @@ impl SieveClient { payload.push_str(token); payload.push('\x01'); payload.push('\x01'); - let encoded = BASE64.encode(payload.as_bytes()); + let encoded = STANDARD.encode(&payload); self.send_authenticate("OAUTHBEARER", &encoded) } diff --git a/src/sync/import_exchange_ews/attachments.rs b/src/sync/import_exchange_ews/attachments.rs index 7f4fd3e..6cd4ae0 100644 --- a/src/sync/import_exchange_ews/attachments.rs +++ b/src/sync/import_exchange_ews/attachments.rs @@ -4,8 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT */ -use base64::Engine; -use base64::engine::general_purpose::STANDARD; +use encodify::base64::LENIENT; use crate::db::blobs; use crate::error::Error; @@ -56,13 +55,12 @@ pub fn fetch_attachments( let resp = ctx.client.call(ctx.url, "GetAttachment", &body)?; let inline = parse_get_attachment_inline(&resp.body)?; for att in inline { - let cleaned = strip_ascii_whitespace(att.content_base64.as_bytes()); - if cleaned.is_empty() { - continue; - } - let bytes = STANDARD.decode(&cleaned).map_err(|e| { + let bytes = LENIENT.decode(&att.content_base64).map_err(|e| { EwsError::Malformed(format!("attachment {}: base64: {e}", att.attachment_id)) })?; + if bytes.is_empty() { + continue; + } let media_type = att .content_type .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 { blobs::intern_blob(conn, bytes).map_err(|e| Error::Partial(e.to_string())) } - -fn strip_ascii_whitespace(input: &[u8]) -> Vec { - 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"); - } -} diff --git a/src/sync/import_exchange_ews/folders.rs b/src/sync/import_exchange_ews/folders.rs index 1df194b..fc4a791 100644 --- a/src/sync/import_exchange_ews/folders.rs +++ b/src/sync/import_exchange_ews/folders.rs @@ -131,7 +131,7 @@ fn resolve_well_known_roles( } }; 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 = HashMap::new(); for (msg, (_, role)) in messages.into_iter().zip(WELL_KNOWN_ROLES.iter()) { if !msg.success { diff --git a/src/sync/import_exchange_ews/items.rs b/src/sync/import_exchange_ews/items.rs index fec72f5..7439c91 100644 --- a/src/sync/import_exchange_ews/items.rs +++ b/src/sync/import_exchange_ews/items.rs @@ -294,7 +294,7 @@ pub fn get_items( for chunk in ids.chunks(batch) { let body = get_item_body(shape, chunk, version); 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), Err(e) if is_per_batch_fault(&e) => { ctx.logger.warn(&format!( @@ -331,7 +331,7 @@ pub fn get_items( let body = get_item_body(shape, &chunk, version); let n = chunk.len(); 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), }; (n, result) diff --git a/src/sync/import_exchange_ews/messages.rs b/src/sync/import_exchange_ews/messages.rs index ebbe7ca..8d2acc5 100644 --- a/src/sync/import_exchange_ews/messages.rs +++ b/src/sync/import_exchange_ews/messages.rs @@ -4,8 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT */ -use base64::Engine; -use base64::engine::general_purpose::STANDARD; +use encodify::base64::LENIENT; use rusqlite::{Connection, params}; use serde_json::{Value, json}; @@ -164,18 +163,15 @@ fn apply_message( existing_local_id: Option, counts: &mut TypeCounts, ) -> Result<(), Error> { - let mime_b64 = match item.mime_content.as_ref() { - Some(s) => s.replace(['\n', '\r', ' ', '\t'], ""), - None => { - counts.skipped += 1; - ctx.logger.warn(&format!( - "message {} has no MimeContent; skipping", - item.id.id - )); - return Ok(()); - } + let Some(mime_b64) = item.mime_content.as_ref() else { + counts.skipped += 1; + ctx.logger.warn(&format!( + "message {} has no MimeContent; skipping", + item.id.id + )); + return Ok(()); }; - let bytes = match STANDARD.decode(mime_b64.as_bytes()) { + let bytes = match LENIENT.decode(mime_b64) { Ok(b) => b, Err(e) => { counts.failed += 1; diff --git a/src/sync/import_exchange_graph/calendar.rs b/src/sync/import_exchange_graph/calendar.rs index d4a600b..02bfb94 100644 --- a/src/sync/import_exchange_graph/calendar.rs +++ b/src/sync/import_exchange_graph/calendar.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; +use encodify::base64::STANDARD; use rusqlite::{Connection, Transaction, params}; use serde_json::{Value, json}; @@ -32,7 +33,6 @@ fn fetch_event_attachments( endpoints: &crate::exchange_graph::api::Endpoints, event_id: &str, ) -> Vec { - use base64::Engine; let url = endpoints.event_attachments(event_id); let Ok(body) = client.get_json_with_prefer(&url, &[]) else { return Vec::new(); @@ -50,7 +50,7 @@ fn fetch_event_attachments( let Some(encoded) = item.get("contentBytes").and_then(Value::as_str) else { continue; }; - let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(encoded) else { + let Ok(bytes) = STANDARD.decode(encoded) else { continue; }; if bytes.is_empty() { diff --git a/src/sync/import_imap/coordinator.rs b/src/sync/import_imap/coordinator.rs index 70f6a61..b5e4c33 100644 --- a/src/sync/import_imap/coordinator.rs +++ b/src/sync/import_imap/coordinator.rs @@ -20,7 +20,6 @@ use crate::error::Error; use crate::imap::client::{ConnectMode, ImapClient}; use crate::imap::command; 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::retry::{ BackoffState, Disposition, RetryPolicy, classify, is_negotiation_failure, @@ -114,28 +113,9 @@ pub(super) fn control_run_collect( pub(super) fn select_folder( client: &mut ImapClient, ctx: &ControlCtx, - folder: &str, + folder: &ResolvedFolder, ) -> Result { - let utf8_accept = client.utf8_accept(); - 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, - } + control_run_collect(client, ctx, &command::select(&folder.wire_name)) } pub(super) fn call_with_retry( @@ -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 uidnext: u32 = 0; for u in &resp.untagged { @@ -826,6 +806,7 @@ fn reconcile_folder( for batch in &batches { pool.submit(FetchJob { folder: folder.name.clone(), + wire_name: folder.wire_name.clone(), uidvalidity, uids: batch.to_vec(), }); @@ -1149,7 +1130,7 @@ fn dry_run_summary( } 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, Err(e) => { log_at( diff --git a/src/sync/import_imap/folders.rs b/src/sync/import_imap/folders.rs index c3810cb..cea6a18 100644 --- a/src/sync/import_imap/folders.rs +++ b/src/sync/import_imap/folders.rs @@ -22,6 +22,7 @@ pub struct FolderStatus { #[derive(Debug, Clone)] pub struct DiscoveredFolder { pub name: String, + pub wire_name: String, pub delimiter: Option, pub attributes: Vec, pub subscribed: bool, @@ -32,6 +33,7 @@ pub struct DiscoveredFolder { #[derive(Debug, Clone)] pub struct ResolvedFolder { pub name: String, + pub wire_name: String, pub leaf: String, pub parent_path: Option, pub delimiter: Option, @@ -85,6 +87,7 @@ pub fn collect_from_list( canonical.clone(), DiscoveredFolder { name: canonical, + wire_name: name.clone(), delimiter: *delimiter, attributes: attributes.clone(), subscribed: false, @@ -156,6 +159,7 @@ pub fn apply_filters( let (leaf, parent_path) = split_parent(&f.name, delim); resolved.push(ResolvedFolder { name: f.name, + wire_name: f.wire_name, leaf, parent_path, delimiter: delim, @@ -437,6 +441,7 @@ mod tests { let mut r = vec![ ResolvedFolder { name: "Projects/Alpha".into(), + wire_name: "Projects/Alpha".into(), leaf: "Alpha".into(), parent_path: Some("Projects".into()), delimiter: Some('/'), @@ -446,6 +451,7 @@ mod tests { }, ResolvedFolder { name: "INBOX".into(), + wire_name: "INBOX".into(), leaf: "INBOX".into(), parent_path: None, delimiter: Some('/'), @@ -455,6 +461,7 @@ mod tests { }, ResolvedFolder { name: "Projects".into(), + wire_name: "Projects".into(), leaf: "Projects".into(), parent_path: None, delimiter: Some('/'), diff --git a/src/sync/import_imap/pool.rs b/src/sync/import_imap/pool.rs index 9f00107..ff9900f 100644 --- a/src/sync/import_imap/pool.rs +++ b/src/sync/import_imap/pool.rs @@ -13,7 +13,6 @@ use crossbeam_channel::{Receiver, Sender, unbounded}; use crate::imap::client::{ConnectMode, ImapClient}; use crate::imap::command; use crate::imap::error::ImapError; -use crate::imap::name::{alternate_mailbox_name, encode_mailbox_name_with}; use crate::imap::response::Untagged; use crate::imap::retry::{BackoffState, Disposition, RetryPolicy, classify}; use crate::imap::transport::Connector; @@ -26,6 +25,7 @@ pub const HARD_CAP: usize = 8; pub struct FetchJob { pub folder: String, + pub wire_name: String, pub uidvalidity: u32, pub uids: Vec, } @@ -231,16 +231,7 @@ fn run_one_job( event_tx: &Sender, ) -> Result<(), ImapError> { if current_folder.as_deref() != Some(job.folder.as_str()) { - let utf8_accept = client.utf8_accept(); - 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), - } - } + client.run_collect(&command::select(&job.wire_name))?; *current_folder = Some(job.folder.clone()); } let set = command::format_uid_set(&job.uids, true); diff --git a/src/sync/import_jmap/mapping.rs b/src/sync/import_jmap/mapping.rs index 982f5a2..78ca2ee 100644 --- a/src/sync/import_jmap/mapping.rs +++ b/src/sync/import_jmap/mapping.rs @@ -897,6 +897,7 @@ fn opt_format_utc(value: &Option) -> Result mod tests { use super::*; use crate::db::init; + use encodify::base64::STANDARD; use std::collections::HashMap; struct MapResolver { @@ -929,15 +930,12 @@ mod tests { } fn decode_data_uri(value: &Value, media_type: &str) -> Vec { - use base64::Engine; let uri = value.as_str().unwrap_or_else(|| panic!("{value} is a URI")); let prefix = format!("data:{media_type};base64,"); let payload = uri .strip_prefix(&prefix) .unwrap_or_else(|| panic!("{uri} does not start with {prefix}")); - base64::engine::general_purpose::STANDARD - .decode(payload) - .expect("base64 payload") + STANDARD.decode(payload).expect("base64 payload") } fn assert_no_blob_id(value: &Value) { diff --git a/tests/integration/dav_client.rs b/tests/integration/dav_client.rs index 8414b99..1e25be5 100644 --- a/tests/integration/dav_client.rs +++ b/tests/integration/dav_client.rs @@ -7,8 +7,7 @@ use std::io; use std::time::Duration; -use base64::Engine; -use base64::engine::general_purpose::STANDARD as B64; +use encodify::base64::STANDARD as B64; use ureq::Agent; use ureq::http::{Method, Request}; diff --git a/tests/integration/dovecot.rs b/tests/integration/dovecot.rs index 7ce33f3..ccb46ee 100644 --- a/tests/integration/dovecot.rs +++ b/tests/integration/dovecot.rs @@ -82,6 +82,10 @@ service managesieve-login { } } +protocol imap { + mail_plugins = $mail_plugins imap_sieve +} + protocol sieve { managesieve_max_line_length = 1M managesieve_max_compile_errors = 5 @@ -96,6 +100,7 @@ info_log_path = /dev/stderr plugin { sieve = file:~/sieve;active=~/.dovecot.sieve 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 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())?); + ImapSeed::connect(&imap.host, imap.port)?.logout()?; let accounts: Vec = layouts::accounts() .iter() diff --git a/tests/integration/sieve_client.rs b/tests/integration/sieve_client.rs index c98a721..e4fed4a 100644 --- a/tests/integration/sieve_client.rs +++ b/tests/integration/sieve_client.rs @@ -8,8 +8,7 @@ use std::io::{BufRead, BufReader, Read, Write}; use std::net::TcpStream; use std::time::Duration; -use base64::Engine; -use base64::engine::general_purpose::STANDARD as B64; +use encodify::base64::STANDARD as B64; use super::error::{ContainerError, ContainerResult}; diff --git a/tests/integration/stalwart.rs b/tests/integration/stalwart.rs index 9ae7550..01dfeec 100644 --- a/tests/integration/stalwart.rs +++ b/tests/integration/stalwart.rs @@ -9,7 +9,7 @@ use std::process::Command; use std::sync::{Arc, OnceLock}; use std::time::{Duration, Instant}; -use base64::Engine; +use encodify::base64::STANDARD; use serde_json::Value; use testcontainers::core::{IntoContainerPort, WaitFor}; use testcontainers::runners::SyncRunner; @@ -74,7 +74,7 @@ impl Stalwart { .with_mapped_port(sieve_port, SIEVE_PORT.tcp()) .with_startup_timeout(Duration::from_secs(180)); - let container = request.start()?; + let container = request.pull_image()?.start()?; let me = Self { _container: container, @@ -183,8 +183,7 @@ fn basic(user: &str, password: &str) -> String { raw.push_str(user); raw.push(':'); raw.push_str(password); - format!( - "Basic {}", - base64::engine::general_purpose::STANDARD.encode(raw.as_bytes()) - ) + let mut header = String::from("Basic "); + STANDARD.encode_append(raw, &mut header); + header } diff --git a/tests/integration/webdav.rs b/tests/integration/webdav.rs index bf45beb..15d74c7 100644 --- a/tests/integration/webdav.rs +++ b/tests/integration/webdav.rs @@ -6,8 +6,7 @@ use std::time::Duration; -use base64::Engine; -use base64::engine::general_purpose::STANDARD as B64; +use encodify::base64::STANDARD as B64; use testcontainers::core::{IntoContainerPort, WaitFor}; use testcontainers::runners::{SyncBuilder, SyncRunner}; use testcontainers::{Container, GenericBuildableImage, GenericImage, ImageExt}; diff --git a/tests/integration_dovecot.rs b/tests/integration_dovecot.rs index 77bdcf6..f99e8ba 100644 --- a/tests/integration_dovecot.rs +++ b/tests/integration_dovecot.rs @@ -18,6 +18,9 @@ use integration::validate::{ use rusqlite::Connection; 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_managesieve::{ManageSieveAuth, ManageSieveImportConfig}; use vandelay::sync::{import_imap, import_managesieve}; @@ -561,3 +564,28 @@ fn dovecot_non_ascii_mailbox_names_round_trip() { ); 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 + ); +} diff --git a/tests/mock_dav.rs b/tests/mock_dav.rs index a264097..54cf59c 100644 --- a/tests/mock_dav.rs +++ b/tests/mock_dav.rs @@ -1448,8 +1448,7 @@ fn dry_run_writes_nothing_but_emits_per_collection_counts() { #[test] fn dav_source_change_protection_fires_across_users_on_same_root() { - use base64::Engine; - use base64::engine::general_purpose::STANDARD; + use encodify::base64::STANDARD; use std::path::PathBuf; use vandelay::logging::Logger; use vandelay::sync::CommonConfig; diff --git a/tests/mock_exchange_ews.rs b/tests/mock_exchange_ews.rs index 3c291b6..f4dd7ac 100644 --- a/tests/mock_exchange_ews.rs +++ b/tests/mock_exchange_ews.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT */ +use encodify::base64::{LENIENT, STANDARD}; use mockito::Matcher; use vandelay::exchange_ews::EwsClient; use vandelay::exchange_ews::autodiscover::{DiscoverySource, discover}; @@ -220,7 +221,7 @@ fn get_item_mixed_success_and_per_item_error() { \ " )); - 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!(r[0].success); assert!(r[0].inner_xml.contains("SGVsbG8=")); @@ -300,8 +301,7 @@ fn http_401_surfaces_as_auth_error() { #[test] fn mime_content_round_trips_through_base64_decode() { let original = b"From: alice@x\r\nSubject: hi\r\n\r\nbody"; - use base64::Engine; - let encoded = base64::engine::general_purpose::STANDARD.encode(original); + let encoded = STANDARD.encode(original); let body = envelope(&format!( "\ \ @@ -312,13 +312,10 @@ fn mime_content_round_trips_through_base64_decode() { \ " )); - 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 s = item.mime_content.unwrap(); - let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect(); - let bytes = base64::engine::general_purpose::STANDARD - .decode(cleaned.as_bytes()) - .unwrap(); + let bytes = LENIENT.decode(&s).unwrap(); assert_eq!(bytes, original); } @@ -893,7 +890,7 @@ fn warning_response_class_is_treated_as_success_in_mock() { \ " )); - 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); } @@ -933,7 +930,7 @@ fn get_folder_messages_preserve_position_when_one_errors() { \ " )); - 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!(msgs[0].success); assert!(!msgs[1].success); diff --git a/tests/mock_exchange_graph.rs b/tests/mock_exchange_graph.rs index 1fd9d4e..bef2075 100644 --- a/tests/mock_exchange_graph.rs +++ b/tests/mock_exchange_graph.rs @@ -6,7 +6,7 @@ use std::sync::Once; -use base64::Engine; +use encodify::base64::URL_SAFE_NO_PAD; use mockito::{Matcher, Server}; use serde_json::json; 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 { - use base64::engine::general_purpose::URL_SAFE_NO_PAD; let header = URL_SAFE_NO_PAD.encode(b"{\"alg\":\"none\"}"); let claims = format!(r#"{{"tid":"tenant-1","upn":"{upn}","exp":{exp}}}"#); let payload = URL_SAFE_NO_PAD.encode(claims.as_bytes()); diff --git a/tests/mock_imap.rs b/tests/mock_imap.rs index 7d540fa..4ec2ab7 100644 --- a/tests/mock_imap.rs +++ b/tests/mock_imap.rs @@ -12,6 +12,7 @@ use std::sync::atomic::{AtomicU32, Ordering}; use std::thread; use std::time::Duration; +use encodify::base64::STANDARD; use rusqlite::Connection; use vandelay::db; use vandelay::imap::client::{ConnectMode, ImapClient}; @@ -230,7 +231,6 @@ fn authenticate_oauthbearer_sasl_ir() { #[test] fn authenticate_oauthbearer_continuation_payload_uses_gs2_header() { - use base64::Engine; let server = MockImap::start(|conn| { conn.write_line("* OK Hello")?; let (tag, _) = conn.read_command()?; @@ -242,9 +242,7 @@ fn authenticate_oauthbearer_continuation_payload_uses_gs2_header() { let mut line = String::new(); conn.reader.read_line(&mut line)?; let payload = line.trim_end_matches(['\r', '\n']).to_owned(); - let decoded = base64::engine::general_purpose::STANDARD - .decode(&payload) - .expect("base64"); + let decoded = STANDARD.decode(&payload).expect("base64"); let text = std::str::from_utf8(&decoded).unwrap_or(""); assert!( text.starts_with("n,a=alice@example.com,"), @@ -489,26 +487,35 @@ fn write_fetch_message( 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 = 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 { Box::new(move |conn: &mut MockConn| -> std::io::Result<()> { auth_preamble(conn, "IMAP4rev2 LITERAL+ AUTH=PLAIN")?; - 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 = 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(()) + serve_one_folder(conn, uidvalidity, uidnext, uids) }) } @@ -585,6 +592,63 @@ fn coordinator_imports_one_folder_one_message() { 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] fn coordinator_uses_esearch_when_advertised() { 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] -fn utf8_name_from_a_server_that_never_enabled_utf8_falls_back_on_select() { - let control: Script = Box::new(|conn: &mut MockConn| -> std::io::Result<()> { +fn utf8_name_from_a_server_that_never_enabled_utf8_is_selected_as_listed() { + 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")?; let (tag, cmd) = conn.read_command()?; 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"))?; let (tag, _) = conn.read_command()?; conn.write_line(&format!("{tag} OK LSUB done"))?; 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!( - name, FRENCH_SENT_UTF8, - "a refused modified UTF-7 name must be retried as UTF-8" + name, listed, + "the name must go back exactly as the server listed it" ); write_select(conn, &tag, 902, 2, 1)?; 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); 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")?; 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!(name, FRENCH_SENT_UTF8); + assert_eq!( + name, listed, + "the fetch worker must agree with the coordinator" + ); write_select(conn, &tag, 902, 2, 1)?; let (tag, _) = conn.read_command()?; 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 archive = tempfile("utf8_no_enable"); + let archive = tempfile(archive_name); let summary = run_import(&server, "alice", archive.clone(), |_| {}).expect("import"); let email = summary .per_type @@ -1925,7 +2000,7 @@ fn utf8_name_from_a_server_that_never_enabled_utf8_falls_back_on_select() { assert_eq!( conn.query_row::("SELECT name FROM mailboxes", [], |r| r.get(0)) .unwrap(), - FRENCH_SENT_UTF8 + stored ); let _ = std::fs::remove_file(&archive); } diff --git a/tests/seeder/admin.rs b/tests/seeder/admin.rs index 9aa6fc6..5a3f954 100644 --- a/tests/seeder/admin.rs +++ b/tests/seeder/admin.rs @@ -197,7 +197,7 @@ impl Admin { "encryptionAtRest": { "@type": "Disabled" }, "permissions": { "@type": "Inherit" }, "roles": { "@type": role }, - "locale": "en_US" + "locale": "en-US" } }); let response = diff --git a/tests/seeder/jmap.rs b/tests/seeder/jmap.rs index 78ba426..d8da270 100644 --- a/tests/seeder/jmap.rs +++ b/tests/seeder/jmap.rs @@ -6,7 +6,7 @@ use std::sync::Arc; -use base64::Engine; +use encodify::base64::STANDARD; use serde_json::{Value, json}; use ureq::Agent; use ureq::config::RedirectAuthHeaders; @@ -43,10 +43,9 @@ fn basic(user: &str, password: &str) -> String { raw.push_str(user); raw.push(':'); raw.push_str(password); - format!( - "Basic {}", - base64::engine::general_purpose::STANDARD.encode(raw.as_bytes()) - ) + let mut header = String::from("Basic "); + STANDARD.encode_append(raw, &mut header); + header } impl Jmap { diff --git a/tests/sync_jmap.rs b/tests/sync_jmap.rs index e06fbc5..bd51ef4 100644 --- a/tests/sync_jmap.rs +++ b/tests/sync_jmap.rs @@ -9,6 +9,7 @@ mod seeder; use std::path::{Path, PathBuf}; +use encodify::base64::STANDARD; use integration::stalwart::shared as shared_stalwart; use rusqlite::Connection; 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"; fn data_uri_bytes(resource: &Value, uri_key: &str, expect_media_type: &str) -> Vec { - use base64::Engine; let uri = resource .get(uri_key) .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 .strip_prefix(&prefix) .unwrap_or_else(|| panic!("{uri_key} is not a {prefix}... data URI: {resource}")); - base64::engine::general_purpose::STANDARD - .decode(payload) - .expect("base64 payload") + STANDARD.decode(payload).expect("base64 payload") } #[test] @@ -1245,6 +1243,10 @@ fn apply_jmap_settings( .is_some(), "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] @@ -1257,8 +1259,13 @@ fn live_blob_quota_429_triggers_retry_after_then_succeeds() { let fx = seeder::provision(base_url()).expect("provision"); let acc = fx.account("test1").expect("test1"); + let blob_size = 8 * 1024 * 1024; let mut updates = serde_json::Map::new(); 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 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"); client.set_limits(&limits); - let blob_size = 8 * 1024 * 1024; let mut blob = vec![0u8; blob_size]; let max_uploads = 8u32; let mut accepted = 0u32;