aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--Cargo.lock990
-rw-r--r--Cargo.nix1528
-rw-r--r--Cargo.toml75
-rw-r--r--README.md6
-rw-r--r--aero-bayou/Cargo.toml19
-rw-r--r--aero-bayou/src/lib.rs (renamed from src/bayou.rs)9
-rw-r--r--aero-bayou/src/timestamp.rs (renamed from src/timestamp.rs)3
-rw-r--r--aero-collections/Cargo.toml25
-rw-r--r--aero-collections/src/calendar/mod.rs204
-rw-r--r--aero-collections/src/calendar/namespace.rs324
-rw-r--r--aero-collections/src/davdag.rs342
-rw-r--r--aero-collections/src/lib.rs5
-rw-r--r--aero-collections/src/mail/incoming.rs (renamed from src/mail/incoming.rs)16
-rw-r--r--aero-collections/src/mail/mailbox.rs (renamed from src/mail/mailbox.rs)17
-rw-r--r--aero-collections/src/mail/mod.rs (renamed from src/mail/mod.rs)5
-rw-r--r--aero-collections/src/mail/namespace.rs206
-rw-r--r--aero-collections/src/mail/query.rs (renamed from src/mail/query.rs)2
-rw-r--r--aero-collections/src/mail/snapshot.rs (renamed from src/mail/snapshot.rs)2
-rw-r--r--aero-collections/src/mail/uidindex.rs (renamed from src/mail/uidindex.rs)4
-rw-r--r--aero-collections/src/unique_ident.rs (renamed from src/mail/unique_ident.rs)6
-rw-r--r--aero-collections/src/user.rs (renamed from src/mail/user.rs)227
-rw-r--r--aero-dav/.gitignore1
-rw-r--r--aero-dav/Cargo.toml15
-rw-r--r--aero-dav/fuzz/.gitignore4
-rw-r--r--aero-dav/fuzz/Cargo.lock4249
-rw-r--r--aero-dav/fuzz/Cargo.toml24
-rw-r--r--aero-dav/fuzz/dav.dict126
-rw-r--r--aero-dav/fuzz/fuzz_targets/dav.rs209
-rw-r--r--aero-dav/src/acldecoder.rs84
-rw-r--r--aero-dav/src/aclencoder.rs71
-rw-r--r--aero-dav/src/acltypes.rs38
-rw-r--r--aero-dav/src/caldecoder.rs1421
-rw-r--r--aero-dav/src/calencoder.rs1036
-rw-r--r--aero-dav/src/caltypes.rs1500
-rw-r--r--aero-dav/src/decoder.rs1152
-rw-r--r--aero-dav/src/encoder.rs1262
-rw-r--r--aero-dav/src/error.rs62
-rw-r--r--aero-dav/src/lib.rs35
-rw-r--r--aero-dav/src/realization.rs260
-rw-r--r--aero-dav/src/syncdecoder.rs248
-rw-r--r--aero-dav/src/syncencoder.rs227
-rw-r--r--aero-dav/src/synctypes.rs86
-rw-r--r--aero-dav/src/types.rs964
-rw-r--r--aero-dav/src/versioningdecoder.rs132
-rw-r--r--aero-dav/src/versioningencoder.rs143
-rw-r--r--aero-dav/src/versioningtypes.rs59
-rw-r--r--aero-dav/src/xml.rs367
-rw-r--r--aero-ical/Cargo.toml15
-rw-r--r--aero-ical/src/lib.rs8
-rw-r--r--aero-ical/src/parser.rs146
-rw-r--r--aero-ical/src/prune.rs55
-rw-r--r--aero-ical/src/query.rs338
-rw-r--r--aero-proto/Cargo.toml39
-rw-r--r--aero-proto/src/dav/codec.rs135
-rw-r--r--aero-proto/src/dav/controller.rs436
-rw-r--r--aero-proto/src/dav/middleware.rs70
-rw-r--r--aero-proto/src/dav/mod.rs195
-rw-r--r--aero-proto/src/dav/node.rs145
-rw-r--r--aero-proto/src/dav/resource.rs999
-rw-r--r--aero-proto/src/imap/attributes.rs (renamed from src/imap/attributes.rs)0
-rw-r--r--aero-proto/src/imap/capability.rs (renamed from src/imap/capability.rs)0
-rw-r--r--aero-proto/src/imap/command/anonymous.rs (renamed from src/imap/command/anonymous.rs)5
-rw-r--r--aero-proto/src/imap/command/anystate.rs (renamed from src/imap/command/anystate.rs)0
-rw-r--r--aero-proto/src/imap/command/authenticated.rs (renamed from src/imap/command/authenticated.rs)14
-rw-r--r--aero-proto/src/imap/command/mod.rs (renamed from src/imap/command/mod.rs)2
-rw-r--r--aero-proto/src/imap/command/selected.rs (renamed from src/imap/command/selected.rs)5
-rw-r--r--aero-proto/src/imap/flags.rs (renamed from src/imap/flags.rs)0
-rw-r--r--aero-proto/src/imap/flow.rs (renamed from src/imap/flow.rs)3
-rw-r--r--aero-proto/src/imap/imf_view.rs (renamed from src/imap/imf_view.rs)0
-rw-r--r--aero-proto/src/imap/index.rs (renamed from src/imap/index.rs)4
-rw-r--r--aero-proto/src/imap/mail_view.rs (renamed from src/imap/mail_view.rs)2
-rw-r--r--aero-proto/src/imap/mailbox_view.rs (renamed from src/imap/mailbox_view.rs)26
-rw-r--r--aero-proto/src/imap/mime_view.rs (renamed from src/imap/mime_view.rs)4
-rw-r--r--aero-proto/src/imap/mod.rs (renamed from src/imap/mod.rs)99
-rw-r--r--aero-proto/src/imap/request.rs (renamed from src/imap/request.rs)0
-rw-r--r--aero-proto/src/imap/response.rs (renamed from src/imap/response.rs)0
-rw-r--r--aero-proto/src/imap/search.rs (renamed from src/imap/search.rs)3
-rw-r--r--aero-proto/src/imap/session.rs (renamed from src/imap/session.rs)8
-rw-r--r--aero-proto/src/lib.rs6
-rw-r--r--aero-proto/src/lmtp.rs (renamed from src/lmtp.rs)18
-rw-r--r--aero-proto/src/sasl.rs142
-rw-r--r--aero-sasl/Cargo.toml22
-rw-r--r--aero-sasl/src/decode.rs243
-rw-r--r--aero-sasl/src/encode.rs157
-rw-r--r--aero-sasl/src/flow.rs201
-rw-r--r--aero-sasl/src/lib.rs43
-rw-r--r--aero-sasl/src/types.rs161
-rw-r--r--aero-user/Cargo.toml30
-rw-r--r--aero-user/src/config.rs (renamed from src/config.rs)16
-rw-r--r--aero-user/src/cryptoblob.rs (renamed from src/cryptoblob.rs)0
-rw-r--r--aero-user/src/lib.rs9
-rw-r--r--aero-user/src/login/demo_provider.rs (renamed from src/login/demo_provider.rs)0
-rw-r--r--aero-user/src/login/ldap_provider.rs (renamed from src/login/ldap_provider.rs)3
-rw-r--r--aero-user/src/login/mod.rs (renamed from src/login/mod.rs)2
-rw-r--r--aero-user/src/login/static_provider.rs (renamed from src/login/static_provider.rs)7
-rw-r--r--aero-user/src/storage/garage.rs (renamed from src/storage/garage.rs)16
-rw-r--r--aero-user/src/storage/in_memory.rs (renamed from src/storage/in_memory.rs)20
-rw-r--r--aero-user/src/storage/mod.rs (renamed from src/storage/mod.rs)5
-rw-r--r--aerogramme/Cargo.toml32
-rw-r--r--aerogramme/src/main.rs (renamed from src/main.rs)25
-rw-r--r--aerogramme/src/server.rs (renamed from src/server.rs)38
-rw-r--r--aerogramme/tests/behavior.rs1292
-rw-r--r--aerogramme/tests/common/constants.rs243
-rw-r--r--aerogramme/tests/common/fragments.rs (renamed from tests/common/fragments.rs)0
-rw-r--r--aerogramme/tests/common/mod.rs (renamed from tests/common/mod.rs)43
-rw-r--r--doc/.gitignore1
-rw-r--r--doc/book.toml9
-rw-r--r--doc/src/SUMMARY.md34
-rw-r--r--doc/src/aero-compo.pngbin26898 -> 0 bytes
-rw-r--r--doc/src/aero-paranoid.pngbin27405 -> 0 bytes
-rw-r--r--doc/src/aero-schema.pngbin74645 -> 0 bytes
-rw-r--r--doc/src/aero-states.pngbin9090 -> 0 bytes
-rw-r--r--doc/src/aero-states2.pngbin17869 -> 0 bytes
-rw-r--r--doc/src/aerogramme.jpgbin563365 -> 0 bytes
-rw-r--r--doc/src/config.md126
-rw-r--r--doc/src/crypt-key.md82
-rw-r--r--doc/src/data_format.md50
-rw-r--r--doc/src/imap_uid.md203
-rw-r--r--doc/src/index.md22
-rw-r--r--doc/src/installation.md25
-rw-r--r--doc/src/log.md149
-rw-r--r--doc/src/mailbox.md56
-rw-r--r--doc/src/mailbox.pngbin10981 -> 0 bytes
-rw-r--r--doc/src/mutt_mail.pngbin24325 -> 0 bytes
-rw-r--r--doc/src/mutt_mb.pngbin39035 -> 0 bytes
-rw-r--r--doc/src/notes.md42
-rw-r--r--doc/src/overview.md61
-rw-r--r--doc/src/rfc.md3
-rw-r--r--doc/src/setup.md90
-rw-r--r--doc/src/validate.md40
-rw-r--r--flake.nix10
-rw-r--r--src/auth.rs941
-rw-r--r--src/k2v_util.rs26
-rw-r--r--tests/behavior.rs357
-rw-r--r--tests/common/constants.rs54
136 files changed, 21760 insertions, 3949 deletions
diff --git a/.gitignore b/.gitignore
index deb0fec..bfe0d50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,11 @@ env.sh
aerogramme.toml
*.swo
*.swp
+aerogramme.pid
+cert.pem
+ec_key.pem
+provider-users.toml
+setup.toml
+test.eml
+test.txt
+users.toml
diff --git a/Cargo.lock b/Cargo.lock
index a50d101..0a159ae 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "addr2line"
-version = "0.21.0"
+version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+checksum = "e7a2e47a1fbe209ee101dd6d61285226744c6c8d3c21c8dc878ba6cb9f467f3a"
dependencies = [
"gimli",
]
@@ -27,63 +27,170 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
-name = "aerogramme"
-version = "0.2.2"
+name = "aero-bayou"
+version = "0.3.0"
dependencies = [
+ "aero-user",
+ "anyhow",
+ "hex",
+ "log",
+ "rand",
+ "serde",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "aero-collections"
+version = "0.3.0"
+dependencies = [
+ "aero-bayou",
+ "aero-user",
+ "anyhow",
+ "base64 0.21.7",
+ "eml-codec",
+ "futures",
+ "hex",
+ "icalendar",
+ "im",
+ "lazy_static",
+ "rand",
+ "serde",
+ "sodiumoxide",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "aero-dav"
+version = "0.3.0"
+dependencies = [
+ "chrono",
+ "futures",
+ "http 1.1.0",
+ "quick-xml",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "aero-ical"
+version = "0.3.0"
+dependencies = [
+ "aero-dav",
+ "chrono",
+ "icalendar",
+ "nom 7.1.3",
+ "tracing",
+]
+
+[[package]]
+name = "aero-proto"
+version = "0.3.0"
+dependencies = [
+ "aero-collections",
+ "aero-dav",
+ "aero-ical",
+ "aero-sasl",
+ "aero-user",
"anyhow",
- "argon2",
"async-trait",
- "aws-config",
- "aws-sdk-s3",
- "aws-smithy-runtime",
- "aws-smithy-runtime-api",
- "backtrace",
"base64 0.21.7",
"chrono",
- "clap",
- "console-subscriber",
"duplexify",
"eml-codec",
"futures",
- "hex",
- "hyper-rustls 0.26.0",
+ "http-body-util",
+ "hyper 1.2.0",
"hyper-util",
- "im",
+ "icalendar",
"imap-codec",
"imap-flow",
- "itertools",
+ "quick-xml",
+ "rustls 0.22.2",
+ "rustls-pemfile 2.1.1",
+ "smtp-message",
+ "smtp-server",
+ "thiserror",
+ "tokio",
+ "tokio-rustls 0.25.0",
+ "tokio-stream",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "aero-sasl"
+version = "0.3.0"
+dependencies = [
+ "anyhow",
+ "base64 0.21.7",
+ "futures",
+ "hex",
+ "nom 7.1.3",
+ "rand",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "aero-user"
+version = "0.3.0"
+dependencies = [
+ "anyhow",
+ "argon2",
+ "async-trait",
+ "aws-config",
+ "aws-sdk-s3",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "base64 0.21.7",
+ "hyper-rustls 0.26.0",
+ "hyper-util",
"k2v-client",
- "lazy_static",
"ldap3",
"log",
- "nix",
- "nom 7.1.3",
"rand",
"rmp-serde",
- "rpassword",
- "rustls 0.22.2",
- "rustls-pemfile 2.0.0",
"serde",
- "smtp-message",
- "smtp-server",
"sodiumoxide",
- "thiserror",
"tokio",
- "tokio-rustls 0.25.0",
- "tokio-util",
"toml",
"tracing",
- "tracing-subscriber",
"zstd",
]
[[package]]
-name = "aho-corasick"
-version = "1.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
+name = "aerogramme"
+version = "0.3.0"
dependencies = [
- "memchr",
+ "aero-dav",
+ "aero-proto",
+ "aero-user",
+ "anyhow",
+ "backtrace",
+ "clap",
+ "futures",
+ "log",
+ "nix",
+ "quick-xml",
+ "reqwest",
+ "rpassword",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "aerogramme-fuzz"
+version = "0.0.0"
+dependencies = [
+ "aero-dav",
+ "arbitrary",
+ "libfuzzer-sys",
+ "quick-xml",
+ "tokio",
]
[[package]]
@@ -108,10 +215,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
+name = "arbitrary"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
+dependencies = [
+ "derive_arbitrary",
+]
+
+[[package]]
name = "argon2"
-version = "0.5.2"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9"
+checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
@@ -177,13 +293,13 @@ dependencies = [
[[package]]
name = "async-channel"
-version = "2.1.1"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c"
+checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3"
dependencies = [
"concurrent-queue",
- "event-listener 4.0.3",
- "event-listener-strategy",
+ "event-listener 5.2.0",
+ "event-listener-strategy 0.5.0",
"futures-core",
"pin-project-lite 0.2.13",
]
@@ -220,9 +336,9 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
dependencies = [
- "async-channel 2.1.1",
+ "async-channel 2.2.0",
"async-executor",
- "async-io 2.3.0",
+ "async-io 2.3.1",
"async-lock 3.3.0",
"blocking",
"futures-lite 2.2.0",
@@ -251,9 +367,9 @@ dependencies = [
[[package]]
name = "async-io"
-version = "2.3.0"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb41eb19024a91746eba0773aa5e16036045bbf45733766661099e182ea6a744"
+checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65"
dependencies = [
"async-lock 3.3.0",
"cfg-if",
@@ -261,8 +377,8 @@ dependencies = [
"futures-io",
"futures-lite 2.2.0",
"parking",
- "polling 3.3.2",
- "rustix 0.38.30",
+ "polling 3.5.0",
+ "rustix 0.38.31",
"slab",
"tracing",
"windows-sys 0.52.0",
@@ -284,7 +400,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b"
dependencies = [
"event-listener 4.0.3",
- "event-listener-strategy",
+ "event-listener-strategy 0.4.0",
"pin-project-lite 0.2.13",
]
@@ -312,7 +428,7 @@ dependencies = [
"cfg-if",
"event-listener 3.1.0",
"futures-lite 1.13.0",
- "rustix 0.38.30",
+ "rustix 0.38.31",
"windows-sys 0.48.0",
]
@@ -322,13 +438,13 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5"
dependencies = [
- "async-io 2.3.0",
+ "async-io 2.3.1",
"async-lock 2.8.0",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
- "rustix 0.38.30",
+ "rustix 0.38.31",
"signal-hook-registry",
"slab",
"windows-sys 0.48.0",
@@ -361,28 +477,6 @@ dependencies = [
]
[[package]]
-name = "async-stream"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
-dependencies = [
- "async-stream-impl",
- "futures-core",
- "pin-project-lite 0.2.13",
-]
-
-[[package]]
-name = "async-stream-impl"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.48",
-]
-
-[[package]]
name = "async-task"
version = "4.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -458,9 +552,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "aws-config"
-version = "1.1.6"
+version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3182c19847238b50b62ae0383a6dbfc14514e552eb5e307e1ea83ccf5840b8a6"
+checksum = "0b96342ea8948ab9bef3e6234ea97fc32e2d8a88d8fb6a084e52267317f94b6b"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -477,7 +571,7 @@ dependencies = [
"bytes",
"fastrand 2.0.1",
"hex",
- "http 0.2.11",
+ "http 0.2.12",
"hyper 0.14.28",
"ring 0.17.7",
"time",
@@ -488,9 +582,9 @@ dependencies = [
[[package]]
name = "aws-credential-types"
-version = "1.1.6"
+version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5635d8707f265c773282a22abe1ecd4fbe96a8eb2f0f14c0796f8016f11a41a"
+checksum = "273fa47dafc9ef14c2c074ddddbea4561ff01b7f68d5091c0e9737ced605c01d"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@@ -500,9 +594,9 @@ dependencies = [
[[package]]
name = "aws-runtime"
-version = "1.1.6"
+version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f82b9ae2adfd9d6582440d0eeb394c07f74d21b4c0cc72bdb73735c9e1a9c0e"
+checksum = "6e38bab716c8bf07da24be07ecc02e0f5656ce8f30a891322ecdcb202f943b85"
dependencies = [
"aws-credential-types",
"aws-sigv4",
@@ -514,7 +608,7 @@ dependencies = [
"aws-types",
"bytes",
"fastrand 2.0.1",
- "http 0.2.11",
+ "http 0.2.12",
"http-body 0.4.6",
"percent-encoding",
"pin-project-lite 0.2.13",
@@ -524,9 +618,9 @@ dependencies = [
[[package]]
name = "aws-sdk-config"
-version = "1.15.0"
+version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0cb71960e3e197c3f512f3bf0f47f444acd708db59733416107ec2ff161ff5c4"
+checksum = "07979fd68679736ba306d6ea2a4dc2fd835ac4d454942c5d8920ef83ed2f979f"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -538,7 +632,7 @@ dependencies = [
"aws-smithy-types",
"aws-types",
"bytes",
- "http 0.2.11",
+ "http 0.2.12",
"once_cell",
"regex-lite",
"tracing",
@@ -546,9 +640,9 @@ dependencies = [
[[package]]
name = "aws-sdk-s3"
-version = "1.16.0"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5076637347e7d0218e61facae853110682ae58efabd2f4e2a9e530c203d5fa7b"
+checksum = "93d35d39379445970fc3e4ddf7559fff2c32935ce0b279f9cb27080d6b7c6d94"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -564,7 +658,7 @@ dependencies = [
"aws-smithy-xml",
"aws-types",
"bytes",
- "http 0.2.11",
+ "http 0.2.12",
"http-body 0.4.6",
"once_cell",
"percent-encoding",
@@ -575,9 +669,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
-version = "1.14.0"
+version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca7e8097448832fcd22faf6bb227e97d76b40e354509d1307653a885811c7151"
+checksum = "d84bd3925a17c9adbf6ec65d52104a44a09629d8f70290542beeee69a95aee7f"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -589,7 +683,7 @@ dependencies = [
"aws-smithy-types",
"aws-types",
"bytes",
- "http 0.2.11",
+ "http 0.2.12",
"once_cell",
"regex-lite",
"tracing",
@@ -597,9 +691,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
-version = "1.14.0"
+version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75073590e23d63044606771afae309fada8eb10ded54a1ce4598347221d3fef"
+checksum = "2c2dae39e997f58bc4d6292e6244b26ba630c01ab671b6f9f44309de3eb80ab8"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -611,7 +705,7 @@ dependencies = [
"aws-smithy-types",
"aws-types",
"bytes",
- "http 0.2.11",
+ "http 0.2.12",
"once_cell",
"regex-lite",
"tracing",
@@ -619,9 +713,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
-version = "1.14.0"
+version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "650e4aaae41547151dea4d8142f7ffcc8ab8ba76d5dccc8933936ef2102c3356"
+checksum = "17fd9a53869fee17cea77e352084e1aa71e2c5e323d974c13a9c2bcfd9544c7f"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -634,7 +728,7 @@ dependencies = [
"aws-smithy-types",
"aws-smithy-xml",
"aws-types",
- "http 0.2.11",
+ "http 0.2.12",
"once_cell",
"regex-lite",
"tracing",
@@ -642,9 +736,9 @@ dependencies = [
[[package]]
name = "aws-sigv4"
-version = "1.1.6"
+version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "404c64a104188ac70dd1684718765cb5559795458e446480e41984e68e57d888"
+checksum = "8ada00a4645d7d89f296fe0ddbc3fe3554f03035937c849a05d37ddffc1f29a1"
dependencies = [
"aws-credential-types",
"aws-smithy-eventstream",
@@ -656,8 +750,8 @@ dependencies = [
"form_urlencoded",
"hex",
"hmac",
- "http 0.2.11",
- "http 1.0.0",
+ "http 0.2.12",
+ "http 1.1.0",
"once_cell",
"p256",
"percent-encoding",
@@ -692,7 +786,7 @@ dependencies = [
"crc32c",
"crc32fast",
"hex",
- "http 0.2.11",
+ "http 0.2.12",
"http-body 0.4.6",
"md-5",
"pin-project-lite 0.2.13",
@@ -724,7 +818,7 @@ dependencies = [
"bytes",
"bytes-utils",
"futures-core",
- "http 0.2.11",
+ "http 0.2.12",
"http-body 0.4.6",
"once_cell",
"percent-encoding",
@@ -765,7 +859,7 @@ dependencies = [
"bytes",
"fastrand 2.0.1",
"h2 0.3.24",
- "http 0.2.11",
+ "http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.28",
"hyper-rustls 0.24.2",
@@ -786,8 +880,8 @@ dependencies = [
"aws-smithy-async",
"aws-smithy-types",
"bytes",
- "http 0.2.11",
- "http 1.0.0",
+ "http 0.2.12",
+ "http 1.1.0",
"pin-project-lite 0.2.13",
"tokio",
"tracing",
@@ -804,7 +898,7 @@ dependencies = [
"bytes",
"bytes-utils",
"futures-core",
- "http 0.2.11",
+ "http 0.2.12",
"http-body 0.4.6",
"itoa",
"num-integer",
@@ -828,69 +922,24 @@ dependencies = [
[[package]]
name = "aws-types"
-version = "1.1.6"
+version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fbb5d48aae496f628e7aa2e41991dd4074f606d9e3ade1ce1059f293d40f9a2"
+checksum = "d07c63521aa1ea9a9f92a701f1a08ce3fd20b46c6efc0d5c8947c1fd879e3df1"
dependencies = [
"aws-credential-types",
"aws-smithy-async",
"aws-smithy-runtime-api",
"aws-smithy-types",
- "http 0.2.11",
+ "http 0.2.12",
"rustc_version",
"tracing",
]
[[package]]
-name = "axum"
-version = "0.6.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
-dependencies = [
- "async-trait",
- "axum-core",
- "bitflags 1.3.2",
- "bytes",
- "futures-util",
- "http 0.2.11",
- "http-body 0.4.6",
- "hyper 0.14.28",
- "itoa",
- "matchit",
- "memchr",
- "mime",
- "percent-encoding",
- "pin-project-lite 0.2.13",
- "rustversion",
- "serde",
- "sync_wrapper",
- "tower",
- "tower-layer",
- "tower-service",
-]
-
-[[package]]
-name = "axum-core"
-version = "0.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
-dependencies = [
- "async-trait",
- "bytes",
- "futures-util",
- "http 0.2.11",
- "http-body 0.4.6",
- "mime",
- "rustversion",
- "tower-layer",
- "tower-service",
-]
-
-[[package]]
name = "backtrace"
-version = "0.3.69"
+version = "0.3.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
+checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744"
dependencies = [
"addr2line",
"cc",
@@ -920,6 +969,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
name = "base64-simd"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -992,7 +1047,7 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
dependencies = [
- "async-channel 2.1.1",
+ "async-channel 2.2.0",
"async-lock 3.3.0",
"async-task",
"fastrand 2.0.1",
@@ -1068,16 +1123,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
-version = "0.4.31"
+version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
- "windows-targets 0.48.5",
+ "windows-targets 0.52.0",
]
[[package]]
@@ -1129,43 +1184,6 @@ dependencies = [
]
[[package]]
-name = "console-api"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787"
-dependencies = [
- "futures-core",
- "prost",
- "prost-types",
- "tonic",
- "tracing-core",
-]
-
-[[package]]
-name = "console-subscriber"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e"
-dependencies = [
- "console-api",
- "crossbeam-channel",
- "crossbeam-utils",
- "futures-task",
- "hdrhistogram",
- "humantime",
- "prost-types",
- "serde",
- "serde_json",
- "thread_local",
- "tokio",
- "tokio-stream",
- "tonic",
- "tracing",
- "tracing-core",
- "tracing-subscriber",
-]
-
-[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1207,23 +1225,14 @@ dependencies = [
[[package]]
name = "crc32fast"
-version = "1.3.2"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
dependencies = [
"cfg-if",
]
[[package]]
-name = "crossbeam-channel"
-version = "0.5.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b"
-dependencies = [
- "crossbeam-utils",
-]
-
-[[package]]
name = "crossbeam-utils"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1301,6 +1310,17 @@ dependencies = [
]
[[package]]
+name = "derive_arbitrary"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
name = "derive_utils"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1366,9 +1386,9 @@ dependencies = [
[[package]]
name = "either"
-version = "1.9.0"
+version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
[[package]]
name = "elliptic-curve"
@@ -1456,6 +1476,17 @@ dependencies = [
]
[[package]]
+name = "event-listener"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
name = "event-listener-strategy"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1466,6 +1497,16 @@ dependencies = [
]
[[package]]
+name = "event-listener-strategy"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291"
+dependencies = [
+ "event-listener 5.2.0",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
name = "fastrand"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1491,20 +1532,25 @@ dependencies = [
]
[[package]]
-name = "flate2"
-version = "1.0.28"
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
- "crc32fast",
- "miniz_oxide",
+ "foreign-types-shared",
]
[[package]]
-name = "fnv"
-version = "1.0.7"
+name = "foreign-types-shared"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
@@ -1655,15 +1701,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"wasi",
+ "wasm-bindgen",
]
[[package]]
name = "gimli"
-version = "0.28.1"
+version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189"
[[package]]
name = "gloo-timers"
@@ -1699,8 +1747,8 @@ dependencies = [
"futures-core",
"futures-sink",
"futures-util",
- "http 0.2.11",
- "indexmap 2.1.0",
+ "http 0.2.12",
+ "indexmap 2.2.5",
"slab",
"tokio",
"tokio-util",
@@ -1718,8 +1766,8 @@ dependencies = [
"futures-core",
"futures-sink",
"futures-util",
- "http 1.0.0",
- "indexmap 2.1.0",
+ "http 1.1.0",
+ "indexmap 2.2.5",
"slab",
"tokio",
"tokio-util",
@@ -1739,19 +1787,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
-name = "hdrhistogram"
-version = "7.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d"
-dependencies = [
- "base64 0.21.7",
- "byteorder",
- "flate2",
- "nom 7.1.3",
- "num-traits",
-]
-
-[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1789,9 +1824,9 @@ dependencies = [
[[package]]
name = "http"
-version = "0.2.11"
+version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
@@ -1800,9 +1835,9 @@ dependencies = [
[[package]]
name = "http"
-version = "1.0.0"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
@@ -1816,7 +1851,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
- "http 0.2.11",
+ "http 0.2.12",
"pin-project-lite 0.2.13",
]
@@ -1827,18 +1862,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [
"bytes",
- "http 1.0.0",
+ "http 1.1.0",
]
[[package]]
name = "http-body-util"
-version = "0.1.0"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840"
+checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
dependencies = [
"bytes",
- "futures-util",
- "http 1.0.0",
+ "futures-core",
+ "http 1.1.0",
"http-body 1.0.0",
"pin-project-lite 0.2.13",
]
@@ -1856,12 +1891,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
-name = "humantime"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
-
-[[package]]
name = "hyper"
version = "0.14.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1872,7 +1901,7 @@ dependencies = [
"futures-core",
"futures-util",
"h2 0.3.24",
- "http 0.2.11",
+ "http 0.2.12",
"http-body 0.4.6",
"httparse",
"httpdate",
@@ -1895,7 +1924,7 @@ dependencies = [
"futures-channel",
"futures-util",
"h2 0.4.2",
- "http 1.0.0",
+ "http 1.1.0",
"http-body 1.0.0",
"httparse",
"httpdate",
@@ -1913,7 +1942,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
- "http 0.2.11",
+ "http 0.2.12",
"hyper 0.14.28",
"log",
"rustls 0.21.10",
@@ -1929,7 +1958,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
dependencies = [
"futures-util",
- "http 1.0.0",
+ "http 1.1.0",
"hyper 1.2.0",
"hyper-util",
"log",
@@ -1942,15 +1971,19 @@ dependencies = [
]
[[package]]
-name = "hyper-timeout"
-version = "0.4.1"
+name = "hyper-tls"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
- "hyper 0.14.28",
- "pin-project-lite 0.2.13",
+ "bytes",
+ "http-body-util",
+ "hyper 1.2.0",
+ "hyper-util",
+ "native-tls",
"tokio",
- "tokio-io-timeout",
+ "tokio-native-tls",
+ "tower-service",
]
[[package]]
@@ -1962,7 +1995,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
- "http 1.0.0",
+ "http 1.1.0",
"http-body 1.0.0",
"hyper 1.2.0",
"pin-project-lite 0.2.13",
@@ -1975,9 +2008,9 @@ dependencies = [
[[package]]
name = "iana-time-zone"
-version = "0.1.59"
+version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539"
+checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -1997,6 +2030,18 @@ dependencies = [
]
[[package]]
+name = "icalendar"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd83e81e8a329918d84e49032f8e596f4f079380942d172724cea3599a80807e"
+dependencies = [
+ "chrono",
+ "iso8601",
+ "nom 7.1.3",
+ "uuid",
+]
+
+[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2049,7 +2094,7 @@ dependencies = [
[[package]]
name = "imap-flow"
version = "0.1.0"
-source = "git+https://github.com/duesee/imap-flow.git?branch=main#68c1da5d1c56dbe543d9736de9683259d1d28191"
+source = "git+https://github.com/duesee/imap-flow.git?branch=main#dce759a8531f317e8d7311fb032b366db6698e38"
dependencies = [
"bounded-static",
"bytes",
@@ -2083,9 +2128,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.1.0"
+version = "2.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
+checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
dependencies = [
"equivalent",
"hashbrown 0.14.3",
@@ -2112,12 +2157,18 @@ dependencies = [
]
[[package]]
-name = "itertools"
-version = "0.10.5"
+name = "ipnet"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
+
+[[package]]
+name = "iso8601"
+version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153"
dependencies = [
- "either",
+ "nom 7.1.3",
]
[[package]]
@@ -2153,7 +2204,7 @@ dependencies = [
"aws-sigv4",
"base64 0.21.7",
"hex",
- "http 1.0.0",
+ "http 1.1.0",
"http-body-util",
"hyper 1.2.0",
"hyper-rustls 0.26.0",
@@ -2240,6 +2291,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
[[package]]
+name = "libfuzzer-sys"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
+dependencies = [
+ "arbitrary",
+ "cc",
+ "once_cell",
+]
+
+[[package]]
name = "libsodium-sys"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2273,27 +2335,12 @@ dependencies = [
]
[[package]]
-name = "matchers"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
-dependencies = [
- "regex-automata 0.1.10",
-]
-
-[[package]]
name = "matches"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
-name = "matchit"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
-
-[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2305,9 +2352,9 @@ dependencies = [
[[package]]
name = "memchr"
-version = "2.7.1"
+version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
[[package]]
name = "mime"
@@ -2323,11 +2370,12 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
-version = "0.7.1"
+version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
dependencies = [
"adler",
+ "autocfg",
]
[[package]]
@@ -2342,6 +2390,24 @@ dependencies = [
]
[[package]]
+name = "native-tls"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2360,9 +2426,9 @@ checksum = "cf51a729ecf40266a2368ad335a5fdde43471f545a967109cd62146ecf8b66ff"
[[package]]
name = "nom"
-version = "6.1.2"
+version = "6.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
+checksum = "c6a7a9657c84d5814c6196b68bb4429df09c18b1573806259fba397ea4ad0d44"
dependencies = [
"bitvec",
"funty",
@@ -2433,12 +2499,9 @@ dependencies = [
[[package]]
name = "object"
-version = "0.32.2"
+version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
-dependencies = [
- "memchr",
-]
+checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170"
[[package]]
name = "oid-registry"
@@ -2456,12 +2519,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
+name = "openssl"
+version = "0.10.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
+dependencies = [
+ "bitflags 2.4.2",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
+name = "openssl-sys"
+version = "0.9.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
name = "os_str_bytes"
version = "6.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2521,18 +2622,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project"
-version = "1.1.3"
+version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
+checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
-version = "1.1.3"
+version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
+checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
@@ -2602,14 +2703,14 @@ dependencies = [
[[package]]
name = "polling"
-version = "3.3.2"
+version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "545c980a3880efd47b2e262f6a4bb6daad6555cf3367aa9c4e52895f69537a41"
+checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9"
dependencies = [
"cfg-if",
"concurrent-queue",
"pin-project-lite 0.2.13",
- "rustix 0.38.30",
+ "rustix 0.38.31",
"tracing",
"windows-sys 0.52.0",
]
@@ -2660,35 +2761,13 @@ dependencies = [
]
[[package]]
-name = "prost"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a"
-dependencies = [
- "bytes",
- "prost-derive",
-]
-
-[[package]]
-name = "prost-derive"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
-dependencies = [
- "anyhow",
- "itertools",
- "proc-macro2",
- "quote",
- "syn 2.0.48",
-]
-
-[[package]]
-name = "prost-types"
-version = "0.12.3"
+name = "quick-xml"
+version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e"
+checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
- "prost",
+ "memchr",
+ "tokio",
]
[[package]]
@@ -2746,35 +2825,12 @@ dependencies = [
]
[[package]]
-name = "regex"
-version = "1.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-automata 0.4.3",
- "regex-syntax 0.8.2",
-]
-
-[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
- "regex-syntax 0.6.29",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax 0.8.2",
+ "regex-syntax",
]
[[package]]
@@ -2790,10 +2846,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
-name = "regex-syntax"
-version = "0.8.2"
+name = "reqwest"
+version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
+checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "encoding_rs",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2 0.4.2",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "http-body-util",
+ "hyper 1.2.0",
+ "hyper-tls",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite 0.2.13",
+ "rustls-pemfile 2.1.1",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "system-configuration",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
[[package]]
name = "rfc6979"
@@ -2918,9 +3011,9 @@ dependencies = [
[[package]]
name = "rustix"
-version = "0.38.30"
+version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca"
+checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [
"bitflags 2.4.2",
"errno",
@@ -2962,7 +3055,7 @@ dependencies = [
"log",
"ring 0.17.7",
"rustls-pki-types",
- "rustls-webpki 0.102.1",
+ "rustls-webpki 0.102.2",
"subtle",
"zeroize",
]
@@ -2986,7 +3079,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"
dependencies = [
"openssl-probe",
- "rustls-pemfile 2.0.0",
+ "rustls-pemfile 2.1.1",
"rustls-pki-types",
"schannel",
"security-framework",
@@ -3003,9 +3096,9 @@ dependencies = [
[[package]]
name = "rustls-pemfile"
-version = "2.0.0"
+version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35e4980fa29e4c4b212ffb3db068a564cbf560e51d3944b7c88bd8bf5bec64f4"
+checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab"
dependencies = [
"base64 0.21.7",
"rustls-pki-types",
@@ -3013,9 +3106,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
-version = "1.1.0"
+version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e9d979b3ce68192e42760c7810125eb6cf2ea10efae545a156063e61f314e2a"
+checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8"
[[package]]
name = "rustls-webpki"
@@ -3029,9 +3122,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
-version = "0.102.1"
+version = "0.102.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef4ca26037c909dedb327b48c3327d0ba91d3dd3c4e05dad328f210ffb68e95b"
+checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
dependencies = [
"ring 0.17.7",
"rustls-pki-types",
@@ -3039,16 +3132,10 @@ dependencies = [
]
[[package]]
-name = "rustversion"
-version = "1.0.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
-
-[[package]]
name = "ryu"
-version = "1.0.16"
+version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
+checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "same-file"
@@ -3117,9 +3204,9 @@ dependencies = [
[[package]]
name = "semver"
-version = "1.0.21"
+version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
+checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]]
name = "serde"
@@ -3143,10 +3230,22 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.111"
+version = "1.0.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
+ "form_urlencoded",
"itoa",
"ryu",
"serde",
@@ -3253,9 +3352,9 @@ dependencies = [
"futures",
"idna 0.2.3",
"lazy_static",
- "nom 6.1.2",
+ "nom 6.2.2",
"pin-project",
- "regex-automata 0.1.10",
+ "regex-automata",
"serde",
]
@@ -3395,12 +3494,45 @@ dependencies = [
]
[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
+name = "tempfile"
+version = "3.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+dependencies = [
+ "cfg-if",
+ "fastrand 2.0.1",
+ "rustix 0.38.31",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3411,9 +3543,9 @@ dependencies = [
[[package]]
name = "textwrap"
-version = "0.16.0"
+version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
+checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
[[package]]
name = "thiserror"
@@ -3437,9 +3569,9 @@ dependencies = [
[[package]]
name = "thread_local"
-version = "1.1.7"
+version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
@@ -3491,9 +3623,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.35.1"
+version = "1.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
+checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
dependencies = [
"backtrace",
"bytes",
@@ -3504,21 +3636,10 @@ dependencies = [
"signal-hook-registry",
"socket2 0.5.5",
"tokio-macros",
- "tracing",
"windows-sys 0.48.0",
]
[[package]]
-name = "tokio-io-timeout"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
-dependencies = [
- "pin-project-lite 0.2.13",
- "tokio",
-]
-
-[[package]]
name = "tokio-macros"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3530,6 +3651,16 @@ dependencies = [
]
[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
name = "tokio-rustls"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3597,33 +3728,6 @@ dependencies = [
]
[[package]]
-name = "tonic"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e"
-dependencies = [
- "async-stream",
- "async-trait",
- "axum",
- "base64 0.21.7",
- "bytes",
- "h2 0.3.24",
- "http 0.2.11",
- "http-body 0.4.6",
- "hyper 0.14.28",
- "hyper-timeout",
- "percent-encoding",
- "pin-project",
- "prost",
- "tokio",
- "tokio-stream",
- "tower",
- "tower-layer",
- "tower-service",
- "tracing",
-]
-
-[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3631,13 +3735,9 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
- "indexmap 1.9.3",
"pin-project",
"pin-project-lite 0.2.13",
- "rand",
- "slab",
"tokio",
- "tokio-util",
"tower-layer",
"tower-service",
"tracing",
@@ -3705,14 +3805,10 @@ version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
- "matchers",
"nu-ansi-term",
- "once_cell",
- "regex",
"sharded-slab",
"smallvec",
"thread_local",
- "tracing",
"tracing-core",
"tracing-log",
]
@@ -3790,6 +3886,10 @@ name = "uuid"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
+dependencies = [
+ "getrandom",
+ "wasm-bindgen",
+]
[[package]]
name = "valuable"
@@ -3799,9 +3899,15 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "value-bag"
-version = "1.6.0"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "126e423afe2dd9ac52142e7e9d5ce4135d7e13776c529d27fd6bc49f19e3280b"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cdbaf5e132e593e9fc1de6a15bbec912395b11fb9719e061cf64f804524c503"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
@@ -4105,6 +4211,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
+name = "winreg"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
name = "wyz"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.nix b/Cargo.nix
index 7c184ab..a1776cf 100644
--- a/Cargo.nix
+++ b/Cargo.nix
@@ -4,6 +4,14 @@
args@{
release ? true,
rootFeatures ? [
+ "aero-user/default"
+ "aero-bayou/default"
+ "aero-sasl/default"
+ "aero-dav/default"
+ "aerogramme-fuzz/default"
+ "aero-collections/default"
+ "aero-proto/default"
+ "aero-ical/default"
"aerogramme/default"
],
rustPackages,
@@ -23,7 +31,7 @@ args@{
ignoreLockHash,
}:
let
- nixifiedLockHash = "1636bcbca38619e40eeddcb9e9c0af1240654ca176fe1a6fb3444b21526b53c2";
+ nixifiedLockHash = "a4bc3889db1e92c4aa5331faefe47cd3fc7925ec548a168e1555b3a8c3eebf08";
workspaceSrc = if args.workspaceSrc == null then ./. else args.workspaceSrc;
currentLockHash = builtins.hashFile "sha256" (workspaceSrc + /Cargo.lock);
lockHashIgnored = if ignoreLockHash
@@ -45,7 +53,15 @@ in
{
cargo2nixVersion = "0.11.0";
workspace = {
- aerogramme = rustPackages.unknown.aerogramme."0.2.2";
+ aero-user = rustPackages.unknown.aero-user."0.3.0";
+ aero-bayou = rustPackages.unknown.aero-bayou."0.3.0";
+ aero-sasl = rustPackages.unknown.aero-sasl."0.3.0";
+ aero-dav = rustPackages.unknown.aero-dav."0.3.0";
+ aerogramme-fuzz = rustPackages.unknown.aerogramme-fuzz."0.0.0";
+ aero-collections = rustPackages.unknown.aero-collections."0.3.0";
+ aero-proto = rustPackages.unknown.aero-proto."0.3.0";
+ aero-ical = rustPackages.unknown.aero-ical."0.3.0";
+ aerogramme = rustPackages.unknown.aerogramme."0.3.0";
};
"registry+https://github.com/rust-lang/crates.io-index".abnf-core."0.6.0" = overridableMkRustCrate (profileName: rec {
name = "abnf-core";
@@ -57,13 +73,13 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".addr2line."0.21.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".addr2line."0.15.2" = overridableMkRustCrate (profileName: rec {
name = "addr2line";
- version = "0.21.0";
+ version = "0.15.2";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"; };
+ src = fetchCratesIo { inherit name version; sha256 = "e7a2e47a1fbe209ee101dd6d61285226744c6c8d3c21c8dc878ba6cb9f467f3a"; };
dependencies = {
- gimli = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".gimli."0.28.1" { inherit profileName; }).out;
+ gimli = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".gimli."0.24.0" { inherit profileName; }).out;
};
});
@@ -74,72 +90,202 @@ in
src = fetchCratesIo { inherit name version; sha256 = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"; };
});
- "unknown".aerogramme."0.2.2" = overridableMkRustCrate (profileName: rec {
- name = "aerogramme";
- version = "0.2.2";
+ "unknown".aero-bayou."0.3.0" = overridableMkRustCrate (profileName: rec {
+ name = "aero-bayou";
+ version = "0.3.0";
+ registry = "unknown";
+ src = fetchCrateLocal (workspaceSrc + "/aero-bayou");
+ dependencies = {
+ aero_user = (rustPackages."unknown".aero-user."0.3.0" { inherit profileName; }).out;
+ anyhow = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".anyhow."1.0.79" { inherit profileName; }).out;
+ hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
+ log = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.20" { inherit profileName; }).out;
+ rand = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }).out;
+ serde = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.195" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
+ tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
+ };
+ });
+
+ "unknown".aero-collections."0.3.0" = overridableMkRustCrate (profileName: rec {
+ name = "aero-collections";
+ version = "0.3.0";
+ registry = "unknown";
+ src = fetchCrateLocal (workspaceSrc + "/aero-collections");
+ dependencies = {
+ aero_bayou = (rustPackages."unknown".aero-bayou."0.3.0" { inherit profileName; }).out;
+ aero_user = (rustPackages."unknown".aero-user."0.3.0" { inherit profileName; }).out;
+ anyhow = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".anyhow."1.0.79" { inherit profileName; }).out;
+ base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.21.7" { inherit profileName; }).out;
+ eml_codec = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".eml-codec."0.1.2" { inherit profileName; }).out;
+ futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.30" { inherit profileName; }).out;
+ hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
+ icalendar = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".icalendar."0.16.1" { inherit profileName; }).out;
+ im = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".im."15.1.0" { inherit profileName; }).out;
+ lazy_static = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }).out;
+ rand = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }).out;
+ serde = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.195" { inherit profileName; }).out;
+ sodiumoxide = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".sodiumoxide."0.2.7" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
+ tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
+ };
+ });
+
+ "unknown".aero-dav."0.3.0" = overridableMkRustCrate (profileName: rec {
+ name = "aero-dav";
+ version = "0.3.0";
+ registry = "unknown";
+ src = fetchCrateLocal (workspaceSrc + "/aero-dav");
+ dependencies = {
+ chrono = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.38" { inherit profileName; }).out;
+ futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.30" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.1.0" { inherit profileName; }).out;
+ quick_xml = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".quick-xml."0.31.0" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
+ tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
+ };
+ });
+
+ "unknown".aero-ical."0.3.0" = overridableMkRustCrate (profileName: rec {
+ name = "aero-ical";
+ version = "0.3.0";
+ registry = "unknown";
+ src = fetchCrateLocal (workspaceSrc + "/aero-ical");
+ dependencies = {
+ aero_dav = (rustPackages."unknown".aero-dav."0.3.0" { inherit profileName; }).out;
+ chrono = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.38" { inherit profileName; }).out;
+ icalendar = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".icalendar."0.16.1" { inherit profileName; }).out;
+ nom = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.3" { inherit profileName; }).out;
+ tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
+ };
+ });
+
+ "unknown".aero-proto."0.3.0" = overridableMkRustCrate (profileName: rec {
+ name = "aero-proto";
+ version = "0.3.0";
registry = "unknown";
- src = fetchCrateLocal workspaceSrc;
+ src = fetchCrateLocal (workspaceSrc + "/aero-proto");
dependencies = {
+ aero_collections = (rustPackages."unknown".aero-collections."0.3.0" { inherit profileName; }).out;
+ aero_dav = (rustPackages."unknown".aero-dav."0.3.0" { inherit profileName; }).out;
+ aero_ical = (rustPackages."unknown".aero-ical."0.3.0" { inherit profileName; }).out;
+ aero_sasl = (rustPackages."unknown".aero-sasl."0.3.0" { inherit profileName; }).out;
+ aero_user = (rustPackages."unknown".aero-user."0.3.0" { inherit profileName; }).out;
anyhow = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".anyhow."1.0.79" { inherit profileName; }).out;
- argon2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".argon2."0.5.2" { inherit profileName; }).out;
async_trait = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.77" { profileName = "__noProfile"; }).out;
- aws_config = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-config."1.1.6" { inherit profileName; }).out;
- aws_sdk_s3 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sdk-s3."1.16.0" { inherit profileName; }).out;
- aws_smithy_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime."1.1.7" { inherit profileName; }).out;
- aws_smithy_runtime_api = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime-api."1.1.7" { inherit profileName; }).out;
- backtrace = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".backtrace."0.3.69" { inherit profileName; }).out;
base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.21.7" { inherit profileName; }).out;
- chrono = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.31" { inherit profileName; }).out;
- clap = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".clap."3.2.25" { inherit profileName; }).out;
- console_subscriber = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".console-subscriber."0.2.0" { inherit profileName; }).out;
+ chrono = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.38" { inherit profileName; }).out;
duplexify = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".duplexify."1.2.2" { inherit profileName; }).out;
eml_codec = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".eml-codec."0.1.2" { inherit profileName; }).out;
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.30" { inherit profileName; }).out;
- hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
- hyper_rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper-rustls."0.26.0" { inherit profileName; }).out;
+ http_body_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body-util."0.1.1" { inherit profileName; }).out;
+ hyper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."1.2.0" { inherit profileName; }).out;
hyper_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper-util."0.1.3" { inherit profileName; }).out;
- im = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".im."15.1.0" { inherit profileName; }).out;
+ icalendar = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".icalendar."0.16.1" { inherit profileName; }).out;
imap_codec = (rustPackages."git+https://github.com/superboum/imap-codec".imap-codec."2.0.0" { inherit profileName; }).out;
imap_flow = (rustPackages."git+https://github.com/duesee/imap-flow.git".imap-flow."0.1.0" { inherit profileName; }).out;
- itertools = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".itertools."0.10.5" { inherit profileName; }).out;
+ quick_xml = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".quick-xml."0.31.0" { inherit profileName; }).out;
+ rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls."0.22.2" { inherit profileName; }).out;
+ rustls_pemfile = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pemfile."2.1.1" { inherit profileName; }).out;
+ smtp_message = (rustPackages."git+http://github.com/Alexis211/kannader".smtp-message."0.1.0" { inherit profileName; }).out;
+ smtp_server = (rustPackages."git+http://github.com/Alexis211/kannader".smtp-server."0.1.0" { inherit profileName; }).out;
+ thiserror = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.56" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
+ tokio_rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-rustls."0.25.0" { inherit profileName; }).out;
+ tokio_stream = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.14" { inherit profileName; }).out;
+ tokio_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.10" { inherit profileName; }).out;
+ tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
+ };
+ });
+
+ "unknown".aero-sasl."0.3.0" = overridableMkRustCrate (profileName: rec {
+ name = "aero-sasl";
+ version = "0.3.0";
+ registry = "unknown";
+ src = fetchCrateLocal (workspaceSrc + "/aero-sasl");
+ dependencies = {
+ anyhow = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".anyhow."1.0.79" { inherit profileName; }).out;
+ base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.21.7" { inherit profileName; }).out;
+ futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.30" { inherit profileName; }).out;
+ hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
+ nom = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.3" { inherit profileName; }).out;
+ rand = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
+ tokio_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.10" { inherit profileName; }).out;
+ tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
+ };
+ });
+
+ "unknown".aero-user."0.3.0" = overridableMkRustCrate (profileName: rec {
+ name = "aero-user";
+ version = "0.3.0";
+ registry = "unknown";
+ src = fetchCrateLocal (workspaceSrc + "/aero-user");
+ dependencies = {
+ anyhow = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".anyhow."1.0.79" { inherit profileName; }).out;
+ argon2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".argon2."0.5.3" { inherit profileName; }).out;
+ async_trait = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.77" { profileName = "__noProfile"; }).out;
+ aws_config = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-config."1.1.7" { inherit profileName; }).out;
+ aws_sdk_s3 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sdk-s3."1.17.0" { inherit profileName; }).out;
+ aws_smithy_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime."1.1.7" { inherit profileName; }).out;
+ aws_smithy_runtime_api = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime-api."1.1.7" { inherit profileName; }).out;
+ base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.21.7" { inherit profileName; }).out;
+ hyper_rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper-rustls."0.26.0" { inherit profileName; }).out;
+ hyper_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper-util."0.1.3" { inherit profileName; }).out;
k2v_client = (rustPackages."git+https://git.deuxfleurs.fr/Deuxfleurs/garage.git".k2v-client."0.0.4" { inherit profileName; }).out;
- lazy_static = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }).out;
ldap3 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".ldap3."0.10.6" { inherit profileName; }).out;
log = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.20" { inherit profileName; }).out;
- nix = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nix."0.27.1" { inherit profileName; }).out;
- nom = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.3" { inherit profileName; }).out;
rand = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }).out;
rmp_serde = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rmp-serde."0.15.5" { inherit profileName; }).out;
- rpassword = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rpassword."7.3.1" { inherit profileName; }).out;
- rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls."0.22.2" { inherit profileName; }).out;
- rustls_pemfile = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pemfile."2.0.0" { inherit profileName; }).out;
serde = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.195" { inherit profileName; }).out;
- smtp_message = (rustPackages."git+http://github.com/Alexis211/kannader".smtp-message."0.1.0" { inherit profileName; }).out;
- smtp_server = (rustPackages."git+http://github.com/Alexis211/kannader".smtp-server."0.1.0" { inherit profileName; }).out;
sodiumoxide = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".sodiumoxide."0.2.7" { inherit profileName; }).out;
- thiserror = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.56" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
- tokio_rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-rustls."0.25.0" { inherit profileName; }).out;
- tokio_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.10" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
toml = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".toml."0.5.11" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
- tracing_subscriber = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-subscriber."0.3.18" { inherit profileName; }).out;
zstd = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".zstd."0.9.2+zstd.1.5.1" { inherit profileName; }).out;
};
});
- "registry+https://github.com/rust-lang/crates.io-index".aho-corasick."1.1.2" = overridableMkRustCrate (profileName: rec {
- name = "aho-corasick";
- version = "1.1.2";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"; };
+ "unknown".aerogramme."0.3.0" = overridableMkRustCrate (profileName: rec {
+ name = "aerogramme";
+ version = "0.3.0";
+ registry = "unknown";
+ src = fetchCrateLocal (workspaceSrc + "/aerogramme");
+ dependencies = {
+ aero_proto = (rustPackages."unknown".aero-proto."0.3.0" { inherit profileName; }).out;
+ aero_user = (rustPackages."unknown".aero-user."0.3.0" { inherit profileName; }).out;
+ anyhow = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".anyhow."1.0.79" { inherit profileName; }).out;
+ backtrace = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".backtrace."0.3.59" { inherit profileName; }).out;
+ clap = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".clap."3.2.25" { inherit profileName; }).out;
+ futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.30" { inherit profileName; }).out;
+ log = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.20" { inherit profileName; }).out;
+ nix = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nix."0.27.1" { inherit profileName; }).out;
+ rpassword = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rpassword."7.3.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
+ tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
+ tracing_subscriber = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-subscriber."0.3.18" { inherit profileName; }).out;
+ };
+ devDependencies = {
+ aero_dav = (rustPackages."unknown".aero-dav."0.3.0" { inherit profileName; }).out;
+ quick_xml = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".quick-xml."0.31.0" { inherit profileName; }).out;
+ reqwest = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".reqwest."0.12.4" { inherit profileName; }).out;
+ };
+ });
+
+ "unknown".aerogramme-fuzz."0.0.0" = overridableMkRustCrate (profileName: rec {
+ name = "aerogramme-fuzz";
+ version = "0.0.0";
+ registry = "unknown";
+ src = fetchCrateLocal (workspaceSrc + "/aero-dav/fuzz");
features = builtins.concatLists [
- [ "default" ]
- [ "perf-literal" ]
- [ "std" ]
+ (lib.optional (rootFeatures' ? "aerogramme-fuzz/arbitrary") "arbitrary")
];
dependencies = {
- memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.7.1" { inherit profileName; }).out;
+ aero_dav = (rustPackages."unknown".aero-dav."0.3.0" { inherit profileName; }).out;
+ ${ if rootFeatures' ? "aerogramme-fuzz/arbitrary" then "arbitrary" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".arbitrary."1.3.2" { inherit profileName; }).out;
+ libfuzzer_sys = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libfuzzer-sys."0.4.7" { inherit profileName; }).out;
+ quick_xml = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".quick-xml."0.31.0" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
};
});
@@ -171,11 +317,25 @@ in
];
});
- "registry+https://github.com/rust-lang/crates.io-index".argon2."0.5.2" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".arbitrary."1.3.2" = overridableMkRustCrate (profileName: rec {
+ name = "arbitrary";
+ version = "1.3.2";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"; };
+ features = builtins.concatLists [
+ [ "derive" ]
+ [ "derive_arbitrary" ]
+ ];
+ dependencies = {
+ derive_arbitrary = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".derive_arbitrary."1.3.2" { profileName = "__noProfile"; }).out;
+ };
+ });
+
+ "registry+https://github.com/rust-lang/crates.io-index".argon2."0.5.3" = overridableMkRustCrate (profileName: rec {
name = "argon2";
- version = "0.5.2";
+ version = "0.5.3";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9"; };
+ src = fetchCratesIo { inherit name version; sha256 = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"; };
features = builtins.concatLists [
[ "alloc" ]
[ "default" ]
@@ -260,19 +420,19 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".async-channel."2.1.1" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".async-channel."2.2.0" = overridableMkRustCrate (profileName: rec {
name = "async-channel";
- version = "2.1.1";
+ version = "2.2.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c"; };
+ src = fetchCratesIo { inherit name version; sha256 = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3"; };
features = builtins.concatLists [
[ "default" ]
[ "std" ]
];
dependencies = {
concurrent_queue = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".concurrent-queue."2.4.0" { inherit profileName; }).out;
- event_listener = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".event-listener."4.0.3" { inherit profileName; }).out;
- event_listener_strategy = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".event-listener-strategy."0.4.0" { inherit profileName; }).out;
+ event_listener = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".event-listener."5.2.0" { inherit profileName; }).out;
+ event_listener_strategy = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".event-listener-strategy."0.5.0" { inherit profileName; }).out;
futures_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.30" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
};
@@ -318,9 +478,9 @@ in
[ "default" ]
];
dependencies = {
- async_channel = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-channel."2.1.1" { inherit profileName; }).out;
+ async_channel = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-channel."2.2.0" { inherit profileName; }).out;
async_executor = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-executor."1.8.0" { inherit profileName; }).out;
- async_io = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-io."2.3.0" { inherit profileName; }).out;
+ async_io = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-io."2.3.1" { inherit profileName; }).out;
async_lock = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-lock."3.3.0" { inherit profileName; }).out;
blocking = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".blocking."1.5.1" { inherit profileName; }).out;
futures_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-lite."2.2.0" { inherit profileName; }).out;
@@ -351,11 +511,11 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".async-io."2.3.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".async-io."2.3.1" = overridableMkRustCrate (profileName: rec {
name = "async-io";
- version = "2.3.0";
+ version = "2.3.1";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "fb41eb19024a91746eba0773aa5e16036045bbf45733766661099e182ea6a744"; };
+ src = fetchCratesIo { inherit name version; sha256 = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65"; };
dependencies = {
async_lock = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-lock."3.3.0" { inherit profileName; }).out;
cfg_if = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }).out;
@@ -363,8 +523,8 @@ in
futures_io = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.30" { inherit profileName; }).out;
futures_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-lite."2.2.0" { inherit profileName; }).out;
parking = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking."2.2.0" { inherit profileName; }).out;
- polling = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".polling."3.3.2" { inherit profileName; }).out;
- rustix = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustix."0.38.30" { inherit profileName; }).out;
+ polling = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".polling."3.5.0" { inherit profileName; }).out;
+ rustix = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustix."0.38.31" { inherit profileName; }).out;
slab = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.9" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
${ if hostPlatform.isWindows then "windows_sys" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows-sys."0.52.0" { inherit profileName; }).out;
@@ -422,7 +582,7 @@ in
cfg_if = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }).out;
event_listener = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".event-listener."3.1.0" { inherit profileName; }).out;
futures_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-lite."1.13.0" { inherit profileName; }).out;
- ${ if hostPlatform.isUnix then "rustix" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustix."0.38.30" { inherit profileName; }).out;
+ ${ if hostPlatform.isUnix then "rustix" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustix."0.38.31" { inherit profileName; }).out;
${ if hostPlatform.isWindows then "windows_sys" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows-sys."0.48.0" { inherit profileName; }).out;
};
});
@@ -433,13 +593,13 @@ in
registry = "registry+https://github.com/rust-lang/crates.io-index";
src = fetchCratesIo { inherit name version; sha256 = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5"; };
dependencies = {
- ${ if hostPlatform.isUnix then "async_io" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-io."2.3.0" { inherit profileName; }).out;
+ ${ if hostPlatform.isUnix then "async_io" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-io."2.3.1" { inherit profileName; }).out;
${ if hostPlatform.isWindows then "async_lock" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-lock."2.8.0" { inherit profileName; }).out;
${ if hostPlatform.isWindows then "atomic_waker" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".atomic-waker."1.1.2" { inherit profileName; }).out;
cfg_if = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }).out;
futures_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.30" { inherit profileName; }).out;
${ if hostPlatform.isUnix then "futures_io" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.30" { inherit profileName; }).out;
- ${ if hostPlatform.isUnix then "rustix" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustix."0.38.30" { inherit profileName; }).out;
+ ${ if hostPlatform.isUnix then "rustix" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustix."0.38.31" { inherit profileName; }).out;
${ if hostPlatform.isUnix then "signal_hook_registry" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".signal-hook-registry."1.4.1" { inherit profileName; }).out;
${ if hostPlatform.isWindows then "slab" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.9" { inherit profileName; }).out;
${ if hostPlatform.isWindows then "windows_sys" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows-sys."0.48.0" { inherit profileName; }).out;
@@ -487,7 +647,7 @@ in
${ if hostPlatform.parsed.cpu.name == "wasm32" then "gloo_timers" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".gloo-timers."0.2.6" { inherit profileName; }).out;
kv_log_macro = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".kv-log-macro."1.0.7" { inherit profileName; }).out;
log = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.20" { inherit profileName; }).out;
- memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.7.1" { inherit profileName; }).out;
+ memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.3.4" { inherit profileName; }).out;
once_cell = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.19.0" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
pin_utils = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-utils."0.1.0" { inherit profileName; }).out;
@@ -496,30 +656,6 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".async-stream."0.3.5" = overridableMkRustCrate (profileName: rec {
- name = "async-stream";
- version = "0.3.5";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"; };
- dependencies = {
- async_stream_impl = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-stream-impl."0.3.5" { profileName = "__noProfile"; }).out;
- futures_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.30" { inherit profileName; }).out;
- pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
- };
- });
-
- "registry+https://github.com/rust-lang/crates.io-index".async-stream-impl."0.3.5" = overridableMkRustCrate (profileName: rec {
- name = "async-stream-impl";
- version = "0.3.5";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"; };
- dependencies = {
- proc_macro2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.76" { inherit profileName; }).out;
- quote = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.35" { inherit profileName; }).out;
- syn = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."2.0.48" { inherit profileName; }).out;
- };
- });
-
"registry+https://github.com/rust-lang/crates.io-index".async-task."4.7.0" = overridableMkRustCrate (profileName: rec {
name = "async-task";
version = "4.7.0";
@@ -615,11 +751,11 @@ in
src = fetchCratesIo { inherit name version; sha256 = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"; };
});
- "registry+https://github.com/rust-lang/crates.io-index".aws-config."1.1.6" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".aws-config."1.1.7" = overridableMkRustCrate (profileName: rec {
name = "aws-config";
- version = "1.1.6";
+ version = "1.1.7";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "3182c19847238b50b62ae0383a6dbfc14514e552eb5e307e1ea83ccf5840b8a6"; };
+ src = fetchCratesIo { inherit name version; sha256 = "0b96342ea8948ab9bef3e6234ea97fc32e2d8a88d8fb6a084e52267317f94b6b"; };
features = builtins.concatLists [
[ "behavior-version-latest" ]
[ "client-hyper" ]
@@ -630,36 +766,36 @@ in
[ "sso" ]
];
dependencies = {
- aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.6" { inherit profileName; }).out;
- aws_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-runtime."1.1.6" { inherit profileName; }).out;
- aws_sdk_sso = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sdk-sso."1.14.0" { inherit profileName; }).out;
- aws_sdk_ssooidc = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sdk-ssooidc."1.14.0" { inherit profileName; }).out;
- aws_sdk_sts = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sdk-sts."1.14.0" { inherit profileName; }).out;
+ aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.7" { inherit profileName; }).out;
+ aws_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-runtime."1.1.7" { inherit profileName; }).out;
+ aws_sdk_sso = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sdk-sso."1.15.0" { inherit profileName; }).out;
+ aws_sdk_ssooidc = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sdk-ssooidc."1.15.0" { inherit profileName; }).out;
+ aws_sdk_sts = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sdk-sts."1.15.0" { inherit profileName; }).out;
aws_smithy_async = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-async."1.1.7" { inherit profileName; }).out;
aws_smithy_http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-http."0.60.6" { inherit profileName; }).out;
aws_smithy_json = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-json."0.60.6" { inherit profileName; }).out;
aws_smithy_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime."1.1.7" { inherit profileName; }).out;
aws_smithy_runtime_api = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime-api."1.1.7" { inherit profileName; }).out;
aws_smithy_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."1.1.7" { inherit profileName; }).out;
- aws_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.6" { inherit profileName; }).out;
+ aws_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.7" { inherit profileName; }).out;
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
fastrand = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".fastrand."2.0.1" { inherit profileName; }).out;
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
hyper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.28" { inherit profileName; }).out;
ring = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".ring."0.17.7" { inherit profileName; }).out;
time = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".time."0.3.31" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
zeroize = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".zeroize."1.7.0" { inherit profileName; }).out;
};
});
- "registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.6" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.7" = overridableMkRustCrate (profileName: rec {
name = "aws-credential-types";
- version = "1.1.6";
+ version = "1.1.7";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "e5635d8707f265c773282a22abe1ecd4fbe96a8eb2f0f14c0796f8016f11a41a"; };
+ src = fetchCratesIo { inherit name version; sha256 = "273fa47dafc9ef14c2c074ddddbea4561ff01b7f68d5091c0e9737ced605c01d"; };
features = builtins.concatLists [
[ "test-util" ]
];
@@ -671,28 +807,28 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".aws-runtime."1.1.6" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".aws-runtime."1.1.7" = overridableMkRustCrate (profileName: rec {
name = "aws-runtime";
- version = "1.1.6";
+ version = "1.1.7";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "6f82b9ae2adfd9d6582440d0eeb394c07f74d21b4c0cc72bdb73735c9e1a9c0e"; };
+ src = fetchCratesIo { inherit name version; sha256 = "6e38bab716c8bf07da24be07ecc02e0f5656ce8f30a891322ecdcb202f943b85"; };
features = builtins.concatLists [
[ "event-stream" ]
[ "http-02x" ]
[ "sigv4a" ]
];
dependencies = {
- aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.6" { inherit profileName; }).out;
- aws_sigv4 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sigv4."1.1.6" { inherit profileName; }).out;
+ aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.7" { inherit profileName; }).out;
+ aws_sigv4 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sigv4."1.1.7" { inherit profileName; }).out;
aws_smithy_async = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-async."1.1.7" { inherit profileName; }).out;
aws_smithy_eventstream = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-eventstream."0.60.4" { inherit profileName; }).out;
aws_smithy_http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-http."0.60.6" { inherit profileName; }).out;
aws_smithy_runtime_api = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime-api."1.1.7" { inherit profileName; }).out;
aws_smithy_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."1.1.7" { inherit profileName; }).out;
- aws_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.6" { inherit profileName; }).out;
+ aws_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.7" { inherit profileName; }).out;
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
fastrand = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".fastrand."2.0.1" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
http_body = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.6" { inherit profileName; }).out;
percent_encoding = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.3.1" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
@@ -701,39 +837,39 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".aws-sdk-config."1.15.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".aws-sdk-config."1.16.0" = overridableMkRustCrate (profileName: rec {
name = "aws-sdk-config";
- version = "1.15.0";
+ version = "1.16.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "0cb71960e3e197c3f512f3bf0f47f444acd708db59733416107ec2ff161ff5c4"; };
+ src = fetchCratesIo { inherit name version; sha256 = "07979fd68679736ba306d6ea2a4dc2fd835ac4d454942c5d8920ef83ed2f979f"; };
features = builtins.concatLists [
[ "default" ]
[ "rt-tokio" ]
[ "rustls" ]
];
dependencies = {
- aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.6" { inherit profileName; }).out;
- aws_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-runtime."1.1.6" { inherit profileName; }).out;
+ aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.7" { inherit profileName; }).out;
+ aws_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-runtime."1.1.7" { inherit profileName; }).out;
aws_smithy_async = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-async."1.1.7" { inherit profileName; }).out;
aws_smithy_http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-http."0.60.6" { inherit profileName; }).out;
aws_smithy_json = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-json."0.60.6" { inherit profileName; }).out;
aws_smithy_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime."1.1.7" { inherit profileName; }).out;
aws_smithy_runtime_api = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime-api."1.1.7" { inherit profileName; }).out;
aws_smithy_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."1.1.7" { inherit profileName; }).out;
- aws_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.6" { inherit profileName; }).out;
+ aws_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.7" { inherit profileName; }).out;
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
once_cell = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.19.0" { inherit profileName; }).out;
regex_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-lite."0.1.5" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
};
});
- "registry+https://github.com/rust-lang/crates.io-index".aws-sdk-s3."1.16.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".aws-sdk-s3."1.17.0" = overridableMkRustCrate (profileName: rec {
name = "aws-sdk-s3";
- version = "1.16.0";
+ version = "1.17.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "5076637347e7d0218e61facae853110682ae58efabd2f4e2a9e530c203d5fa7b"; };
+ src = fetchCratesIo { inherit name version; sha256 = "93d35d39379445970fc3e4ddf7559fff2c32935ce0b279f9cb27080d6b7c6d94"; };
features = builtins.concatLists [
[ "default" ]
[ "rt-tokio" ]
@@ -741,9 +877,9 @@ in
[ "sigv4a" ]
];
dependencies = {
- aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.6" { inherit profileName; }).out;
- aws_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-runtime."1.1.6" { inherit profileName; }).out;
- aws_sigv4 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sigv4."1.1.6" { inherit profileName; }).out;
+ aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.7" { inherit profileName; }).out;
+ aws_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-runtime."1.1.7" { inherit profileName; }).out;
+ aws_sigv4 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sigv4."1.1.7" { inherit profileName; }).out;
aws_smithy_async = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-async."1.1.7" { inherit profileName; }).out;
aws_smithy_checksums = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-checksums."0.60.6" { inherit profileName; }).out;
aws_smithy_eventstream = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-eventstream."0.60.4" { inherit profileName; }).out;
@@ -753,9 +889,9 @@ in
aws_smithy_runtime_api = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime-api."1.1.7" { inherit profileName; }).out;
aws_smithy_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."1.1.7" { inherit profileName; }).out;
aws_smithy_xml = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-xml."0.60.6" { inherit profileName; }).out;
- aws_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.6" { inherit profileName; }).out;
+ aws_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.7" { inherit profileName; }).out;
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
http_body = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.6" { inherit profileName; }).out;
once_cell = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.19.0" { inherit profileName; }).out;
percent_encoding = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.3.1" { inherit profileName; }).out;
@@ -765,60 +901,60 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".aws-sdk-sso."1.14.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".aws-sdk-sso."1.15.0" = overridableMkRustCrate (profileName: rec {
name = "aws-sdk-sso";
- version = "1.14.0";
+ version = "1.15.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "ca7e8097448832fcd22faf6bb227e97d76b40e354509d1307653a885811c7151"; };
+ src = fetchCratesIo { inherit name version; sha256 = "d84bd3925a17c9adbf6ec65d52104a44a09629d8f70290542beeee69a95aee7f"; };
dependencies = {
- aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.6" { inherit profileName; }).out;
- aws_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-runtime."1.1.6" { inherit profileName; }).out;
+ aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.7" { inherit profileName; }).out;
+ aws_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-runtime."1.1.7" { inherit profileName; }).out;
aws_smithy_async = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-async."1.1.7" { inherit profileName; }).out;
aws_smithy_http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-http."0.60.6" { inherit profileName; }).out;
aws_smithy_json = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-json."0.60.6" { inherit profileName; }).out;
aws_smithy_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime."1.1.7" { inherit profileName; }).out;
aws_smithy_runtime_api = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime-api."1.1.7" { inherit profileName; }).out;
aws_smithy_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."1.1.7" { inherit profileName; }).out;
- aws_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.6" { inherit profileName; }).out;
+ aws_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.7" { inherit profileName; }).out;
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
once_cell = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.19.0" { inherit profileName; }).out;
regex_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-lite."0.1.5" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
};
});
- "registry+https://github.com/rust-lang/crates.io-index".aws-sdk-ssooidc."1.14.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".aws-sdk-ssooidc."1.15.0" = overridableMkRustCrate (profileName: rec {
name = "aws-sdk-ssooidc";
- version = "1.14.0";
+ version = "1.15.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "a75073590e23d63044606771afae309fada8eb10ded54a1ce4598347221d3fef"; };
+ src = fetchCratesIo { inherit name version; sha256 = "2c2dae39e997f58bc4d6292e6244b26ba630c01ab671b6f9f44309de3eb80ab8"; };
dependencies = {
- aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.6" { inherit profileName; }).out;
- aws_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-runtime."1.1.6" { inherit profileName; }).out;
+ aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.7" { inherit profileName; }).out;
+ aws_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-runtime."1.1.7" { inherit profileName; }).out;
aws_smithy_async = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-async."1.1.7" { inherit profileName; }).out;
aws_smithy_http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-http."0.60.6" { inherit profileName; }).out;
aws_smithy_json = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-json."0.60.6" { inherit profileName; }).out;
aws_smithy_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime."1.1.7" { inherit profileName; }).out;
aws_smithy_runtime_api = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime-api."1.1.7" { inherit profileName; }).out;
aws_smithy_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."1.1.7" { inherit profileName; }).out;
- aws_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.6" { inherit profileName; }).out;
+ aws_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.7" { inherit profileName; }).out;
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
once_cell = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.19.0" { inherit profileName; }).out;
regex_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-lite."0.1.5" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
};
});
- "registry+https://github.com/rust-lang/crates.io-index".aws-sdk-sts."1.14.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".aws-sdk-sts."1.15.0" = overridableMkRustCrate (profileName: rec {
name = "aws-sdk-sts";
- version = "1.14.0";
+ version = "1.15.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "650e4aaae41547151dea4d8142f7ffcc8ab8ba76d5dccc8933936ef2102c3356"; };
+ src = fetchCratesIo { inherit name version; sha256 = "17fd9a53869fee17cea77e352084e1aa71e2c5e323d974c13a9c2bcfd9544c7f"; };
dependencies = {
- aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.6" { inherit profileName; }).out;
- aws_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-runtime."1.1.6" { inherit profileName; }).out;
+ aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.7" { inherit profileName; }).out;
+ aws_runtime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-runtime."1.1.7" { inherit profileName; }).out;
aws_smithy_async = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-async."1.1.7" { inherit profileName; }).out;
aws_smithy_http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-http."0.60.6" { inherit profileName; }).out;
aws_smithy_json = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-json."0.60.6" { inherit profileName; }).out;
@@ -827,19 +963,19 @@ in
aws_smithy_runtime_api = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime-api."1.1.7" { inherit profileName; }).out;
aws_smithy_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."1.1.7" { inherit profileName; }).out;
aws_smithy_xml = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-xml."0.60.6" { inherit profileName; }).out;
- aws_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.6" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ aws_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.7" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
once_cell = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.19.0" { inherit profileName; }).out;
regex_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-lite."0.1.5" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
};
});
- "registry+https://github.com/rust-lang/crates.io-index".aws-sigv4."1.1.6" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".aws-sigv4."1.1.7" = overridableMkRustCrate (profileName: rec {
name = "aws-sigv4";
- version = "1.1.6";
+ version = "1.1.7";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "404c64a104188ac70dd1684718765cb5559795458e446480e41984e68e57d888"; };
+ src = fetchCratesIo { inherit name version; sha256 = "8ada00a4645d7d89f296fe0ddbc3fe3554f03035937c849a05d37ddffc1f29a1"; };
features = builtins.concatLists [
[ "default" ]
[ "http0-compat" ]
@@ -849,7 +985,7 @@ in
[ "sigv4a" ]
];
dependencies = {
- aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.6" { inherit profileName; }).out;
+ aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.7" { inherit profileName; }).out;
aws_smithy_eventstream = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-eventstream."0.60.4" { inherit profileName; }).out;
aws_smithy_http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-http."0.60.6" { inherit profileName; }).out;
aws_smithy_runtime_api = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime-api."1.1.7" { inherit profileName; }).out;
@@ -859,8 +995,8 @@ in
form_urlencoded = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".form_urlencoded."1.2.1" { inherit profileName; }).out;
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
hmac = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hmac."0.12.1" { inherit profileName; }).out;
- http0 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.0.0" { inherit profileName; }).out;
+ http0 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.1.0" { inherit profileName; }).out;
once_cell = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.19.0" { inherit profileName; }).out;
p256 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".p256."0.11.1" { inherit profileName; }).out;
percent_encoding = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.3.1" { inherit profileName; }).out;
@@ -884,7 +1020,7 @@ in
dependencies = {
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.30" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
};
});
@@ -898,9 +1034,9 @@ in
aws_smithy_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."1.1.7" { inherit profileName; }).out;
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
crc32c = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crc32c."0.6.5" { inherit profileName; }).out;
- crc32fast = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crc32fast."1.3.2" { inherit profileName; }).out;
+ crc32fast = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crc32fast."1.4.0" { inherit profileName; }).out;
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
http_body = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.6" { inherit profileName; }).out;
md5 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".md-5."0.10.6" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
@@ -918,7 +1054,7 @@ in
dependencies = {
aws_smithy_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."1.1.7" { inherit profileName; }).out;
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
- crc32fast = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crc32fast."1.3.2" { inherit profileName; }).out;
+ crc32fast = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crc32fast."1.4.0" { inherit profileName; }).out;
};
});
@@ -938,7 +1074,7 @@ in
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
bytes_utils = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes-utils."0.1.4" { inherit profileName; }).out;
futures_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.30" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
http_body = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.6" { inherit profileName; }).out;
once_cell = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.19.0" { inherit profileName; }).out;
percent_encoding = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.3.1" { inherit profileName; }).out;
@@ -988,7 +1124,7 @@ in
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
fastrand = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".fastrand."2.0.1" { inherit profileName; }).out;
h2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.24" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
http_body_0_4 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.6" { inherit profileName; }).out;
hyper_0_14 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.28" { inherit profileName; }).out;
hyper_rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper-rustls."0.24.2" { inherit profileName; }).out;
@@ -996,7 +1132,7 @@ in
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
pin_utils = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-utils."0.1.0" { inherit profileName; }).out;
rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls."0.21.10" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
};
});
@@ -1016,10 +1152,10 @@ in
aws_smithy_async = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-async."1.1.7" { inherit profileName; }).out;
aws_smithy_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."1.1.7" { inherit profileName; }).out;
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
- http1 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.0.0" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
+ http1 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.1.0" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
zeroize = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".zeroize."1.7.0" { inherit profileName; }).out;
};
@@ -1040,16 +1176,16 @@ in
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
bytes_utils = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes-utils."0.1.4" { inherit profileName; }).out;
futures_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.30" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
http_body_0_4 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.6" { inherit profileName; }).out;
itoa = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.10" { inherit profileName; }).out;
num_integer = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-integer."0.1.45" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
pin_utils = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-utils."0.1.0" { inherit profileName; }).out;
- ryu = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".ryu."1.0.16" { inherit profileName; }).out;
+ ryu = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".ryu."1.0.17" { inherit profileName; }).out;
${ if false then "serde" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.195" { inherit profileName; }).out;
time = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".time."0.3.31" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
tokio_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.10" { inherit profileName; }).out;
};
});
@@ -1064,17 +1200,17 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.6" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".aws-types."1.1.7" = overridableMkRustCrate (profileName: rec {
name = "aws-types";
- version = "1.1.6";
+ version = "1.1.7";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "8fbb5d48aae496f628e7aa2e41991dd4074f606d9e3ade1ce1059f293d40f9a2"; };
+ src = fetchCratesIo { inherit name version; sha256 = "d07c63521aa1ea9a9f92a701f1a08ce3fd20b46c6efc0d5c8947c1fd879e3df1"; };
dependencies = {
- aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.6" { inherit profileName; }).out;
+ aws_credential_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-credential-types."1.1.7" { inherit profileName; }).out;
aws_smithy_async = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-async."1.1.7" { inherit profileName; }).out;
aws_smithy_runtime_api = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-runtime-api."1.1.7" { inherit profileName; }).out;
aws_smithy_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-smithy-types."1.1.7" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
};
buildDependencies = {
@@ -1082,72 +1218,21 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".axum."0.6.20" = overridableMkRustCrate (profileName: rec {
- name = "axum";
- version = "0.6.20";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"; };
- dependencies = {
- async_trait = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.77" { profileName = "__noProfile"; }).out;
- axum_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".axum-core."0.3.4" { inherit profileName; }).out;
- bitflags = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }).out;
- bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
- futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.30" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
- http_body = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.6" { inherit profileName; }).out;
- hyper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.28" { inherit profileName; }).out;
- itoa = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.10" { inherit profileName; }).out;
- matchit = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".matchit."0.7.3" { inherit profileName; }).out;
- memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.7.1" { inherit profileName; }).out;
- mime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".mime."0.3.17" { inherit profileName; }).out;
- percent_encoding = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.3.1" { inherit profileName; }).out;
- pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
- serde = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.195" { inherit profileName; }).out;
- sync_wrapper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".sync_wrapper."0.1.2" { inherit profileName; }).out;
- tower = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower."0.4.13" { inherit profileName; }).out;
- tower_layer = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-layer."0.3.2" { inherit profileName; }).out;
- tower_service = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.2" { inherit profileName; }).out;
- };
- buildDependencies = {
- rustversion = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".rustversion."1.0.14" { profileName = "__noProfile"; }).out;
- };
- });
-
- "registry+https://github.com/rust-lang/crates.io-index".axum-core."0.3.4" = overridableMkRustCrate (profileName: rec {
- name = "axum-core";
- version = "0.3.4";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"; };
- dependencies = {
- async_trait = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.77" { profileName = "__noProfile"; }).out;
- bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
- futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.30" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
- http_body = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.6" { inherit profileName; }).out;
- mime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".mime."0.3.17" { inherit profileName; }).out;
- tower_layer = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-layer."0.3.2" { inherit profileName; }).out;
- tower_service = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.2" { inherit profileName; }).out;
- };
- buildDependencies = {
- rustversion = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".rustversion."1.0.14" { profileName = "__noProfile"; }).out;
- };
- });
-
- "registry+https://github.com/rust-lang/crates.io-index".backtrace."0.3.69" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".backtrace."0.3.59" = overridableMkRustCrate (profileName: rec {
name = "backtrace";
- version = "0.3.69";
+ version = "0.3.59";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"; };
+ src = fetchCratesIo { inherit name version; sha256 = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744"; };
features = builtins.concatLists [
[ "default" ]
[ "std" ]
];
dependencies = {
- ${ if !(hostPlatform.isWindows && hostPlatform.parsed.abi.name == "msvc" && !(hostPlatform.parsed.vendor.name == "uwp")) then "addr2line" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".addr2line."0.21.0" { inherit profileName; }).out;
+ addr2line = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".addr2line."0.15.2" { inherit profileName; }).out;
cfg_if = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }).out;
- ${ if !(hostPlatform.isWindows && hostPlatform.parsed.abi.name == "msvc" && !(hostPlatform.parsed.vendor.name == "uwp")) then "libc" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.152" { inherit profileName; }).out;
- ${ if !(hostPlatform.isWindows && hostPlatform.parsed.abi.name == "msvc" && !(hostPlatform.parsed.vendor.name == "uwp")) then "miniz_oxide" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".miniz_oxide."0.7.1" { inherit profileName; }).out;
- ${ if !(hostPlatform.isWindows && hostPlatform.parsed.abi.name == "msvc" && !(hostPlatform.parsed.vendor.name == "uwp")) then "object" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".object."0.32.2" { inherit profileName; }).out;
+ libc = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.152" { inherit profileName; }).out;
+ miniz_oxide = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".miniz_oxide."0.4.4" { inherit profileName; }).out;
+ object = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".object."0.24.0" { inherit profileName; }).out;
rustc_demangle = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustc-demangle."0.1.23" { inherit profileName; }).out;
};
buildDependencies = {
@@ -1188,6 +1273,18 @@ in
];
});
+ "registry+https://github.com/rust-lang/crates.io-index".base64."0.22.1" = overridableMkRustCrate (profileName: rec {
+ name = "base64";
+ version = "0.22.1";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"; };
+ features = builtins.concatLists [
+ [ "alloc" ]
+ [ "default" ]
+ [ "std" ]
+ ];
+ });
+
"registry+https://github.com/rust-lang/crates.io-index".base64-simd."0.8.0" = overridableMkRustCrate (profileName: rec {
name = "base64-simd";
version = "0.8.0";
@@ -1292,7 +1389,7 @@ in
registry = "registry+https://github.com/rust-lang/crates.io-index";
src = fetchCratesIo { inherit name version; sha256 = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"; };
dependencies = {
- async_channel = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-channel."2.1.1" { inherit profileName; }).out;
+ async_channel = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-channel."2.2.0" { inherit profileName; }).out;
async_lock = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-lock."3.3.0" { inherit profileName; }).out;
async_task = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-task."4.7.0" { inherit profileName; }).out;
fastrand = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".fastrand."2.0.1" { inherit profileName; }).out;
@@ -1376,7 +1473,7 @@ in
];
dependencies = {
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
- either = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".either."1.9.0" { inherit profileName; }).out;
+ either = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".either."1.10.0" { inherit profileName; }).out;
};
});
@@ -1402,11 +1499,11 @@ in
src = fetchCratesIo { inherit name version; sha256 = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"; };
});
- "registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.31" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.38" = overridableMkRustCrate (profileName: rec {
name = "chrono";
- version = "0.4.31";
+ version = "0.4.38";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"; };
+ src = fetchCratesIo { inherit name version; sha256 = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"; };
features = builtins.concatLists [
[ "alloc" ]
[ "android-tzdata" ]
@@ -1414,6 +1511,7 @@ in
[ "default" ]
[ "iana-time-zone" ]
[ "js-sys" ]
+ [ "now" ]
[ "oldtime" ]
[ "std" ]
[ "wasm-bindgen" ]
@@ -1423,11 +1521,11 @@ in
];
dependencies = {
${ if hostPlatform.parsed.kernel.name == "android" then "android_tzdata" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".android-tzdata."0.1.1" { inherit profileName; }).out;
- ${ if hostPlatform.isUnix then "iana_time_zone" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".iana-time-zone."0.1.59" { inherit profileName; }).out;
+ ${ if hostPlatform.isUnix then "iana_time_zone" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".iana-time-zone."0.1.60" { inherit profileName; }).out;
${ if hostPlatform.parsed.cpu.name == "wasm32" && !(hostPlatform.parsed.kernel.name == "emscripten" || hostPlatform.parsed.kernel.name == "wasi") then "js_sys" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".js-sys."0.3.67" { inherit profileName; }).out;
num_traits = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-traits."0.2.17" { inherit profileName; }).out;
${ if hostPlatform.parsed.cpu.name == "wasm32" && !(hostPlatform.parsed.kernel.name == "emscripten" || hostPlatform.parsed.kernel.name == "wasi") then "wasm_bindgen" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-bindgen."0.2.90" { inherit profileName; }).out;
- ${ if hostPlatform.isWindows then "windows_targets" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows-targets."0.48.5" { inherit profileName; }).out;
+ ${ if hostPlatform.isWindows then "windows_targets" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows-targets."0.52.0" { inherit profileName; }).out;
};
});
@@ -1458,7 +1556,7 @@ in
once_cell = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.19.0" { inherit profileName; }).out;
strsim = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".strsim."0.10.0" { inherit profileName; }).out;
termcolor = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".termcolor."1.4.1" { inherit profileName; }).out;
- textwrap = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".textwrap."0.16.0" { inherit profileName; }).out;
+ textwrap = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".textwrap."0.16.1" { inherit profileName; }).out;
};
});
@@ -1503,52 +1601,6 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".console-api."0.6.0" = overridableMkRustCrate (profileName: rec {
- name = "console-api";
- version = "0.6.0";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787"; };
- features = builtins.concatLists [
- [ "transport" ]
- ];
- dependencies = {
- futures_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.30" { inherit profileName; }).out;
- prost = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".prost."0.12.3" { inherit profileName; }).out;
- prost_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".prost-types."0.12.3" { inherit profileName; }).out;
- tonic = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tonic."0.10.2" { inherit profileName; }).out;
- tracing_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.32" { inherit profileName; }).out;
- };
- });
-
- "registry+https://github.com/rust-lang/crates.io-index".console-subscriber."0.2.0" = overridableMkRustCrate (profileName: rec {
- name = "console-subscriber";
- version = "0.2.0";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e"; };
- features = builtins.concatLists [
- [ "default" ]
- [ "env-filter" ]
- ];
- dependencies = {
- console_api = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".console-api."0.6.0" { inherit profileName; }).out;
- crossbeam_channel = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crossbeam-channel."0.5.11" { inherit profileName; }).out;
- crossbeam_utils = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crossbeam-utils."0.8.19" { inherit profileName; }).out;
- futures_task = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-task."0.3.30" { inherit profileName; }).out;
- hdrhistogram = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hdrhistogram."7.5.4" { inherit profileName; }).out;
- humantime = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".humantime."2.1.0" { inherit profileName; }).out;
- prost_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".prost-types."0.12.3" { inherit profileName; }).out;
- serde = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.195" { inherit profileName; }).out;
- serde_json = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.111" { inherit profileName; }).out;
- thread_local = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".thread_local."1.1.7" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
- tokio_stream = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.14" { inherit profileName; }).out;
- tonic = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tonic."0.10.2" { inherit profileName; }).out;
- tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
- tracing_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.32" { inherit profileName; }).out;
- tracing_subscriber = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-subscriber."0.3.18" { inherit profileName; }).out;
- };
- });
-
"registry+https://github.com/rust-lang/crates.io-index".const-oid."0.9.6" = overridableMkRustCrate (profileName: rec {
name = "const-oid";
version = "0.9.6";
@@ -1602,11 +1654,11 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".crc32fast."1.3.2" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".crc32fast."1.4.0" = overridableMkRustCrate (profileName: rec {
name = "crc32fast";
- version = "1.3.2";
+ version = "1.4.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"; };
+ src = fetchCratesIo { inherit name version; sha256 = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"; };
features = builtins.concatLists [
[ "default" ]
[ "std" ]
@@ -1616,20 +1668,6 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".crossbeam-channel."0.5.11" = overridableMkRustCrate (profileName: rec {
- name = "crossbeam-channel";
- version = "0.5.11";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b"; };
- features = builtins.concatLists [
- [ "default" ]
- [ "std" ]
- ];
- dependencies = {
- crossbeam_utils = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crossbeam-utils."0.8.19" { inherit profileName; }).out;
- };
- });
-
"registry+https://github.com/rust-lang/crates.io-index".crossbeam-utils."0.8.19" = overridableMkRustCrate (profileName: rec {
name = "crossbeam-utils";
version = "0.8.19";
@@ -1754,6 +1792,18 @@ in
};
});
+ "registry+https://github.com/rust-lang/crates.io-index".derive_arbitrary."1.3.2" = overridableMkRustCrate (profileName: rec {
+ name = "derive_arbitrary";
+ version = "1.3.2";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"; };
+ dependencies = {
+ proc_macro2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.76" { inherit profileName; }).out;
+ quote = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.35" { inherit profileName; }).out;
+ syn = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."2.0.48" { inherit profileName; }).out;
+ };
+ });
+
"registry+https://github.com/rust-lang/crates.io-index".derive_utils."0.11.2" = overridableMkRustCrate (profileName: rec {
name = "derive_utils";
version = "0.11.2";
@@ -1849,14 +1899,11 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".either."1.9.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".either."1.10.0" = overridableMkRustCrate (profileName: rec {
name = "either";
- version = "1.9.0";
+ version = "1.10.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"; };
- features = builtins.concatLists [
- [ "use_std" ]
- ];
+ src = fetchCratesIo { inherit name version; sha256 = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"; };
});
"registry+https://github.com/rust-lang/crates.io-index".elliptic-curve."0.12.3" = overridableMkRustCrate (profileName: rec {
@@ -1898,7 +1945,7 @@ in
src = fetchCratesIo { inherit name version; sha256 = "d4499124d87abce26a57ef96ece800fa8babc38fbedd81c607c340ae83d46d2e"; };
dependencies = {
base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.21.7" { inherit profileName; }).out;
- chrono = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.31" { inherit profileName; }).out;
+ chrono = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.38" { inherit profileName; }).out;
encoding_rs = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".encoding_rs."0.8.33" { inherit profileName; }).out;
nom = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.3" { inherit profileName; }).out;
};
@@ -1979,6 +2026,22 @@ in
};
});
+ "registry+https://github.com/rust-lang/crates.io-index".event-listener."5.2.0" = overridableMkRustCrate (profileName: rec {
+ name = "event-listener";
+ version = "5.2.0";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91"; };
+ features = builtins.concatLists [
+ [ "parking" ]
+ [ "std" ]
+ ];
+ dependencies = {
+ concurrent_queue = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".concurrent-queue."2.4.0" { inherit profileName; }).out;
+ parking = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking."2.2.0" { inherit profileName; }).out;
+ pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
+ };
+ });
+
"registry+https://github.com/rust-lang/crates.io-index".event-listener-strategy."0.4.0" = overridableMkRustCrate (profileName: rec {
name = "event-listener-strategy";
version = "0.4.0";
@@ -1993,6 +2056,20 @@ in
};
});
+ "registry+https://github.com/rust-lang/crates.io-index".event-listener-strategy."0.5.0" = overridableMkRustCrate (profileName: rec {
+ name = "event-listener-strategy";
+ version = "0.5.0";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291"; };
+ features = builtins.concatLists [
+ [ "std" ]
+ ];
+ dependencies = {
+ event_listener = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".event-listener."5.2.0" { inherit profileName; }).out;
+ pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
+ };
+ });
+
"registry+https://github.com/rust-lang/crates.io-index".fastrand."1.9.0" = overridableMkRustCrate (profileName: rec {
name = "fastrand";
version = "1.9.0";
@@ -2026,23 +2103,6 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".flate2."1.0.28" = overridableMkRustCrate (profileName: rec {
- name = "flate2";
- version = "1.0.28";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"; };
- features = builtins.concatLists [
- [ "any_impl" ]
- [ "default" ]
- [ "miniz_oxide" ]
- [ "rust_backend" ]
- ];
- dependencies = {
- crc32fast = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".crc32fast."1.3.2" { inherit profileName; }).out;
- miniz_oxide = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".miniz_oxide."0.7.1" { inherit profileName; }).out;
- };
- });
-
"registry+https://github.com/rust-lang/crates.io-index".fnv."1.0.7" = overridableMkRustCrate (profileName: rec {
name = "fnv";
version = "1.0.7";
@@ -2054,6 +2114,23 @@ in
];
});
+ "registry+https://github.com/rust-lang/crates.io-index".foreign-types."0.3.2" = overridableMkRustCrate (profileName: rec {
+ name = "foreign-types";
+ version = "0.3.2";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"; };
+ dependencies = {
+ foreign_types_shared = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".foreign-types-shared."0.1.1" { inherit profileName; }).out;
+ };
+ });
+
+ "registry+https://github.com/rust-lang/crates.io-index".foreign-types-shared."0.1.1" = overridableMkRustCrate (profileName: rec {
+ name = "foreign-types-shared";
+ version = "0.1.1";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"; };
+ });
+
"registry+https://github.com/rust-lang/crates.io-index".form_urlencoded."1.2.1" = overridableMkRustCrate (profileName: rec {
name = "form_urlencoded";
version = "1.2.1";
@@ -2176,7 +2253,7 @@ in
fastrand = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".fastrand."1.9.0" { inherit profileName; }).out;
futures_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.30" { inherit profileName; }).out;
futures_io = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.30" { inherit profileName; }).out;
- memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.7.1" { inherit profileName; }).out;
+ memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.3.4" { inherit profileName; }).out;
parking = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".parking."2.2.0" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
waker_fn = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".waker-fn."1.1.1" { inherit profileName; }).out;
@@ -2270,7 +2347,7 @@ in
futures_macro = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-macro."0.3.30" { profileName = "__noProfile"; }).out;
futures_sink = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.30" { inherit profileName; }).out;
futures_task = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-task."0.3.30" { inherit profileName; }).out;
- memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.7.1" { inherit profileName; }).out;
+ memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.3.4" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
pin_utils = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-utils."0.1.0" { inherit profileName; }).out;
slab = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.9" { inherit profileName; }).out;
@@ -2299,23 +2376,27 @@ in
registry = "registry+https://github.com/rust-lang/crates.io-index";
src = fetchCratesIo { inherit name version; sha256 = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"; };
features = builtins.concatLists [
+ [ "js" ]
+ [ "js-sys" ]
[ "std" ]
+ [ "wasm-bindgen" ]
];
dependencies = {
cfg_if = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }).out;
+ ${ if (hostPlatform.parsed.cpu.name == "wasm32" || hostPlatform.parsed.cpu.name == "wasm64") && hostPlatform.parsed.kernel.name == "unknown" then "js_sys" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".js-sys."0.3.67" { inherit profileName; }).out;
${ if hostPlatform.isUnix then "libc" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.152" { inherit profileName; }).out;
${ if hostPlatform.parsed.kernel.name == "wasi" then "wasi" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasi."0.11.0+wasi-snapshot-preview1" { inherit profileName; }).out;
+ ${ if (hostPlatform.parsed.cpu.name == "wasm32" || hostPlatform.parsed.cpu.name == "wasm64") && hostPlatform.parsed.kernel.name == "unknown" then "wasm_bindgen" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-bindgen."0.2.90" { inherit profileName; }).out;
};
});
- "registry+https://github.com/rust-lang/crates.io-index".gimli."0.28.1" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".gimli."0.24.0" = overridableMkRustCrate (profileName: rec {
name = "gimli";
- version = "0.28.1";
+ version = "0.24.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"; };
+ src = fetchCratesIo { inherit name version; sha256 = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189"; };
features = builtins.concatLists [
[ "read" ]
- [ "read-core" ]
];
});
@@ -2361,10 +2442,10 @@ in
futures_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.30" { inherit profileName; }).out;
futures_sink = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.30" { inherit profileName; }).out;
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.30" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
- indexmap = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."2.1.0" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
+ indexmap = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."2.2.5" { inherit profileName; }).out;
slab = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.9" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
tokio_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.10" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
};
@@ -2381,10 +2462,10 @@ in
futures_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.30" { inherit profileName; }).out;
futures_sink = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.30" { inherit profileName; }).out;
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.30" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.0.0" { inherit profileName; }).out;
- indexmap = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."2.1.0" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.1.0" { inherit profileName; }).out;
+ indexmap = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."2.2.5" { inherit profileName; }).out;
slab = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.9" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
tokio_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.10" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
};
@@ -2410,26 +2491,6 @@ in
];
});
- "registry+https://github.com/rust-lang/crates.io-index".hdrhistogram."7.5.4" = overridableMkRustCrate (profileName: rec {
- name = "hdrhistogram";
- version = "7.5.4";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d"; };
- features = builtins.concatLists [
- [ "base64" ]
- [ "flate2" ]
- [ "nom" ]
- [ "serialization" ]
- ];
- dependencies = {
- base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.21.7" { inherit profileName; }).out;
- byteorder = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".byteorder."1.5.0" { inherit profileName; }).out;
- flate2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".flate2."1.0.28" { inherit profileName; }).out;
- nom = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.3" { inherit profileName; }).out;
- num_traits = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-traits."0.2.17" { inherit profileName; }).out;
- };
- });
-
"registry+https://github.com/rust-lang/crates.io-index".heck."0.4.1" = overridableMkRustCrate (profileName: rec {
name = "heck";
version = "0.4.1";
@@ -2488,11 +2549,11 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" = overridableMkRustCrate (profileName: rec {
name = "http";
- version = "0.2.11";
+ version = "0.2.12";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"; };
+ src = fetchCratesIo { inherit name version; sha256 = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"; };
dependencies = {
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
fnv = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".fnv."1.0.7" { inherit profileName; }).out;
@@ -2500,11 +2561,11 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".http."1.0.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".http."1.1.0" = overridableMkRustCrate (profileName: rec {
name = "http";
- version = "1.0.0";
+ version = "1.1.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea"; };
+ src = fetchCratesIo { inherit name version; sha256 = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"; };
features = builtins.concatLists [
[ "default" ]
[ "std" ]
@@ -2523,7 +2584,7 @@ in
src = fetchCratesIo { inherit name version; sha256 = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"; };
dependencies = {
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
};
});
@@ -2535,19 +2596,19 @@ in
src = fetchCratesIo { inherit name version; sha256 = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"; };
dependencies = {
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.0.0" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.1.0" { inherit profileName; }).out;
};
});
- "registry+https://github.com/rust-lang/crates.io-index".http-body-util."0.1.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".http-body-util."0.1.1" = overridableMkRustCrate (profileName: rec {
name = "http-body-util";
- version = "0.1.0";
+ version = "0.1.1";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840"; };
+ src = fetchCratesIo { inherit name version; sha256 = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"; };
dependencies = {
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
- futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.30" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.0.0" { inherit profileName; }).out;
+ futures_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.30" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.1.0" { inherit profileName; }).out;
http_body = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."1.0.0" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
};
@@ -2571,13 +2632,6 @@ in
src = fetchCratesIo { inherit name version; sha256 = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"; };
});
- "registry+https://github.com/rust-lang/crates.io-index".humantime."2.1.0" = overridableMkRustCrate (profileName: rec {
- name = "humantime";
- version = "2.1.0";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"; };
- });
-
"registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.28" = overridableMkRustCrate (profileName: rec {
name = "hyper";
version = "0.14.28";
@@ -2585,8 +2639,6 @@ in
src = fetchCratesIo { inherit name version; sha256 = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"; };
features = builtins.concatLists [
[ "client" ]
- [ "default" ]
- [ "full" ]
[ "h2" ]
[ "http1" ]
[ "http2" ]
@@ -2602,14 +2654,14 @@ in
futures_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.30" { inherit profileName; }).out;
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.30" { inherit profileName; }).out;
h2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.24" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
http_body = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.6" { inherit profileName; }).out;
httparse = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".httparse."1.8.0" { inherit profileName; }).out;
httpdate = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".httpdate."1.0.3" { inherit profileName; }).out;
itoa = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.10" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
socket2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.5.5" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
tower_service = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.2" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
want = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".want."0.3.1" { inherit profileName; }).out;
@@ -2633,14 +2685,14 @@ in
futures_channel = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-channel."0.3.30" { inherit profileName; }).out;
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.30" { inherit profileName; }).out;
h2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.4.2" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.0.0" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.1.0" { inherit profileName; }).out;
http_body = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."1.0.0" { inherit profileName; }).out;
httparse = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".httparse."1.8.0" { inherit profileName; }).out;
httpdate = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".httpdate."1.0.3" { inherit profileName; }).out;
itoa = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.10" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
smallvec = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".smallvec."1.13.1" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
want = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".want."0.3.1" { inherit profileName; }).out;
};
});
@@ -2664,12 +2716,12 @@ in
];
dependencies = {
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.30" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.12" { inherit profileName; }).out;
hyper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.28" { inherit profileName; }).out;
log = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.20" { inherit profileName; }).out;
rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls."0.21.10" { inherit profileName; }).out;
rustls_native_certs = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-native-certs."0.6.3" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
tokio_rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-rustls."0.24.1" { inherit profileName; }).out;
};
});
@@ -2692,29 +2744,33 @@ in
];
dependencies = {
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.30" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.0.0" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.1.0" { inherit profileName; }).out;
hyper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."1.2.0" { inherit profileName; }).out;
hyper_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper-util."0.1.3" { inherit profileName; }).out;
log = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.20" { inherit profileName; }).out;
rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls."0.22.2" { inherit profileName; }).out;
rustls_native_certs = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-native-certs."0.7.0" { inherit profileName; }).out;
- pki_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pki-types."1.1.0" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ pki_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pki-types."1.3.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
tokio_rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-rustls."0.25.0" { inherit profileName; }).out;
tower_service = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.2" { inherit profileName; }).out;
};
});
- "registry+https://github.com/rust-lang/crates.io-index".hyper-timeout."0.4.1" = overridableMkRustCrate (profileName: rec {
- name = "hyper-timeout";
- version = "0.4.1";
+ "registry+https://github.com/rust-lang/crates.io-index".hyper-tls."0.6.0" = overridableMkRustCrate (profileName: rec {
+ name = "hyper-tls";
+ version = "0.6.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"; };
+ src = fetchCratesIo { inherit name version; sha256 = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"; };
dependencies = {
- hyper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.28" { inherit profileName; }).out;
- pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
- tokio_io_timeout = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-io-timeout."1.2.0" { inherit profileName; }).out;
+ bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
+ http_body_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body-util."0.1.1" { inherit profileName; }).out;
+ hyper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."1.2.0" { inherit profileName; }).out;
+ hyper_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper-util."0.1.3" { inherit profileName; }).out;
+ native_tls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".native-tls."0.2.11" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
+ tokio_native_tls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-native-tls."0.3.1" { inherit profileName; }).out;
+ tower_service = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.2" { inherit profileName; }).out;
};
});
@@ -2739,23 +2795,23 @@ in
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
futures_channel = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-channel."0.3.30" { inherit profileName; }).out;
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.30" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.0.0" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.1.0" { inherit profileName; }).out;
http_body = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."1.0.0" { inherit profileName; }).out;
hyper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."1.2.0" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
socket2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.5.5" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
tower = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower."0.4.13" { inherit profileName; }).out;
tower_service = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.2" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
};
});
- "registry+https://github.com/rust-lang/crates.io-index".iana-time-zone."0.1.59" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".iana-time-zone."0.1.60" = overridableMkRustCrate (profileName: rec {
name = "iana-time-zone";
- version = "0.1.59";
+ version = "0.1.60";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539"; };
+ src = fetchCratesIo { inherit name version; sha256 = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"; };
features = builtins.concatLists [
[ "fallback" ]
];
@@ -2779,6 +2835,24 @@ in
};
});
+ "registry+https://github.com/rust-lang/crates.io-index".icalendar."0.16.1" = overridableMkRustCrate (profileName: rec {
+ name = "icalendar";
+ version = "0.16.1";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "cd83e81e8a329918d84e49032f8e596f4f079380942d172724cea3599a80807e"; };
+ features = builtins.concatLists [
+ [ "default" ]
+ [ "nom" ]
+ [ "parser" ]
+ ];
+ dependencies = {
+ chrono = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.38" { inherit profileName; }).out;
+ iso8601 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".iso8601."0.6.1" { inherit profileName; }).out;
+ nom = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.3" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") || hostPlatform.parsed.cpu.name == "wasm32" then "uuid" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".uuid."1.7.0" { inherit profileName; }).out;
+ };
+ });
+
"registry+https://github.com/rust-lang/crates.io-index".idna."0.2.3" = overridableMkRustCrate (profileName: rec {
name = "idna";
version = "0.2.3";
@@ -2846,7 +2920,7 @@ in
abnf_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".abnf-core."0.6.0" { inherit profileName; }).out;
base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.21.7" { inherit profileName; }).out;
bounded_static = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bounded-static."0.5.0" { inherit profileName; }).out;
- chrono = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.31" { inherit profileName; }).out;
+ chrono = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.38" { inherit profileName; }).out;
imap_types = (rustPackages."git+https://github.com/superboum/imap-codec".imap-types."2.0.0" { inherit profileName; }).out;
log = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.20" { inherit profileName; }).out;
nom = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.3" { inherit profileName; }).out;
@@ -2862,7 +2936,7 @@ in
url = https://github.com/duesee/imap-flow.git;
name = "imap-flow";
version = "0.1.0";
- rev = "68c1da5d1c56dbe543d9736de9683259d1d28191";
+ rev = "dce759a8531f317e8d7311fb032b366db6698e38";
ref = "main";};
dependencies = {
bounded_static = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bounded-static."0.5.0" { inherit profileName; }).out;
@@ -2870,7 +2944,7 @@ in
imap_codec = (rustPackages."git+https://github.com/superboum/imap-codec".imap-codec."2.0.0" { inherit profileName; }).out;
imap_types = (rustPackages."git+https://github.com/superboum/imap-codec".imap-types."2.0.0" { inherit profileName; }).out;
thiserror = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.56" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
};
});
@@ -2893,7 +2967,7 @@ in
dependencies = {
base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.21.7" { inherit profileName; }).out;
bounded_static = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bounded-static."0.5.0" { inherit profileName; }).out;
- chrono = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.31" { inherit profileName; }).out;
+ chrono = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.38" { inherit profileName; }).out;
thiserror = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.56" { inherit profileName; }).out;
};
});
@@ -2914,11 +2988,11 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".indexmap."2.1.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".indexmap."2.2.5" = overridableMkRustCrate (profileName: rec {
name = "indexmap";
- version = "2.1.0";
+ version = "2.2.5";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"; };
+ src = fetchCratesIo { inherit name version; sha256 = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"; };
features = builtins.concatLists [
[ "default" ]
[ "std" ]
@@ -2957,18 +3031,28 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".itertools."0.10.5" = overridableMkRustCrate (profileName: rec {
- name = "itertools";
- version = "0.10.5";
+ "registry+https://github.com/rust-lang/crates.io-index".ipnet."2.9.0" = overridableMkRustCrate (profileName: rec {
+ name = "ipnet";
+ version = "2.9.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"; };
+ src = fetchCratesIo { inherit name version; sha256 = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"; };
features = builtins.concatLists [
[ "default" ]
- [ "use_alloc" ]
- [ "use_std" ]
+ [ "std" ]
+ ];
+ });
+
+ "registry+https://github.com/rust-lang/crates.io-index".iso8601."0.6.1" = overridableMkRustCrate (profileName: rec {
+ name = "iso8601";
+ version = "0.6.1";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153"; };
+ features = builtins.concatLists [
+ [ "default" ]
+ [ "std" ]
];
dependencies = {
- either = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".either."1.9.0" { inherit profileName; }).out;
+ nom = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.3" { inherit profileName; }).out;
};
});
@@ -3010,22 +3094,22 @@ in
rev = "8b35a946d9f6b31b26b9783acbfab984316051f4";
ref = "k2v/shared_http_client";};
dependencies = {
- aws_sdk_config = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sdk-config."1.15.0" { inherit profileName; }).out;
- aws_sigv4 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sigv4."1.1.6" { inherit profileName; }).out;
+ aws_sdk_config = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sdk-config."1.16.0" { inherit profileName; }).out;
+ aws_sigv4 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aws-sigv4."1.1.7" { inherit profileName; }).out;
base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.21.7" { inherit profileName; }).out;
hex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hex."0.4.3" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.0.0" { inherit profileName; }).out;
- http_body_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body-util."0.1.0" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.1.0" { inherit profileName; }).out;
+ http_body_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body-util."0.1.1" { inherit profileName; }).out;
hyper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."1.2.0" { inherit profileName; }).out;
hyper_rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper-rustls."0.26.0" { inherit profileName; }).out;
hyper_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper-util."0.1.3" { inherit profileName; }).out;
log = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.20" { inherit profileName; }).out;
percent_encoding = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.3.1" { inherit profileName; }).out;
serde = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.195" { inherit profileName; }).out;
- serde_json = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.111" { inherit profileName; }).out;
+ serde_json = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.114" { inherit profileName; }).out;
sha2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".sha2."0.10.8" { inherit profileName; }).out;
thiserror = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.56" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
};
});
@@ -3085,7 +3169,7 @@ in
rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls."0.20.9" { inherit profileName; }).out;
rustls_native_certs = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-native-certs."0.6.3" { inherit profileName; }).out;
thiserror = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.56" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
tokio_rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-rustls."0.23.4" { inherit profileName; }).out;
tokio_stream = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.14" { inherit profileName; }).out;
tokio_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.10" { inherit profileName; }).out;
@@ -3112,7 +3196,7 @@ in
arrayvec = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".arrayvec."0.5.2" { inherit profileName; }).out;
bitflags = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }).out;
cfg_if = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }).out;
- ryu = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".ryu."1.0.16" { inherit profileName; }).out;
+ ryu = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".ryu."1.0.17" { inherit profileName; }).out;
static_assertions = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".static_assertions."1.1.0" { inherit profileName; }).out;
};
});
@@ -3129,6 +3213,25 @@ in
];
});
+ "registry+https://github.com/rust-lang/crates.io-index".libfuzzer-sys."0.4.7" = overridableMkRustCrate (profileName: rec {
+ name = "libfuzzer-sys";
+ version = "0.4.7";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"; };
+ features = builtins.concatLists [
+ [ "arbitrary-derive" ]
+ [ "default" ]
+ [ "link_libfuzzer" ]
+ ];
+ dependencies = {
+ arbitrary = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".arbitrary."1.3.2" { inherit profileName; }).out;
+ once_cell = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.19.0" { inherit profileName; }).out;
+ };
+ buildDependencies = {
+ cc = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cc."1.0.83" { profileName = "__noProfile"; }).out;
+ };
+ });
+
"registry+https://github.com/rust-lang/crates.io-index".libsodium-sys."0.2.7" = overridableMkRustCrate (profileName: rec {
name = "libsodium-sys";
version = "0.2.7";
@@ -3188,17 +3291,7 @@ in
[ "value-bag" ]
];
dependencies = {
- value_bag = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".value-bag."1.6.0" { inherit profileName; }).out;
- };
- });
-
- "registry+https://github.com/rust-lang/crates.io-index".matchers."0.1.0" = overridableMkRustCrate (profileName: rec {
- name = "matchers";
- version = "0.1.0";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"; };
- dependencies = {
- regex_automata = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-automata."0.1.10" { inherit profileName; }).out;
+ value_bag = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".value-bag."1.7.0" { inherit profileName; }).out;
};
});
@@ -3209,16 +3302,6 @@ in
src = fetchCratesIo { inherit name version; sha256 = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"; };
});
- "registry+https://github.com/rust-lang/crates.io-index".matchit."0.7.3" = overridableMkRustCrate (profileName: rec {
- name = "matchit";
- version = "0.7.3";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"; };
- features = builtins.concatLists [
- [ "default" ]
- ];
- });
-
"registry+https://github.com/rust-lang/crates.io-index".md-5."0.10.6" = overridableMkRustCrate (profileName: rec {
name = "md-5";
version = "0.10.6";
@@ -3234,13 +3317,12 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".memchr."2.7.1" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".memchr."2.3.4" = overridableMkRustCrate (profileName: rec {
name = "memchr";
- version = "2.7.1";
+ version = "2.3.4";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"; };
+ src = fetchCratesIo { inherit name version; sha256 = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"; };
features = builtins.concatLists [
- [ "alloc" ]
[ "default" ]
[ "std" ]
[ "use_std" ]
@@ -3264,17 +3346,17 @@ in
];
});
- "registry+https://github.com/rust-lang/crates.io-index".miniz_oxide."0.7.1" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".miniz_oxide."0.4.4" = overridableMkRustCrate (profileName: rec {
name = "miniz_oxide";
- version = "0.7.1";
+ version = "0.4.4";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"; };
- features = builtins.concatLists [
- [ "with-alloc" ]
- ];
+ src = fetchCratesIo { inherit name version; sha256 = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"; };
dependencies = {
adler = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".adler."1.0.2" { inherit profileName; }).out;
};
+ buildDependencies = {
+ autocfg = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".autocfg."1.1.0" { profileName = "__noProfile"; }).out;
+ };
});
"registry+https://github.com/rust-lang/crates.io-index".mio."0.8.10" = overridableMkRustCrate (profileName: rec {
@@ -3294,6 +3376,25 @@ in
};
});
+ "registry+https://github.com/rust-lang/crates.io-index".native-tls."0.2.11" = overridableMkRustCrate (profileName: rec {
+ name = "native-tls";
+ version = "0.2.11";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"; };
+ dependencies = {
+ ${ if hostPlatform.parsed.kernel.name == "darwin" || hostPlatform.parsed.kernel.name == "ios" then "lazy_static" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }).out;
+ ${ if hostPlatform.parsed.kernel.name == "darwin" || hostPlatform.parsed.kernel.name == "ios" then "libc" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.152" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.kernel.name == "windows" || hostPlatform.parsed.kernel.name == "darwin" || hostPlatform.parsed.kernel.name == "ios") then "log" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.20" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.kernel.name == "windows" || hostPlatform.parsed.kernel.name == "darwin" || hostPlatform.parsed.kernel.name == "ios") then "openssl" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl."0.10.64" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.kernel.name == "windows" || hostPlatform.parsed.kernel.name == "darwin" || hostPlatform.parsed.kernel.name == "ios") then "openssl_probe" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl-probe."0.1.5" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.kernel.name == "windows" || hostPlatform.parsed.kernel.name == "darwin" || hostPlatform.parsed.kernel.name == "ios") then "openssl_sys" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl-sys."0.9.102" { inherit profileName; }).out;
+ ${ if hostPlatform.parsed.kernel.name == "windows" then "schannel" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".schannel."0.1.23" { inherit profileName; }).out;
+ ${ if hostPlatform.parsed.kernel.name == "darwin" || hostPlatform.parsed.kernel.name == "ios" then "security_framework" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".security-framework."2.9.2" { inherit profileName; }).out;
+ ${ if hostPlatform.parsed.kernel.name == "darwin" || hostPlatform.parsed.kernel.name == "ios" then "security_framework_sys" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".security-framework-sys."2.9.1" { inherit profileName; }).out;
+ ${ if hostPlatform.parsed.kernel.name == "darwin" || hostPlatform.parsed.kernel.name == "ios" then "tempfile" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tempfile."3.10.1" { inherit profileName; }).out;
+ };
+ });
+
"registry+https://github.com/rust-lang/crates.io-index".nix."0.27.1" = overridableMkRustCrate (profileName: rec {
name = "nix";
version = "0.27.1";
@@ -3322,11 +3423,11 @@ in
];
});
- "registry+https://github.com/rust-lang/crates.io-index".nom."6.1.2" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".nom."6.2.2" = overridableMkRustCrate (profileName: rec {
name = "nom";
- version = "6.1.2";
+ version = "6.2.2";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"; };
+ src = fetchCratesIo { inherit name version; sha256 = "c6a7a9657c84d5814c6196b68bb4429df09c18b1573806259fba397ea4ad0d44"; };
features = builtins.concatLists [
[ "alloc" ]
[ "bitvec" ]
@@ -3340,7 +3441,7 @@ in
bitvec = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitvec."0.19.6" { inherit profileName; }).out;
funty = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".funty."1.1.0" { inherit profileName; }).out;
lexical_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".lexical-core."0.7.6" { inherit profileName; }).out;
- memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.7.1" { inherit profileName; }).out;
+ memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.3.4" { inherit profileName; }).out;
};
buildDependencies = {
version_check = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".version_check."0.9.4" { profileName = "__noProfile"; }).out;
@@ -3358,7 +3459,7 @@ in
[ "std" ]
];
dependencies = {
- memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.7.1" { inherit profileName; }).out;
+ memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.3.4" { inherit profileName; }).out;
minimal_lexical = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".minimal-lexical."0.2.1" { inherit profileName; }).out;
};
});
@@ -3436,11 +3537,11 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".object."0.32.2" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".object."0.24.0" = overridableMkRustCrate (profileName: rec {
name = "object";
- version = "0.32.2";
+ version = "0.24.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"; };
+ src = fetchCratesIo { inherit name version; sha256 = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170"; };
features = builtins.concatLists [
[ "archive" ]
[ "coff" ]
@@ -3450,9 +3551,6 @@ in
[ "read_core" ]
[ "unaligned" ]
];
- dependencies = {
- memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.7.1" { inherit profileName; }).out;
- };
});
"registry+https://github.com/rust-lang/crates.io-index".oid-registry."0.4.0" = overridableMkRustCrate (profileName: rec {
@@ -3491,6 +3589,37 @@ in
];
});
+ "registry+https://github.com/rust-lang/crates.io-index".openssl."0.10.64" = overridableMkRustCrate (profileName: rec {
+ name = "openssl";
+ version = "0.10.64";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"; };
+ features = builtins.concatLists [
+ [ "default" ]
+ ];
+ dependencies = {
+ bitflags = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."2.4.2" { inherit profileName; }).out;
+ cfg_if = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }).out;
+ foreign_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".foreign-types."0.3.2" { inherit profileName; }).out;
+ libc = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.152" { inherit profileName; }).out;
+ once_cell = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.19.0" { inherit profileName; }).out;
+ openssl_macros = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl-macros."0.1.1" { profileName = "__noProfile"; }).out;
+ ffi = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl-sys."0.9.102" { inherit profileName; }).out;
+ };
+ });
+
+ "registry+https://github.com/rust-lang/crates.io-index".openssl-macros."0.1.1" = overridableMkRustCrate (profileName: rec {
+ name = "openssl-macros";
+ version = "0.1.1";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"; };
+ dependencies = {
+ proc_macro2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.76" { inherit profileName; }).out;
+ quote = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.35" { inherit profileName; }).out;
+ syn = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."2.0.48" { inherit profileName; }).out;
+ };
+ });
+
"registry+https://github.com/rust-lang/crates.io-index".openssl-probe."0.1.5" = overridableMkRustCrate (profileName: rec {
name = "openssl-probe";
version = "0.1.5";
@@ -3498,6 +3627,21 @@ in
src = fetchCratesIo { inherit name version; sha256 = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"; };
});
+ "registry+https://github.com/rust-lang/crates.io-index".openssl-sys."0.9.102" = overridableMkRustCrate (profileName: rec {
+ name = "openssl-sys";
+ version = "0.9.102";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"; };
+ dependencies = {
+ libc = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.152" { inherit profileName; }).out;
+ };
+ buildDependencies = {
+ cc = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cc."1.0.83" { profileName = "__noProfile"; }).out;
+ pkg_config = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".pkg-config."0.3.29" { profileName = "__noProfile"; }).out;
+ vcpkg = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".vcpkg."0.2.15" { profileName = "__noProfile"; }).out;
+ };
+ });
+
"registry+https://github.com/rust-lang/crates.io-index".os_str_bytes."6.6.1" = overridableMkRustCrate (profileName: rec {
name = "os_str_bytes";
version = "6.6.1";
@@ -3588,21 +3732,21 @@ in
];
});
- "registry+https://github.com/rust-lang/crates.io-index".pin-project."1.1.3" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".pin-project."1.1.5" = overridableMkRustCrate (profileName: rec {
name = "pin-project";
- version = "1.1.3";
+ version = "1.1.5";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"; };
+ src = fetchCratesIo { inherit name version; sha256 = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"; };
dependencies = {
- pin_project_internal = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-internal."1.1.3" { profileName = "__noProfile"; }).out;
+ pin_project_internal = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-internal."1.1.5" { profileName = "__noProfile"; }).out;
};
});
- "registry+https://github.com/rust-lang/crates.io-index".pin-project-internal."1.1.3" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".pin-project-internal."1.1.5" = overridableMkRustCrate (profileName: rec {
name = "pin-project-internal";
- version = "1.1.3";
+ version = "1.1.5";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"; };
+ src = fetchCratesIo { inherit name version; sha256 = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"; };
dependencies = {
proc_macro2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.76" { inherit profileName; }).out;
quote = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.35" { inherit profileName; }).out;
@@ -3692,16 +3836,16 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".polling."3.3.2" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".polling."3.5.0" = overridableMkRustCrate (profileName: rec {
name = "polling";
- version = "3.3.2";
+ version = "3.5.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "545c980a3880efd47b2e262f6a4bb6daad6555cf3367aa9c4e52895f69537a41"; };
+ src = fetchCratesIo { inherit name version; sha256 = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9"; };
dependencies = {
cfg_if = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }).out;
${ if hostPlatform.isWindows then "concurrent_queue" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".concurrent-queue."2.4.0" { inherit profileName; }).out;
${ if hostPlatform.isWindows then "pin_project_lite" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
- ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "vxworks" then "rustix" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustix."0.38.30" { inherit profileName; }).out;
+ ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "fuchsia" || hostPlatform.parsed.kernel.name == "vxworks" then "rustix" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustix."0.38.31" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
${ if hostPlatform.isWindows then "windows_sys" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows-sys."0.52.0" { inherit profileName; }).out;
};
@@ -3774,47 +3918,19 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".prost."0.12.3" = overridableMkRustCrate (profileName: rec {
- name = "prost";
- version = "0.12.3";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a"; };
- features = builtins.concatLists [
- [ "default" ]
- [ "prost-derive" ]
- [ "std" ]
- ];
- dependencies = {
- bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
- prost_derive = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".prost-derive."0.12.3" { profileName = "__noProfile"; }).out;
- };
- });
-
- "registry+https://github.com/rust-lang/crates.io-index".prost-derive."0.12.3" = overridableMkRustCrate (profileName: rec {
- name = "prost-derive";
- version = "0.12.3";
+ "registry+https://github.com/rust-lang/crates.io-index".quick-xml."0.31.0" = overridableMkRustCrate (profileName: rec {
+ name = "quick-xml";
+ version = "0.31.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"; };
- dependencies = {
- anyhow = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".anyhow."1.0.79" { inherit profileName; }).out;
- itertools = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".itertools."0.10.5" { inherit profileName; }).out;
- proc_macro2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".proc-macro2."1.0.76" { inherit profileName; }).out;
- quote = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".quote."1.0.35" { inherit profileName; }).out;
- syn = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".syn."2.0.48" { inherit profileName; }).out;
- };
- });
-
- "registry+https://github.com/rust-lang/crates.io-index".prost-types."0.12.3" = overridableMkRustCrate (profileName: rec {
- name = "prost-types";
- version = "0.12.3";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e"; };
+ src = fetchCratesIo { inherit name version; sha256 = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"; };
features = builtins.concatLists [
+ [ "async-tokio" ]
[ "default" ]
- [ "std" ]
+ [ "tokio" ]
];
dependencies = {
- prost = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".prost."0.12.3" { inherit profileName; }).out;
+ memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.3.4" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
};
});
@@ -3850,7 +3966,6 @@ in
[ "getrandom" ]
[ "libc" ]
[ "rand_chacha" ]
- [ "small_rng" ]
[ "std" ]
[ "std_rng" ]
];
@@ -3900,24 +4015,6 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".regex."1.10.2" = overridableMkRustCrate (profileName: rec {
- name = "regex";
- version = "1.10.2";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"; };
- features = builtins.concatLists [
- [ "std" ]
- [ "unicode-case" ]
- [ "unicode-perl" ]
- ];
- dependencies = {
- aho_corasick = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aho-corasick."1.1.2" { inherit profileName; }).out;
- memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.7.1" { inherit profileName; }).out;
- regex_automata = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-automata."0.4.3" { inherit profileName; }).out;
- regex_syntax = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.8.2" { inherit profileName; }).out;
- };
- });
-
"registry+https://github.com/rust-lang/crates.io-index".regex-automata."0.1.10" = overridableMkRustCrate (profileName: rec {
name = "regex-automata";
version = "0.1.10";
@@ -3933,29 +4030,6 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".regex-automata."0.4.3" = overridableMkRustCrate (profileName: rec {
- name = "regex-automata";
- version = "0.4.3";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"; };
- features = builtins.concatLists [
- [ "alloc" ]
- [ "meta" ]
- [ "nfa-pikevm" ]
- [ "nfa-thompson" ]
- [ "std" ]
- [ "syntax" ]
- [ "unicode-case" ]
- [ "unicode-perl" ]
- [ "unicode-word-boundary" ]
- ];
- dependencies = {
- aho_corasick = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".aho-corasick."1.1.2" { inherit profileName; }).out;
- memchr = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".memchr."2.7.1" { inherit profileName; }).out;
- regex_syntax = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.8.2" { inherit profileName; }).out;
- };
- });
-
"registry+https://github.com/rust-lang/crates.io-index".regex-lite."0.1.5" = overridableMkRustCrate (profileName: rec {
name = "regex-lite";
version = "0.1.5";
@@ -3986,16 +4060,59 @@ in
];
});
- "registry+https://github.com/rust-lang/crates.io-index".regex-syntax."0.8.2" = overridableMkRustCrate (profileName: rec {
- name = "regex-syntax";
- version = "0.8.2";
+ "registry+https://github.com/rust-lang/crates.io-index".reqwest."0.12.4" = overridableMkRustCrate (profileName: rec {
+ name = "reqwest";
+ version = "0.12.4";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"; };
+ src = fetchCratesIo { inherit name version; sha256 = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"; };
features = builtins.concatLists [
- [ "std" ]
- [ "unicode-case" ]
- [ "unicode-perl" ]
+ [ "__tls" ]
+ [ "blocking" ]
+ [ "charset" ]
+ [ "default" ]
+ [ "default-tls" ]
+ [ "futures-channel" ]
+ [ "h2" ]
+ [ "http2" ]
+ [ "macos-system-configuration" ]
];
+ dependencies = {
+ base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.22.1" { inherit profileName; }).out;
+ bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "encoding_rs" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".encoding_rs."0.8.33" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "futures_channel" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-channel."0.3.30" { inherit profileName; }).out;
+ futures_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.30" { inherit profileName; }).out;
+ futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.30" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "h2" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.4.2" { inherit profileName; }).out;
+ http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."1.1.0" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "http_body" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."1.0.0" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "http_body_util" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body-util."0.1.1" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "hyper" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."1.2.0" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "hyper_tls" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper-tls."0.6.0" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "hyper_util" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper-util."0.1.3" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "ipnet" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".ipnet."2.9.0" { inherit profileName; }).out;
+ ${ if hostPlatform.parsed.cpu.name == "wasm32" then "js_sys" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".js-sys."0.3.67" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "log" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.20" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "mime" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".mime."0.3.17" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "native_tls_crate" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".native-tls."0.2.11" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "once_cell" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.19.0" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "percent_encoding" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.3.1" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "pin_project_lite" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "rustls_pemfile" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pemfile."2.1.1" { inherit profileName; }).out;
+ serde = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.195" { inherit profileName; }).out;
+ ${ if hostPlatform.parsed.cpu.name == "wasm32" then "serde_json" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.114" { inherit profileName; }).out;
+ serde_urlencoded = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_urlencoded."0.7.1" { inherit profileName; }).out;
+ sync_wrapper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".sync_wrapper."0.1.2" { inherit profileName; }).out;
+ ${ if hostPlatform.parsed.kernel.name == "darwin" then "system_configuration" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".system-configuration."0.5.1" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "tokio" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
+ ${ if !(hostPlatform.parsed.cpu.name == "wasm32") then "tokio_native_tls" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-native-tls."0.3.1" { inherit profileName; }).out;
+ tower_service = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.2" { inherit profileName; }).out;
+ url = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".url."2.5.0" { inherit profileName; }).out;
+ ${ if hostPlatform.parsed.cpu.name == "wasm32" then "wasm_bindgen" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-bindgen."0.2.90" { inherit profileName; }).out;
+ ${ if hostPlatform.parsed.cpu.name == "wasm32" then "wasm_bindgen_futures" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-bindgen-futures."0.4.40" { inherit profileName; }).out;
+ ${ if hostPlatform.parsed.cpu.name == "wasm32" then "web_sys" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".web-sys."0.3.67" { inherit profileName; }).out;
+ ${ if hostPlatform.isWindows then "winreg" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".winreg."0.52.0" { inherit profileName; }).out;
+ };
});
"registry+https://github.com/rust-lang/crates.io-index".rfc6979."0.3.1" = overridableMkRustCrate (profileName: rec {
@@ -4120,7 +4237,7 @@ in
registry = "registry+https://github.com/rust-lang/crates.io-index";
src = fetchCratesIo { inherit name version; sha256 = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"; };
dependencies = {
- semver = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".semver."1.0.21" { inherit profileName; }).out;
+ semver = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".semver."1.0.22" { inherit profileName; }).out;
};
});
@@ -4154,13 +4271,14 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".rustix."0.38.30" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".rustix."0.38.31" = overridableMkRustCrate (profileName: rec {
name = "rustix";
- version = "0.38.30";
+ version = "0.38.31";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca"; };
+ src = fetchCratesIo { inherit name version; sha256 = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"; };
features = builtins.concatLists [
[ "alloc" ]
+ [ "default" ]
[ "event" ]
[ "fs" ]
[ "net" ]
@@ -4168,6 +4286,7 @@ in
[ "process" ]
[ "std" ]
[ "time" ]
+ [ "use-libc-auxv" ]
];
dependencies = {
bitflags = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."2.4.2" { inherit profileName; }).out;
@@ -4232,8 +4351,8 @@ in
dependencies = {
log = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.20" { inherit profileName; }).out;
ring = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".ring."0.17.7" { inherit profileName; }).out;
- pki_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pki-types."1.1.0" { inherit profileName; }).out;
- webpki = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-webpki."0.102.1" { inherit profileName; }).out;
+ pki_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pki-types."1.3.1" { inherit profileName; }).out;
+ webpki = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-webpki."0.102.2" { inherit profileName; }).out;
subtle = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".subtle."2.5.0" { inherit profileName; }).out;
zeroize = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".zeroize."1.7.0" { inherit profileName; }).out;
};
@@ -4259,8 +4378,8 @@ in
src = fetchCratesIo { inherit name version; sha256 = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"; };
dependencies = {
${ if hostPlatform.isUnix && !(hostPlatform.parsed.kernel.name == "darwin") then "openssl_probe" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".openssl-probe."0.1.5" { inherit profileName; }).out;
- rustls_pemfile = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pemfile."2.0.0" { inherit profileName; }).out;
- pki_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pki-types."1.1.0" { inherit profileName; }).out;
+ rustls_pemfile = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pemfile."2.1.1" { inherit profileName; }).out;
+ pki_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pki-types."1.3.1" { inherit profileName; }).out;
${ if hostPlatform.isWindows then "schannel" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".schannel."0.1.23" { inherit profileName; }).out;
${ if hostPlatform.parsed.kernel.name == "darwin" then "security_framework" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".security-framework."2.9.2" { inherit profileName; }).out;
};
@@ -4276,26 +4395,26 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".rustls-pemfile."2.0.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".rustls-pemfile."2.1.1" = overridableMkRustCrate (profileName: rec {
name = "rustls-pemfile";
- version = "2.0.0";
+ version = "2.1.1";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "35e4980fa29e4c4b212ffb3db068a564cbf560e51d3944b7c88bd8bf5bec64f4"; };
+ src = fetchCratesIo { inherit name version; sha256 = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab"; };
features = builtins.concatLists [
[ "default" ]
[ "std" ]
];
dependencies = {
base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.21.7" { inherit profileName; }).out;
- pki_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pki-types."1.1.0" { inherit profileName; }).out;
+ pki_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pki-types."1.3.1" { inherit profileName; }).out;
};
});
- "registry+https://github.com/rust-lang/crates.io-index".rustls-pki-types."1.1.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".rustls-pki-types."1.3.1" = overridableMkRustCrate (profileName: rec {
name = "rustls-pki-types";
- version = "1.1.0";
+ version = "1.3.1";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "9e9d979b3ce68192e42760c7810125eb6cf2ea10efae545a156063e61f314e2a"; };
+ src = fetchCratesIo { inherit name version; sha256 = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8"; };
features = builtins.concatLists [
[ "alloc" ]
[ "default" ]
@@ -4319,11 +4438,11 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".rustls-webpki."0.102.1" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".rustls-webpki."0.102.2" = overridableMkRustCrate (profileName: rec {
name = "rustls-webpki";
- version = "0.102.1";
+ version = "0.102.2";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "ef4ca26037c909dedb327b48c3327d0ba91d3dd3c4e05dad328f210ffb68e95b"; };
+ src = fetchCratesIo { inherit name version; sha256 = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"; };
features = builtins.concatLists [
[ "alloc" ]
[ "ring" ]
@@ -4331,23 +4450,16 @@ in
];
dependencies = {
ring = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".ring."0.17.7" { inherit profileName; }).out;
- pki_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pki-types."1.1.0" { inherit profileName; }).out;
+ pki_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pki-types."1.3.1" { inherit profileName; }).out;
untrusted = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".untrusted."0.9.0" { inherit profileName; }).out;
};
});
- "registry+https://github.com/rust-lang/crates.io-index".rustversion."1.0.14" = overridableMkRustCrate (profileName: rec {
- name = "rustversion";
- version = "1.0.14";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"; };
- });
-
- "registry+https://github.com/rust-lang/crates.io-index".ryu."1.0.16" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".ryu."1.0.17" = overridableMkRustCrate (profileName: rec {
name = "ryu";
- version = "1.0.16";
+ version = "1.0.17";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"; };
+ src = fetchCratesIo { inherit name version; sha256 = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"; };
});
"registry+https://github.com/rust-lang/crates.io-index".same-file."1.0.6" = overridableMkRustCrate (profileName: rec {
@@ -4432,6 +4544,7 @@ in
src = fetchCratesIo { inherit name version; sha256 = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"; };
features = builtins.concatLists [
[ "OSX_10_9" ]
+ [ "default" ]
];
dependencies = {
core_foundation_sys = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".core-foundation-sys."0.8.6" { inherit profileName; }).out;
@@ -4439,11 +4552,11 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".semver."1.0.21" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".semver."1.0.22" = overridableMkRustCrate (profileName: rec {
name = "semver";
- version = "1.0.21";
+ version = "1.0.22";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"; };
+ src = fetchCratesIo { inherit name version; sha256 = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"; };
features = builtins.concatLists [
[ "default" ]
[ "std" ]
@@ -4483,18 +4596,31 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.111" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.114" = overridableMkRustCrate (profileName: rec {
name = "serde_json";
- version = "1.0.111";
+ version = "1.0.114";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"; };
+ src = fetchCratesIo { inherit name version; sha256 = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"; };
features = builtins.concatLists [
[ "default" ]
[ "std" ]
];
dependencies = {
itoa = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.10" { inherit profileName; }).out;
- ryu = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".ryu."1.0.16" { inherit profileName; }).out;
+ ryu = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".ryu."1.0.17" { inherit profileName; }).out;
+ serde = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.195" { inherit profileName; }).out;
+ };
+ });
+
+ "registry+https://github.com/rust-lang/crates.io-index".serde_urlencoded."0.7.1" = overridableMkRustCrate (profileName: rec {
+ name = "serde_urlencoded";
+ version = "0.7.1";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"; };
+ dependencies = {
+ form_urlencoded = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".form_urlencoded."1.2.1" { inherit profileName; }).out;
+ itoa = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.10" { inherit profileName; }).out;
+ ryu = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".ryu."1.0.17" { inherit profileName; }).out;
serde = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.195" { inherit profileName; }).out;
};
});
@@ -4646,8 +4772,8 @@ in
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.30" { inherit profileName; }).out;
idna = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".idna."0.2.3" { inherit profileName; }).out;
lazy_static = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }).out;
- nom = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."6.1.2" { inherit profileName; }).out;
- pin_project = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.1.3" { inherit profileName; }).out;
+ nom = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."6.2.2" { inherit profileName; }).out;
+ pin_project = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.1.5" { inherit profileName; }).out;
regex_automata = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex-automata."0.1.10" { inherit profileName; }).out;
serde = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.195" { inherit profileName; }).out;
};
@@ -4665,7 +4791,7 @@ in
ref = "feature/lmtp";};
dependencies = {
async_trait = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.77" { profileName = "__noProfile"; }).out;
- chrono = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.31" { inherit profileName; }).out;
+ chrono = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".chrono."0.4.38" { inherit profileName; }).out;
duplexify = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".duplexify."1.2.2" { inherit profileName; }).out;
futures = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures."0.3.30" { inherit profileName; }).out;
smol = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".smol."1.3.0" { inherit profileName; }).out;
@@ -4868,6 +4994,29 @@ in
};
});
+ "registry+https://github.com/rust-lang/crates.io-index".system-configuration."0.5.1" = overridableMkRustCrate (profileName: rec {
+ name = "system-configuration";
+ version = "0.5.1";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"; };
+ dependencies = {
+ bitflags = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitflags."1.3.2" { inherit profileName; }).out;
+ core_foundation = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".core-foundation."0.9.4" { inherit profileName; }).out;
+ system_configuration_sys = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".system-configuration-sys."0.5.0" { inherit profileName; }).out;
+ };
+ });
+
+ "registry+https://github.com/rust-lang/crates.io-index".system-configuration-sys."0.5.0" = overridableMkRustCrate (profileName: rec {
+ name = "system-configuration-sys";
+ version = "0.5.0";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"; };
+ dependencies = {
+ core_foundation_sys = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".core-foundation-sys."0.8.6" { inherit profileName; }).out;
+ libc = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.152" { inherit profileName; }).out;
+ };
+ });
+
"registry+https://github.com/rust-lang/crates.io-index".tap."1.0.1" = overridableMkRustCrate (profileName: rec {
name = "tap";
version = "1.0.1";
@@ -4875,6 +5024,19 @@ in
src = fetchCratesIo { inherit name version; sha256 = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"; };
});
+ "registry+https://github.com/rust-lang/crates.io-index".tempfile."3.10.1" = overridableMkRustCrate (profileName: rec {
+ name = "tempfile";
+ version = "3.10.1";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"; };
+ dependencies = {
+ cfg_if = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }).out;
+ fastrand = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".fastrand."2.0.1" { inherit profileName; }).out;
+ ${ if hostPlatform.isUnix || hostPlatform.parsed.kernel.name == "wasi" then "rustix" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustix."0.38.31" { inherit profileName; }).out;
+ ${ if hostPlatform.isWindows then "windows_sys" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows-sys."0.52.0" { inherit profileName; }).out;
+ };
+ });
+
"registry+https://github.com/rust-lang/crates.io-index".termcolor."1.4.1" = overridableMkRustCrate (profileName: rec {
name = "termcolor";
version = "1.4.1";
@@ -4885,11 +5047,11 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".textwrap."0.16.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".textwrap."0.16.1" = overridableMkRustCrate (profileName: rec {
name = "textwrap";
- version = "0.16.0";
+ version = "0.16.1";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"; };
+ src = fetchCratesIo { inherit name version; sha256 = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"; };
});
"registry+https://github.com/rust-lang/crates.io-index".thiserror."1.0.56" = overridableMkRustCrate (profileName: rec {
@@ -4914,11 +5076,11 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".thread_local."1.1.7" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".thread_local."1.1.8" = overridableMkRustCrate (profileName: rec {
name = "thread_local";
- version = "1.1.7";
+ version = "1.1.8";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"; };
+ src = fetchCratesIo { inherit name version; sha256 = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"; };
dependencies = {
cfg_if = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }).out;
once_cell = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.19.0" { inherit profileName; }).out;
@@ -4991,11 +5153,11 @@ in
src = fetchCratesIo { inherit name version; sha256 = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"; };
});
- "registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" = overridableMkRustCrate (profileName: rec {
name = "tokio";
- version = "1.35.1";
+ version = "1.36.0";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"; };
+ src = fetchCratesIo { inherit name version; sha256 = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"; };
features = builtins.concatLists [
[ "bytes" ]
[ "default" ]
@@ -5016,11 +5178,10 @@ in
[ "sync" ]
[ "time" ]
[ "tokio-macros" ]
- [ "tracing" ]
[ "windows-sys" ]
];
dependencies = {
- ${ if false then "backtrace" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".backtrace."0.3.69" { inherit profileName; }).out;
+ ${ if false then "backtrace" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".backtrace."0.3.59" { inherit profileName; }).out;
bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
${ if hostPlatform.isUnix then "libc" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.152" { inherit profileName; }).out;
mio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".mio."0.8.10" { inherit profileName; }).out;
@@ -5029,22 +5190,10 @@ in
${ if hostPlatform.isUnix then "signal_hook_registry" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".signal-hook-registry."1.4.1" { inherit profileName; }).out;
socket2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".socket2."0.5.5" { inherit profileName; }).out;
tokio_macros = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-macros."2.2.0" { profileName = "__noProfile"; }).out;
- ${ if false then "tracing" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
${ if hostPlatform.isWindows then "windows_sys" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows-sys."0.48.0" { inherit profileName; }).out;
};
});
- "registry+https://github.com/rust-lang/crates.io-index".tokio-io-timeout."1.2.0" = overridableMkRustCrate (profileName: rec {
- name = "tokio-io-timeout";
- version = "1.2.0";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"; };
- dependencies = {
- pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
- };
- });
-
"registry+https://github.com/rust-lang/crates.io-index".tokio-macros."2.2.0" = overridableMkRustCrate (profileName: rec {
name = "tokio-macros";
version = "2.2.0";
@@ -5057,6 +5206,17 @@ in
};
});
+ "registry+https://github.com/rust-lang/crates.io-index".tokio-native-tls."0.3.1" = overridableMkRustCrate (profileName: rec {
+ name = "tokio-native-tls";
+ version = "0.3.1";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"; };
+ dependencies = {
+ native_tls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".native-tls."0.2.11" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
+ };
+ });
+
"registry+https://github.com/rust-lang/crates.io-index".tokio-rustls."0.23.4" = overridableMkRustCrate (profileName: rec {
name = "tokio-rustls";
version = "0.23.4";
@@ -5070,7 +5230,7 @@ in
];
dependencies = {
rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls."0.20.9" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
webpki = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".webpki."0.22.4" { inherit profileName; }).out;
};
});
@@ -5086,7 +5246,7 @@ in
];
dependencies = {
rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls."0.21.10" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
};
});
@@ -5103,8 +5263,8 @@ in
];
dependencies = {
rustls = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls."0.22.2" { inherit profileName; }).out;
- pki_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pki-types."1.1.0" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ pki_types = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rustls-pki-types."1.3.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
};
});
@@ -5115,13 +5275,12 @@ in
src = fetchCratesIo { inherit name version; sha256 = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"; };
features = builtins.concatLists [
[ "default" ]
- [ "net" ]
[ "time" ]
];
dependencies = {
futures_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.30" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
};
});
@@ -5144,7 +5303,7 @@ in
futures_io = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-io."0.3.30" { inherit profileName; }).out;
futures_sink = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-sink."0.3.30" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
};
});
@@ -5162,41 +5321,6 @@ in
};
});
- "registry+https://github.com/rust-lang/crates.io-index".tonic."0.10.2" = overridableMkRustCrate (profileName: rec {
- name = "tonic";
- version = "0.10.2";
- registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e"; };
- features = builtins.concatLists [
- [ "channel" ]
- [ "codegen" ]
- [ "default" ]
- [ "prost" ]
- [ "transport" ]
- ];
- dependencies = {
- async_stream = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".async-stream."0.3.5" { inherit profileName; }).out;
- async_trait = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".async-trait."0.1.77" { profileName = "__noProfile"; }).out;
- axum = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".axum."0.6.20" { inherit profileName; }).out;
- base64 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".base64."0.21.7" { inherit profileName; }).out;
- bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytes."1.5.0" { inherit profileName; }).out;
- h2 = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".h2."0.3.24" { inherit profileName; }).out;
- http = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http."0.2.11" { inherit profileName; }).out;
- http_body = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".http-body."0.4.6" { inherit profileName; }).out;
- hyper = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper."0.14.28" { inherit profileName; }).out;
- hyper_timeout = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".hyper-timeout."0.4.1" { inherit profileName; }).out;
- percent_encoding = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.3.1" { inherit profileName; }).out;
- pin_project = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.1.3" { inherit profileName; }).out;
- prost = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".prost."0.12.3" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
- tokio_stream = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.14" { inherit profileName; }).out;
- tower = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower."0.4.13" { inherit profileName; }).out;
- tower_layer = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-layer."0.3.2" { inherit profileName; }).out;
- tower_service = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.2" { inherit profileName; }).out;
- tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
- };
- });
-
"registry+https://github.com/rust-lang/crates.io-index".tower."0.4.13" = overridableMkRustCrate (profileName: rec {
name = "tower";
version = "0.4.13";
@@ -5204,38 +5328,23 @@ in
src = fetchCratesIo { inherit name version; sha256 = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"; };
features = builtins.concatLists [
[ "__common" ]
- [ "balance" ]
- [ "buffer" ]
[ "default" ]
- [ "discover" ]
[ "futures-core" ]
[ "futures-util" ]
- [ "indexmap" ]
- [ "limit" ]
- [ "load" ]
[ "log" ]
[ "make" ]
[ "pin-project" ]
[ "pin-project-lite" ]
- [ "rand" ]
- [ "ready-cache" ]
- [ "slab" ]
- [ "timeout" ]
[ "tokio" ]
- [ "tokio-util" ]
[ "tracing" ]
[ "util" ]
];
dependencies = {
futures_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-core."0.3.30" { inherit profileName; }).out;
futures_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".futures-util."0.3.30" { inherit profileName; }).out;
- indexmap = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".indexmap."1.9.3" { inherit profileName; }).out;
- pin_project = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.1.3" { inherit profileName; }).out;
+ pin_project = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.1.5" { inherit profileName; }).out;
pin_project_lite = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project-lite."0.2.13" { inherit profileName; }).out;
- rand = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".rand."0.8.5" { inherit profileName; }).out;
- slab = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".slab."0.4.9" { inherit profileName; }).out;
- tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.35.1" { inherit profileName; }).out;
- tokio_util = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-util."0.7.10" { inherit profileName; }).out;
+ tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.36.0" { inherit profileName; }).out;
tower_layer = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-layer."0.3.2" { inherit profileName; }).out;
tower_service = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tower-service."0.3.2" { inherit profileName; }).out;
tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
@@ -5330,29 +5439,20 @@ in
[ "alloc" ]
[ "ansi" ]
[ "default" ]
- [ "env-filter" ]
[ "fmt" ]
- [ "matchers" ]
[ "nu-ansi-term" ]
- [ "once_cell" ]
- [ "regex" ]
[ "registry" ]
[ "sharded-slab" ]
[ "smallvec" ]
[ "std" ]
[ "thread_local" ]
- [ "tracing" ]
[ "tracing-log" ]
];
dependencies = {
- matchers = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".matchers."0.1.0" { inherit profileName; }).out;
nu_ansi_term = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nu-ansi-term."0.46.0" { inherit profileName; }).out;
- once_cell = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".once_cell."1.19.0" { inherit profileName; }).out;
- regex = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".regex."1.10.2" { inherit profileName; }).out;
sharded_slab = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".sharded-slab."0.1.7" { inherit profileName; }).out;
smallvec = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".smallvec."1.13.1" { inherit profileName; }).out;
- thread_local = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".thread_local."1.1.7" { inherit profileName; }).out;
- tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.40" { inherit profileName; }).out;
+ thread_local = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".thread_local."1.1.8" { inherit profileName; }).out;
tracing_core = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-core."0.1.32" { inherit profileName; }).out;
tracing_log = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing-log."0.2.0" { inherit profileName; }).out;
};
@@ -5458,8 +5558,15 @@ in
src = fetchCratesIo { inherit name version; sha256 = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"; };
features = builtins.concatLists [
[ "default" ]
+ [ "js" ]
+ [ "rng" ]
[ "std" ]
+ [ "v4" ]
];
+ dependencies = {
+ getrandom = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".getrandom."0.2.12" { inherit profileName; }).out;
+ wasm_bindgen = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".wasm-bindgen."0.2.90" { inherit profileName; }).out;
+ };
});
"registry+https://github.com/rust-lang/crates.io-index".valuable."0.1.0" = overridableMkRustCrate (profileName: rec {
@@ -5473,11 +5580,18 @@ in
];
});
- "registry+https://github.com/rust-lang/crates.io-index".value-bag."1.6.0" = overridableMkRustCrate (profileName: rec {
+ "registry+https://github.com/rust-lang/crates.io-index".value-bag."1.7.0" = overridableMkRustCrate (profileName: rec {
name = "value-bag";
- version = "1.6.0";
+ version = "1.7.0";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "126e423afe2dd9ac52142e7e9d5ce4135d7e13776c529d27fd6bc49f19e3280b"; };
+ });
+
+ "registry+https://github.com/rust-lang/crates.io-index".vcpkg."0.2.15" = overridableMkRustCrate (profileName: rec {
+ name = "vcpkg";
+ version = "0.2.15";
registry = "registry+https://github.com/rust-lang/crates.io-index";
- src = fetchCratesIo { inherit name version; sha256 = "7cdbaf5e132e593e9fc1de6a15bbec912395b11fb9719e061cf64f804524c503"; };
+ src = fetchCratesIo { inherit name version; sha256 = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"; };
});
"registry+https://github.com/rust-lang/crates.io-index".version_check."0.9.4" = overridableMkRustCrate (profileName: rec {
@@ -5630,12 +5744,27 @@ in
registry = "registry+https://github.com/rust-lang/crates.io-index";
src = fetchCratesIo { inherit name version; sha256 = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed"; };
features = builtins.concatLists [
+ [ "AbortController" ]
+ [ "AbortSignal" ]
+ [ "Blob" ]
+ [ "BlobPropertyBag" ]
[ "Crypto" ]
[ "Event" ]
[ "EventTarget" ]
+ [ "File" ]
+ [ "FormData" ]
+ [ "Headers" ]
[ "MessageEvent" ]
+ [ "ReadableStream" ]
+ [ "Request" ]
+ [ "RequestCredentials" ]
+ [ "RequestInit" ]
+ [ "RequestMode" ]
+ [ "Response" ]
+ [ "ServiceWorkerGlobalScope" ]
[ "Window" ]
[ "Worker" ]
+ [ "WorkerGlobalScope" ]
];
dependencies = {
js_sys = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".js-sys."0.3.67" { inherit profileName; }).out;
@@ -5742,11 +5871,15 @@ in
[ "Win32_Storage_FileSystem" ]
[ "Win32_System" ]
[ "Win32_System_Console" ]
+ [ "Win32_System_Diagnostics" ]
+ [ "Win32_System_Diagnostics_Debug" ]
[ "Win32_System_IO" ]
[ "Win32_System_LibraryLoader" ]
[ "Win32_System_Pipes" ]
+ [ "Win32_System_Registry" ]
[ "Win32_System_SystemServices" ]
[ "Win32_System_Threading" ]
+ [ "Win32_System_Time" ]
[ "Win32_System_WindowsProgramming" ]
[ "default" ]
];
@@ -5923,6 +6056,17 @@ in
src = fetchCratesIo { inherit name version; sha256 = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"; };
});
+ "registry+https://github.com/rust-lang/crates.io-index".winreg."0.52.0" = overridableMkRustCrate (profileName: rec {
+ name = "winreg";
+ version = "0.52.0";
+ registry = "registry+https://github.com/rust-lang/crates.io-index";
+ src = fetchCratesIo { inherit name version; sha256 = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"; };
+ dependencies = {
+ cfg_if = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }).out;
+ windows_sys = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".windows-sys."0.48.0" { inherit profileName; }).out;
+ };
+ });
+
"registry+https://github.com/rust-lang/crates.io-index".wyz."0.2.0" = overridableMkRustCrate (profileName: rec {
name = "wyz";
version = "0.2.0";
diff --git a/Cargo.toml b/Cargo.toml
index 7d2e032..91c6413 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,15 +1,33 @@
-[package]
-name = "aerogramme"
-version = "0.2.2"
-authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
-edition = "2021"
-license = "EUPL-1.2"
-description = "A robust email server"
+[workspace]
+resolver = "2"
+members = [
+ "aero-user",
+ "aero-bayou",
+ "aero-sasl",
+ "aero-dav",
+ "aero-dav/fuzz",
+ "aero-collections",
+ "aero-proto",
+ "aerogramme",
+]
+
+default-members = ["aerogramme"]
+
+[workspace.dependencies]
+# internal crates
+aero-user = { version = "0.3.0", path = "aero-user" }
+aero-bayou = { version = "0.3.0", path = "aero-bayou" }
+aero-sasl = { version = "0.3.0", path = "aero-sasl" }
+aero-dav = { version = "0.3.0", path = "aero-dav" }
+aero-ical = { version = "0.3.0", path = "aero-ical" }
+aero-collections = { version = "0.3.0", path = "aero-collections" }
+aero-proto = { version = "0.3.0", path = "aero-proto" }
+aerogramme = { version = "0.3.0", path = "aerogramme" }
-[dependencies]
# async runtime
-tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
+tokio = { version = "1.36", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
tokio-util = { version = "0.7", features = [ "compat" ] }
+tokio-stream = { version = "0.1" }
futures = "0.3"
# debug
@@ -18,6 +36,7 @@ backtrace = "0.3"
console-subscriber = "0.2"
tracing-subscriber = "0.3"
tracing = "0.1"
+thiserror = "1.0.56"
# language extensions
lazy_static = "1.4"
@@ -32,13 +51,32 @@ chrono = { version = "0.4", default-features = false, features = ["alloc"] }
nix = { version = "0.27", features = ["signal"] }
clap = { version = "3.1.18", features = ["derive", "env"] }
-# serialization & parsing
+# email protocols
+eml-codec = "0.1.2"
+smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
+smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
+imap-codec = { version = "2.0.0", features = ["bounded-static", "ext_condstore_qresync"] }
+imap-flow = { git = "https://github.com/duesee/imap-flow.git", branch = "main" }
+
+# dav protocols
+icalendar = "0.16"
+
+# http & web
+http = "1.1"
+http-body-util = "0.1.1"
+hyper = "1.2"
+hyper-rustls = { version = "0.26", features = ["http2"] }
+hyper-util = { version = "0.1", features = ["full"] }
+reqwest = { version = "0.12", features = [ "blocking" ]} # for testing purposes only
+
+# serialization, compression & parsing
serde = "1.0.137"
rmp-serde = "0.15"
toml = "0.5"
base64 = "0.21"
hex = "0.4"
nom = "7.1"
+quick-xml = { version = "0.31", features = ["async-tokio"] }
zstd = { version = "0.9", default-features = false }
# cryptography & security
@@ -48,8 +86,6 @@ rand = "0.8.5"
rustls = "0.22"
rustls-pemfile = "2.0"
tokio-rustls = "0.25"
-hyper-rustls = { version = "0.26", features = ["http2"] }
-hyper-util = { version = "0.1", features = ["full"] }
rpassword = "7.0"
# login
@@ -62,21 +98,6 @@ aws-sdk-s3 = "1"
aws-smithy-runtime = "1"
aws-smithy-runtime-api = "1"
-# email protocols
-eml-codec = "0.1.2"
-smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
-smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
-imap-codec = { version = "2.0.0", features = ["bounded-static", "ext_condstore_qresync"] }
-imap-flow = { git = "https://github.com/duesee/imap-flow.git", branch = "main" }
-thiserror = "1.0.56"
-
-[dev-dependencies]
-
[patch.crates-io]
imap-types = { git = "https://github.com/superboum/imap-codec", branch = "custom/aerogramme" }
imap-codec = { git = "https://github.com/superboum/imap-codec", branch = "custom/aerogramme" }
-
-[[test]]
-name = "behavior"
-path = "tests/behavior.rs"
-harness = false
diff --git a/README.md b/README.md
index b15330d..2778ffa 100644
--- a/README.md
+++ b/README.md
@@ -18,9 +18,9 @@ A resilient & standards-compliant open-source IMAP server with built-in encrypti
## Roadmap
- - ✅ 0.1 Better emails parsing (july '23, see [eml-codec](https://git.deuxfleurs.fr/Deuxfleurs/eml-codec)).
- - ✅ 0.2 Support of IMAP4. (~january '24).
- - ⌛0.3 CalDAV support. (~february '24).
+ - ✅ 0.1 Better emails parsing.
+ - ✅ 0.2 Support of IMAP4..
+ - ✅ 0.3 CalDAV support.
- ⌛0.4 CardDAV support.
- ⌛0.5 Public beta.
diff --git a/aero-bayou/Cargo.toml b/aero-bayou/Cargo.toml
new file mode 100644
index 0000000..cade216
--- /dev/null
+++ b/aero-bayou/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "aero-bayou"
+version = "0.3.0"
+authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
+edition = "2021"
+license = "EUPL-1.2"
+description = "A simplified version of Bayou by Terry et al. (ACM SIGOPS 1995)"
+
+[dependencies]
+aero-user.workspace = true
+
+anyhow.workspace = true
+hex.workspace = true
+tracing.workspace = true
+log.workspace = true
+rand.workspace = true
+serde.workspace = true
+tokio.workspace = true
+
diff --git a/src/bayou.rs b/aero-bayou/src/lib.rs
index 9faff5a..159dbbf 100644
--- a/src/bayou.rs
+++ b/aero-bayou/src/lib.rs
@@ -1,3 +1,5 @@
+pub mod timestamp;
+
use std::sync::{Arc, Weak};
use std::time::{Duration, Instant};
@@ -7,9 +9,10 @@ use rand::prelude::*;
use serde::{Deserialize, Serialize};
use tokio::sync::{watch, Notify};
-use crate::cryptoblob::*;
-use crate::login::Credentials;
-use crate::storage;
+use aero_user::cryptoblob::*;
+use aero_user::login::Credentials;
+use aero_user::storage;
+
use crate::timestamp::*;
const KEEP_STATE_EVERY: usize = 64;
diff --git a/src/timestamp.rs b/aero-bayou/src/timestamp.rs
index 76cb74b..4aa5399 100644
--- a/src/timestamp.rs
+++ b/aero-bayou/src/timestamp.rs
@@ -1,7 +1,8 @@
-use rand::prelude::*;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
+use rand::prelude::*;
+
/// Returns milliseconds since UNIX Epoch
pub fn now_msec() -> u64 {
SystemTime::now()
diff --git a/aero-collections/Cargo.toml b/aero-collections/Cargo.toml
new file mode 100644
index 0000000..95ab142
--- /dev/null
+++ b/aero-collections/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "aero-collections"
+version = "0.3.0"
+authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
+edition = "2021"
+license = "EUPL-1.2"
+description = "Aerogramme own representation of the different objects it manipulates"
+
+[dependencies]
+aero-user.workspace = true
+aero-bayou.workspace = true
+
+anyhow.workspace = true
+base64.workspace = true
+futures.workspace = true
+lazy_static.workspace = true
+serde.workspace = true
+hex.workspace = true
+tokio.workspace = true
+tracing.workspace = true
+rand.workspace = true
+im.workspace = true
+sodiumoxide.workspace = true
+eml-codec.workspace = true
+icalendar.workspace = true
diff --git a/aero-collections/src/calendar/mod.rs b/aero-collections/src/calendar/mod.rs
new file mode 100644
index 0000000..ac07842
--- /dev/null
+++ b/aero-collections/src/calendar/mod.rs
@@ -0,0 +1,204 @@
+pub mod namespace;
+
+use anyhow::{anyhow, bail, Result};
+use tokio::sync::RwLock;
+
+use aero_bayou::Bayou;
+use aero_user::cryptoblob::{self, gen_key, Key};
+use aero_user::login::Credentials;
+use aero_user::storage::{self, BlobRef, BlobVal, Store};
+
+use crate::davdag::{BlobId, DavDag, IndexEntry, SyncChange, Token};
+use crate::unique_ident::*;
+
+pub struct Calendar {
+ pub(super) id: UniqueIdent,
+ internal: RwLock<CalendarInternal>,
+}
+
+impl Calendar {
+ pub(crate) async fn open(creds: &Credentials, id: UniqueIdent) -> Result<Self> {
+ let bayou_path = format!("calendar/dag/{}", id);
+ let cal_path = format!("calendar/events/{}", id);
+
+ let mut davdag = Bayou::<DavDag>::new(creds, bayou_path).await?;
+ davdag.sync().await?;
+
+ let internal = RwLock::new(CalendarInternal {
+ id,
+ encryption_key: creds.keys.master.clone(),
+ storage: creds.storage.build().await?,
+ davdag,
+ cal_path,
+ });
+
+ Ok(Self { id, internal })
+ }
+
+ // ---- DAG sync utilities
+
+ /// Sync data with backing store
+ pub async fn force_sync(&self) -> Result<()> {
+ self.internal.write().await.force_sync().await
+ }
+
+ /// Sync data with backing store only if changes are detected
+ /// or last sync is too old
+ pub async fn opportunistic_sync(&self) -> Result<()> {
+ self.internal.write().await.opportunistic_sync().await
+ }
+
+ // ---- Data API
+
+ /// Access the DAG internal data (you can get the list of files for example)
+ pub async fn dag(&self) -> DavDag {
+ // Cloning is cheap
+ self.internal.read().await.davdag.state().clone()
+ }
+
+ /// Access the current token
+ pub async fn token(&self) -> Result<Token> {
+ self.internal.write().await.current_token().await
+ }
+
+ /// The diff API is a write API as we might need to push a merge node
+ /// to get a new sync token
+ pub async fn diff(&self, sync_token: Token) -> Result<(Token, Vec<SyncChange>)> {
+ self.internal.write().await.diff(sync_token).await
+ }
+
+ /// Get a specific event
+ pub async fn get(&self, evt_id: UniqueIdent) -> Result<Vec<u8>> {
+ self.internal.read().await.get(evt_id).await
+ }
+
+ /// Put a specific event
+ pub async fn put<'a>(&self, name: &str, evt: &'a [u8]) -> Result<(Token, IndexEntry)> {
+ self.internal.write().await.put(name, evt).await
+ }
+
+ /// Delete a specific event
+ pub async fn delete(&self, blob_id: UniqueIdent) -> Result<Token> {
+ self.internal.write().await.delete(blob_id).await
+ }
+}
+
+use base64::Engine;
+const MESSAGE_KEY: &str = "message-key";
+struct CalendarInternal {
+ #[allow(dead_code)]
+ id: UniqueIdent,
+ cal_path: String,
+ encryption_key: Key,
+ storage: Store,
+ davdag: Bayou<DavDag>,
+}
+
+impl CalendarInternal {
+ async fn force_sync(&mut self) -> Result<()> {
+ self.davdag.sync().await?;
+ Ok(())
+ }
+
+ async fn opportunistic_sync(&mut self) -> Result<()> {
+ self.davdag.opportunistic_sync().await?;
+ Ok(())
+ }
+
+ async fn get(&self, blob_id: BlobId) -> Result<Vec<u8>> {
+ // Fetch message from S3
+ let blob_ref = storage::BlobRef(format!("{}/{}", self.cal_path, blob_id));
+ let object = self.storage.blob_fetch(&blob_ref).await?;
+
+ // Decrypt message key from headers
+ let key_encrypted_b64 = object
+ .meta
+ .get(MESSAGE_KEY)
+ .ok_or(anyhow!("Missing key in metadata"))?;
+ let key_encrypted = base64::engine::general_purpose::STANDARD.decode(key_encrypted_b64)?;
+ let message_key_raw = cryptoblob::open(&key_encrypted, &self.encryption_key)?;
+ let message_key =
+ cryptoblob::Key::from_slice(&message_key_raw).ok_or(anyhow!("Invalid message key"))?;
+
+ // Decrypt body
+ let body = object.value;
+ cryptoblob::open(&body, &message_key)
+ }
+
+ async fn put<'a>(&mut self, name: &str, evt: &'a [u8]) -> Result<(Token, IndexEntry)> {
+ let message_key = gen_key();
+ let blob_id = gen_ident();
+
+ let encrypted_msg_key = cryptoblob::seal(&message_key.as_ref(), &self.encryption_key)?;
+ let key_header = base64::engine::general_purpose::STANDARD.encode(&encrypted_msg_key);
+
+ // Write event to S3
+ let message_blob = cryptoblob::seal(evt, &message_key)?;
+ let blob_val = BlobVal::new(
+ BlobRef(format!("{}/{}", self.cal_path, blob_id)),
+ message_blob,
+ )
+ .with_meta(MESSAGE_KEY.to_string(), key_header);
+
+ let etag = self.storage.blob_insert(blob_val).await?;
+
+ // Add entry to Bayou
+ let entry: IndexEntry = (blob_id, name.to_string(), etag);
+ let davstate = self.davdag.state();
+ let put_op = davstate.op_put(entry.clone());
+ let token = put_op.token();
+ self.davdag.push(put_op).await?;
+
+ Ok((token, entry))
+ }
+
+ async fn delete(&mut self, blob_id: BlobId) -> Result<Token> {
+ let davstate = self.davdag.state();
+
+ if !davstate.table.contains_key(&blob_id) {
+ bail!("Cannot delete event that doesn't exist");
+ }
+
+ let del_op = davstate.op_delete(blob_id);
+ let token = del_op.token();
+ self.davdag.push(del_op).await?;
+
+ let blob_ref = BlobRef(format!("{}/{}", self.cal_path, blob_id));
+ self.storage.blob_rm(&blob_ref).await?;
+
+ Ok(token)
+ }
+
+ async fn diff(&mut self, sync_token: Token) -> Result<(Token, Vec<SyncChange>)> {
+ let davstate = self.davdag.state();
+
+ let token_changed = davstate.resolve(sync_token)?;
+ let changes = token_changed
+ .iter()
+ .filter_map(|t: &Token| davstate.change.get(t))
+ .map(|s| s.clone())
+ .filter(|s| match s {
+ SyncChange::Ok((filename, _)) => davstate.idx_by_filename.get(filename).is_some(),
+ SyncChange::NotFound(filename) => davstate.idx_by_filename.get(filename).is_none(),
+ })
+ .collect();
+
+ let token = self.current_token().await?;
+ Ok((token, changes))
+ }
+
+ async fn current_token(&mut self) -> Result<Token> {
+ let davstate = self.davdag.state();
+ let heads = davstate.heads_vec();
+ let token = match heads.as_slice() {
+ [token] => *token,
+ _ => {
+ let op_mg = davstate.op_merge();
+ let token = op_mg.token();
+ self.davdag.push(op_mg).await?;
+ token
+ }
+ };
+ Ok(token)
+ }
+}
diff --git a/aero-collections/src/calendar/namespace.rs b/aero-collections/src/calendar/namespace.rs
new file mode 100644
index 0000000..db65703
--- /dev/null
+++ b/aero-collections/src/calendar/namespace.rs
@@ -0,0 +1,324 @@
+use anyhow::{bail, Result};
+use std::collections::{BTreeMap, HashMap};
+use std::sync::{Arc, Weak};
+
+use serde::{Deserialize, Serialize};
+
+use aero_bayou::timestamp::now_msec;
+use aero_user::cryptoblob::{open_deserialize, seal_serialize};
+use aero_user::storage;
+
+use super::Calendar;
+use crate::unique_ident::{gen_ident, UniqueIdent};
+use crate::user::User;
+
+pub(crate) const CAL_LIST_PK: &str = "calendars";
+pub(crate) const CAL_LIST_SK: &str = "list";
+pub(crate) const MAIN_CAL: &str = "Personal";
+pub(crate) const MAX_CALNAME_CHARS: usize = 32;
+
+pub struct CalendarNs(std::sync::Mutex<HashMap<UniqueIdent, Weak<Calendar>>>);
+
+impl CalendarNs {
+ /// Create a new calendar namespace
+ pub fn new() -> Self {
+ Self(std::sync::Mutex::new(HashMap::new()))
+ }
+
+ /// Open a calendar by name
+ pub async fn open(&self, user: &Arc<User>, name: &str) -> Result<Option<Arc<Calendar>>> {
+ let (list, _ct) = CalendarList::load(user).await?;
+
+ match list.get(name) {
+ None => Ok(None),
+ Some(ident) => Ok(Some(self.open_by_id(user, ident).await?)),
+ }
+ }
+
+ /// Open a calendar by unique id
+ /// Check user.rs::open_mailbox_by_id to understand this function
+ pub async fn open_by_id(&self, user: &Arc<User>, id: UniqueIdent) -> Result<Arc<Calendar>> {
+ {
+ let cache = self.0.lock().unwrap();
+ if let Some(cal) = cache.get(&id).and_then(Weak::upgrade) {
+ return Ok(cal);
+ }
+ }
+
+ let cal = Arc::new(Calendar::open(&user.creds, id).await?);
+
+ let mut cache = self.0.lock().unwrap();
+ if let Some(concurrent_cal) = cache.get(&id).and_then(Weak::upgrade) {
+ drop(cal); // we worked for nothing but at least we didn't starve someone else
+ Ok(concurrent_cal)
+ } else {
+ cache.insert(id, Arc::downgrade(&cal));
+ Ok(cal)
+ }
+ }
+
+ /// List calendars
+ pub async fn list(&self, user: &Arc<User>) -> Result<Vec<String>> {
+ CalendarList::load(user).await.map(|(list, _)| list.names())
+ }
+
+ /// Delete a calendar from the index
+ pub async fn delete(&self, user: &Arc<User>, name: &str) -> Result<()> {
+ // We currently assume that main cal is a bit specific
+ if name == MAIN_CAL {
+ bail!("Cannot delete main calendar");
+ }
+
+ let (mut list, ct) = CalendarList::load(user).await?;
+ if list.has(name) {
+ //@TODO: actually delete calendar content
+ list.bind(name, None);
+ list.save(user, ct).await?;
+ Ok(())
+ } else {
+ bail!("Calendar {} does not exist", name);
+ }
+ }
+
+ /// Rename a calendar in the index
+ pub async fn rename(&self, user: &Arc<User>, old: &str, new: &str) -> Result<()> {
+ if old == MAIN_CAL {
+ bail!("Renaming main calendar is not supported currently");
+ }
+ if !new.chars().all(char::is_alphanumeric) {
+ bail!("Unsupported characters in new calendar name, only alphanumeric characters are allowed currently");
+ }
+ if new.len() > MAX_CALNAME_CHARS {
+ bail!("Calendar name can't contain more than 32 characters");
+ }
+
+ let (mut list, ct) = CalendarList::load(user).await?;
+ list.rename(old, new)?;
+ list.save(user, ct).await?;
+
+ Ok(())
+ }
+
+ /// Create calendar
+ pub async fn create(&self, user: &Arc<User>, name: &str) -> Result<()> {
+ if name == MAIN_CAL {
+ bail!("Main calendar is automatically created, can't create it manually");
+ }
+ if !name.chars().all(char::is_alphanumeric) {
+ bail!("Unsupported characters in new calendar name, only alphanumeric characters are allowed");
+ }
+ if name.len() > MAX_CALNAME_CHARS {
+ bail!("Calendar name can't contain more than 32 characters");
+ }
+
+ let (mut list, ct) = CalendarList::load(user).await?;
+ match list.create(name) {
+ CalendarExists::Existed(_) => bail!("Calendar {} already exists", name),
+ CalendarExists::Created(_) => (),
+ }
+ list.save(user, ct).await?;
+
+ Ok(())
+ }
+
+ /// Has calendar
+ pub async fn has(&self, user: &Arc<User>, name: &str) -> Result<bool> {
+ CalendarList::load(user)
+ .await
+ .map(|(list, _)| list.has(name))
+ }
+}
+
+// ------
+// ------ From this point, implementation is hidden from the rest of the crate
+// ------
+
+#[derive(Serialize, Deserialize)]
+struct CalendarList(BTreeMap<String, CalendarListEntry>);
+
+#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
+struct CalendarListEntry {
+ id_lww: (u64, Option<UniqueIdent>),
+}
+
+impl CalendarList {
+ // ---- Index persistence related functions
+
+ /// Load from storage
+ async fn load(user: &Arc<User>) -> Result<(Self, Option<storage::RowRef>)> {
+ let row_ref = storage::RowRef::new(CAL_LIST_PK, CAL_LIST_SK);
+ let (mut list, row) = match user
+ .storage
+ .row_fetch(&storage::Selector::Single(&row_ref))
+ .await
+ {
+ Err(storage::StorageError::NotFound) => (Self::new(), None),
+ Err(e) => return Err(e.into()),
+ Ok(rv) => {
+ let mut list = Self::new();
+ let (row_ref, row_vals) = match rv.into_iter().next() {
+ Some(row_val) => (row_val.row_ref, row_val.value),
+ None => (row_ref, vec![]),
+ };
+
+ for v in row_vals {
+ if let storage::Alternative::Value(vbytes) = v {
+ let list2 =
+ open_deserialize::<CalendarList>(&vbytes, &user.creds.keys.master)?;
+ list.merge(list2);
+ }
+ }
+ (list, Some(row_ref))
+ }
+ };
+
+ // Create default calendars (currently only one calendar is created)
+ let is_default_cal_missing = [MAIN_CAL]
+ .iter()
+ .map(|calname| list.create(calname))
+ .fold(false, |acc, r| {
+ acc || matches!(r, CalendarExists::Created(..))
+ });
+
+ // Save the index if we created a new calendar
+ if is_default_cal_missing {
+ list.save(user, row.clone()).await?;
+ }
+
+ Ok((list, row))
+ }
+
+ /// Save an updated index
+ async fn save(&self, user: &Arc<User>, ct: Option<storage::RowRef>) -> Result<()> {
+ let list_blob = seal_serialize(self, &user.creds.keys.master)?;
+ let rref = ct.unwrap_or(storage::RowRef::new(CAL_LIST_PK, CAL_LIST_SK));
+ let row_val = storage::RowVal::new(rref, list_blob);
+ user.storage.row_insert(vec![row_val]).await?;
+ Ok(())
+ }
+
+ // ----- Index manipulation functions
+
+ /// Ensure that a given calendar exists
+ /// (Don't forget to save if it returns CalendarExists::Created)
+ fn create(&mut self, name: &str) -> CalendarExists {
+ if let Some(CalendarListEntry {
+ id_lww: (_, Some(id)),
+ }) = self.0.get(name)
+ {
+ return CalendarExists::Existed(*id);
+ }
+
+ let id = gen_ident();
+ self.bind(name, Some(id)).unwrap();
+ CalendarExists::Created(id)
+ }
+
+ /// Get a list of all calendar names
+ fn names(&self) -> Vec<String> {
+ self.0
+ .iter()
+ .filter(|(_, v)| v.id_lww.1.is_some())
+ .map(|(k, _)| k.to_string())
+ .collect()
+ }
+
+ /// For a given calendar name, get its Unique Identifier
+ fn get(&self, name: &str) -> Option<UniqueIdent> {
+ self.0
+ .get(name)
+ .map(|CalendarListEntry { id_lww: (_, ident) }| *ident)
+ .flatten()
+ }
+
+ /// Check if a given calendar name exists
+ fn has(&self, name: &str) -> bool {
+ self.get(name).is_some()
+ }
+
+ /// Rename a calendar
+ fn rename(&mut self, old: &str, new: &str) -> Result<()> {
+ if self.has(new) {
+ bail!("Calendar {} already exists", new);
+ }
+ let ident = match self.get(old) {
+ None => bail!("Calendar {} does not exist", old),
+ Some(ident) => ident,
+ };
+
+ self.bind(old, None);
+ self.bind(new, Some(ident));
+
+ Ok(())
+ }
+
+ // ----- Internal logic
+
+ /// New is not publicly exposed, use `load` instead
+ fn new() -> Self {
+ Self(BTreeMap::new())
+ }
+
+ /// Low level index updating logic (used to add/rename/delete) an entry
+ fn bind(&mut self, name: &str, id: Option<UniqueIdent>) -> Option<()> {
+ let (ts, id) = match self.0.get_mut(name) {
+ None => {
+ if id.is_none() {
+ // User wants to delete entry with given name (passed id is None)
+ // Entry does not exist (get_mut is None)
+ // Nothing to do
+ return None;
+ } else {
+ // User wants entry with given name to be present (id is Some)
+ // Entry does not exist
+ // Initialize entry
+ (now_msec(), id)
+ }
+ }
+ Some(CalendarListEntry { id_lww }) => {
+ if id_lww.1 == id {
+ // Entry is already equals to the requested id (Option<UniqueIdent)
+ // Nothing to do
+ return None;
+ } else {
+ // Entry does not equal to what we know internally
+ // We update the Last Write Win CRDT here with the new id value
+ (std::cmp::max(id_lww.0 + 1, now_msec()), id)
+ }
+ }
+ };
+
+ // If we did not return here, that's because we have to update
+ // something in our internal index.
+ self.0
+ .insert(name.into(), CalendarListEntry { id_lww: (ts, id) });
+ Some(())
+ }
+
+ // Merge 2 calendar lists by applying a LWW logic on each element
+ fn merge(&mut self, list2: Self) {
+ for (k, v) in list2.0.into_iter() {
+ if let Some(e) = self.0.get_mut(&k) {
+ e.merge(&v);
+ } else {
+ self.0.insert(k, v);
+ }
+ }
+ }
+}
+
+impl CalendarListEntry {
+ fn merge(&mut self, other: &Self) {
+ // Simple CRDT merge rule
+ if other.id_lww.0 > self.id_lww.0
+ || (other.id_lww.0 == self.id_lww.0 && other.id_lww.1 > self.id_lww.1)
+ {
+ self.id_lww = other.id_lww;
+ }
+ }
+}
+
+pub(crate) enum CalendarExists {
+ Created(UniqueIdent),
+ Existed(UniqueIdent),
+}
diff --git a/aero-collections/src/davdag.rs b/aero-collections/src/davdag.rs
new file mode 100644
index 0000000..74e745f
--- /dev/null
+++ b/aero-collections/src/davdag.rs
@@ -0,0 +1,342 @@
+use anyhow::{bail, Result};
+use im::{ordset, OrdMap, OrdSet};
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+use aero_bayou::*;
+
+use crate::unique_ident::{gen_ident, UniqueIdent};
+
+/// Parents are only persisted in the event log,
+/// not in the checkpoints.
+pub type Token = UniqueIdent;
+pub type Parents = Vec<Token>;
+pub type SyncDesc = (Parents, Token);
+
+pub type BlobId = UniqueIdent;
+pub type Etag = String;
+pub type FileName = String;
+pub type IndexEntry = (BlobId, FileName, Etag);
+
+#[derive(Clone, Default)]
+pub struct DavDag {
+ /// Source of trust
+ pub table: OrdMap<BlobId, IndexEntry>,
+
+ /// Indexes optimized for queries
+ pub idx_by_filename: OrdMap<FileName, BlobId>,
+
+ // ------------ Below this line, data is ephemeral, ie. not checkpointed
+ /// Partial synchronization graph
+ pub ancestors: OrdMap<Token, OrdSet<Token>>,
+
+ /// All nodes
+ pub all_nodes: OrdSet<Token>,
+ /// Head nodes
+ pub heads: OrdSet<Token>,
+ /// Origin nodes
+ pub origins: OrdSet<Token>,
+
+ /// File change token by token
+ pub change: OrdMap<Token, SyncChange>,
+}
+
+#[derive(Clone, Debug)]
+pub enum SyncChange {
+ Ok((FileName, BlobId)),
+ NotFound(FileName),
+}
+
+#[derive(Clone, Serialize, Deserialize, Debug)]
+pub enum DavDagOp {
+ /// Merge is a virtual operation run when multiple heads are discovered
+ Merge(SyncDesc),
+
+ /// Add an item to the collection
+ Put(SyncDesc, IndexEntry),
+
+ /// Delete an item from the collection
+ Delete(SyncDesc, BlobId),
+}
+impl DavDagOp {
+ pub fn token(&self) -> Token {
+ match self {
+ Self::Merge((_, t)) => *t,
+ Self::Put((_, t), _) => *t,
+ Self::Delete((_, t), _) => *t,
+ }
+ }
+}
+
+impl DavDag {
+ pub fn op_merge(&self) -> DavDagOp {
+ DavDagOp::Merge(self.sync_desc())
+ }
+
+ pub fn op_put(&self, entry: IndexEntry) -> DavDagOp {
+ DavDagOp::Put(self.sync_desc(), entry)
+ }
+
+ pub fn op_delete(&self, blob_id: BlobId) -> DavDagOp {
+ DavDagOp::Delete(self.sync_desc(), blob_id)
+ }
+
+ // HELPER functions
+
+ pub fn heads_vec(&self) -> Vec<Token> {
+ self.heads.clone().into_iter().collect()
+ }
+
+ /// A sync descriptor
+ pub fn sync_desc(&self) -> SyncDesc {
+ (self.heads_vec(), gen_ident())
+ }
+
+ /// Resolve a sync token
+ pub fn resolve(&self, known: Token) -> Result<OrdSet<Token>> {
+ let already_known = self.all_ancestors(known);
+
+ // We can't capture all missing events if we are not connected
+ // to all sinks of the graph,
+ // ie. if we don't already know all the sinks,
+ // ie. if we are missing so much history that
+ // the event log has been transformed into a checkpoint
+ if !self.origins.is_subset(already_known.clone()) {
+ bail!("Not enough history to produce a correct diff, a full resync is needed");
+ }
+
+ // Missing items are *all existing graph items* from which
+ // we removed *all items known by the given node*.
+ // In other words, all values in `all_nodes` that are not in `already_known`.
+ Ok(self.all_nodes.clone().relative_complement(already_known))
+ }
+
+ /// Find all ancestors of a given node
+ fn all_ancestors(&self, known: Token) -> OrdSet<Token> {
+ let mut all_known: OrdSet<UniqueIdent> = OrdSet::new();
+ let mut to_collect = vec![known];
+ loop {
+ let cursor = match to_collect.pop() {
+ // Loop stops here
+ None => break,
+ Some(v) => v,
+ };
+
+ if all_known.insert(cursor).is_some() {
+ // Item already processed
+ continue;
+ }
+
+ // Collect parents
+ let parents = match self.ancestors.get(&cursor) {
+ None => continue,
+ Some(c) => c,
+ };
+ to_collect.extend(parents.iter());
+ }
+ all_known
+ }
+
+ // INTERNAL functions
+
+ /// Register a WebDAV item (put, copy, move)
+ fn register(&mut self, sync_token: Option<Token>, entry: IndexEntry) {
+ let (blob_id, filename, _etag) = entry.clone();
+
+ // Insert item in the source of trust
+ self.table.insert(blob_id, entry);
+
+ // Update the cache
+ self.idx_by_filename.insert(filename.to_string(), blob_id);
+
+ // Record the change in the ephemeral synchronization map
+ if let Some(sync_token) = sync_token {
+ self.change
+ .insert(sync_token, SyncChange::Ok((filename, blob_id)));
+ }
+ }
+
+ /// Unregister a WebDAV item (delete, move)
+ fn unregister(&mut self, sync_token: Token, blob_id: &BlobId) {
+ // Query the source of truth to get the information we
+ // need to clean the indexes
+ let (_blob_id, filename, _etag) = match self.table.get(blob_id) {
+ Some(v) => v,
+ // Element does not exist, return early
+ None => return,
+ };
+ self.idx_by_filename.remove(filename);
+
+ // Record the change in the ephemeral synchronization map
+ self.change
+ .insert(sync_token, SyncChange::NotFound(filename.to_string()));
+
+ // Finally clear item from the source of trust
+ self.table.remove(blob_id);
+ }
+
+ /// When an event is processed, update the synchronization DAG
+ fn sync_dag(&mut self, sync_desc: &SyncDesc) {
+ let (parents, child) = sync_desc;
+
+ // --- Update ANCESTORS
+ // We register ancestors as it is required for the sync algorithm
+ self.ancestors.insert(
+ *child,
+ parents.iter().fold(ordset![], |mut acc, p| {
+ acc.insert(*p);
+ acc
+ }),
+ );
+
+ // --- Update ORIGINS
+ // If this event has no parents, it's an origin
+ if parents.is_empty() {
+ self.origins.insert(*child);
+ }
+
+ // --- Update HEADS
+ // Remove from HEADS this event's parents
+ parents.iter().for_each(|par| {
+ self.heads.remove(par);
+ });
+
+ // This event becomes a new HEAD in turn
+ self.heads.insert(*child);
+
+ // --- Update ALL NODES
+ self.all_nodes.insert(*child);
+ }
+}
+
+impl std::fmt::Debug for DavDag {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str("DavDag\n")?;
+ for elem in self.table.iter() {
+ f.write_fmt(format_args!("\t{:?} => {:?}", elem.0, elem.1))?;
+ }
+ Ok(())
+ }
+}
+
+impl BayouState for DavDag {
+ type Op = DavDagOp;
+
+ fn apply(&self, op: &Self::Op) -> Self {
+ let mut new = self.clone();
+
+ match op {
+ DavDagOp::Put(sync_desc, entry) => {
+ new.sync_dag(sync_desc);
+ new.register(Some(sync_desc.1), entry.clone());
+ }
+ DavDagOp::Delete(sync_desc, blob_id) => {
+ new.sync_dag(sync_desc);
+ new.unregister(sync_desc.1, blob_id);
+ }
+ DavDagOp::Merge(sync_desc) => {
+ new.sync_dag(sync_desc);
+ }
+ }
+
+ new
+ }
+}
+
+// CUSTOM SERIALIZATION & DESERIALIZATION
+#[derive(Serialize, Deserialize)]
+struct DavDagSerializedRepr {
+ items: Vec<IndexEntry>,
+ heads: Vec<UniqueIdent>,
+}
+
+impl<'de> Deserialize<'de> for DavDag {
+ fn deserialize<D>(d: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let val: DavDagSerializedRepr = DavDagSerializedRepr::deserialize(d)?;
+ let mut davdag = DavDag::default();
+
+ // Build the table + index
+ val.items
+ .into_iter()
+ .for_each(|entry| davdag.register(None, entry));
+
+ // Initialize the synchronization DAG with its roots
+ val.heads.into_iter().for_each(|ident| {
+ davdag.heads.insert(ident);
+ davdag.origins.insert(ident);
+ davdag.all_nodes.insert(ident);
+ });
+
+ Ok(davdag)
+ }
+}
+
+impl Serialize for DavDag {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ // Indexes are rebuilt on the fly, we serialize only the core database
+ let items = self.table.iter().map(|(_, entry)| entry.clone()).collect();
+
+ // We keep only the head entries from the sync graph,
+ // these entries will be used to initialize it back when deserializing
+ let heads = self.heads_vec();
+
+ // Finale serialization object
+ let val = DavDagSerializedRepr { items, heads };
+ val.serialize(serializer)
+ }
+}
+
+// ---- TESTS ----
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn base() {
+ let mut state = DavDag::default();
+
+ // Add item 1
+ {
+ let m = UniqueIdent([0x01; 24]);
+ let ev = state.op_put((m, "cal.ics".into(), "321-321".into()));
+ state = state.apply(&ev);
+
+ assert_eq!(state.table.len(), 1);
+ assert_eq!(state.resolve(ev.token()).unwrap().len(), 0);
+ }
+
+ // Add 2 concurrent items
+ let (t1, t2) = {
+ let blob1 = UniqueIdent([0x02; 24]);
+ let ev1 = state.op_put((blob1, "cal2.ics".into(), "321-321".into()));
+
+ let blob2 = UniqueIdent([0x01; 24]);
+ let ev2 = state.op_delete(blob2);
+
+ state = state.apply(&ev1);
+ state = state.apply(&ev2);
+
+ assert_eq!(state.table.len(), 1);
+ assert_eq!(state.resolve(ev1.token()).unwrap(), ordset![ev2.token()]);
+
+ (ev1.token(), ev2.token())
+ };
+
+ // Add later a new item
+ {
+ let blob3 = UniqueIdent([0x03; 24]);
+ let ev = state.op_put((blob3, "cal3.ics".into(), "321-321".into()));
+
+ state = state.apply(&ev);
+ assert_eq!(state.table.len(), 2);
+ assert_eq!(state.resolve(ev.token()).unwrap().len(), 0);
+ assert_eq!(state.resolve(t1).unwrap(), ordset![t2, ev.token()]);
+ }
+ }
+}
diff --git a/aero-collections/src/lib.rs b/aero-collections/src/lib.rs
new file mode 100644
index 0000000..eabf61c
--- /dev/null
+++ b/aero-collections/src/lib.rs
@@ -0,0 +1,5 @@
+pub mod calendar;
+pub mod davdag;
+pub mod mail;
+pub mod unique_ident;
+pub mod user;
diff --git a/src/mail/incoming.rs b/aero-collections/src/mail/incoming.rs
index 781d8dc..55c2515 100644
--- a/src/mail/incoming.rs
+++ b/aero-collections/src/mail/incoming.rs
@@ -1,6 +1,3 @@
-//use std::collections::HashMap;
-use std::convert::TryFrom;
-
use std::sync::{Arc, Weak};
use std::time::Duration;
@@ -11,15 +8,16 @@ use futures::{future::BoxFuture, FutureExt};
use tokio::sync::watch;
use tracing::{debug, error, info, warn};
-use crate::cryptoblob;
-use crate::login::{Credentials, PublicCredentials};
+use aero_bayou::timestamp::now_msec;
+use aero_user::cryptoblob;
+use aero_user::login::{Credentials, PublicCredentials};
+use aero_user::storage;
+
use crate::mail::mailbox::Mailbox;
use crate::mail::uidindex::ImapUidvalidity;
-use crate::mail::unique_ident::*;
-use crate::mail::user::User;
use crate::mail::IMF;
-use crate::storage;
-use crate::timestamp::now_msec;
+use crate::unique_ident::*;
+use crate::user::User;
const INCOMING_PK: &str = "incoming";
const INCOMING_LOCK_SK: &str = "lock";
diff --git a/src/mail/mailbox.rs b/aero-collections/src/mail/mailbox.rs
index 9190883..bec9669 100644
--- a/src/mail/mailbox.rs
+++ b/aero-collections/src/mail/mailbox.rs
@@ -2,14 +2,15 @@ use anyhow::{anyhow, bail, Result};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
-use crate::bayou::Bayou;
-use crate::cryptoblob::{self, gen_key, open_deserialize, seal_serialize, Key};
-use crate::login::Credentials;
+use aero_bayou::timestamp::now_msec;
+use aero_bayou::Bayou;
+use aero_user::cryptoblob::{self, gen_key, open_deserialize, seal_serialize, Key};
+use aero_user::login::Credentials;
+use aero_user::storage::{self, BlobRef, BlobVal, RowRef, RowVal, Selector, Store};
+
use crate::mail::uidindex::*;
-use crate::mail::unique_ident::*;
use crate::mail::IMF;
-use crate::storage::{self, BlobRef, BlobVal, RowRef, RowVal, Selector, Store};
-use crate::timestamp::now_msec;
+use crate::unique_ident::*;
pub struct Mailbox {
pub(super) id: UniqueIdent,
@@ -17,7 +18,7 @@ pub struct Mailbox {
}
impl Mailbox {
- pub(super) async fn open(
+ pub(crate) async fn open(
creds: &Credentials,
id: UniqueIdent,
min_uidvalidity: ImapUidvalidity,
@@ -374,7 +375,7 @@ impl MailboxInternal {
async fn delete(&mut self, ident: UniqueIdent) -> Result<()> {
if !self.uid_index.state().table.contains_key(&ident) {
- bail!("Cannot delete mail that doesn't exit");
+ bail!("Cannot delete mail that doesn't exist");
}
let del_mail_op = self.uid_index.state().op_mail_del(ident);
diff --git a/src/mail/mod.rs b/aero-collections/src/mail/mod.rs
index 37578b8..584a9eb 100644
--- a/src/mail/mod.rs
+++ b/aero-collections/src/mail/mod.rs
@@ -1,12 +1,9 @@
-use std::convert::TryFrom;
-
pub mod incoming;
pub mod mailbox;
+pub mod namespace;
pub mod query;
pub mod snapshot;
pub mod uidindex;
-pub mod unique_ident;
-pub mod user;
// Internet Message Format
// aka RFC 822 - RFC 2822 - RFC 5322
diff --git a/aero-collections/src/mail/namespace.rs b/aero-collections/src/mail/namespace.rs
new file mode 100644
index 0000000..0f1db7d
--- /dev/null
+++ b/aero-collections/src/mail/namespace.rs
@@ -0,0 +1,206 @@
+use std::collections::BTreeMap;
+
+use anyhow::{bail, Result};
+use serde::{Deserialize, Serialize};
+
+use aero_bayou::timestamp::now_msec;
+
+use crate::mail::uidindex::ImapUidvalidity;
+use crate::unique_ident::{gen_ident, UniqueIdent};
+
+pub const MAILBOX_HIERARCHY_DELIMITER: char = '.';
+
+/// INBOX is the only mailbox that must always exist.
+/// It is created automatically when the account is created.
+/// IMAP allows the user to rename INBOX to something else,
+/// in this case all messages from INBOX are moved to a mailbox
+/// with the new name and the INBOX mailbox still exists and is empty.
+/// In our implementation, we indeed move the underlying mailbox
+/// to the new name (i.e. the new name has the same id as the previous
+/// INBOX), and we create a new empty mailbox for INBOX.
+pub const INBOX: &str = "INBOX";
+
+/// For convenience purpose, we also create some special mailbox
+/// that are described in RFC6154 SPECIAL-USE
+/// @FIXME maybe it should be a configuration parameter
+/// @FIXME maybe we should have a per-mailbox flag mechanism, either an enum or a string, so we
+/// track which mailbox is used for what.
+/// @FIXME Junk could be useful but we don't have any antispam solution yet so...
+/// @FIXME IMAP supports virtual mailbox. \All or \Flagged are intended to be virtual mailboxes.
+/// \Trash might be one, or not one. I don't know what we should do there.
+pub const DRAFTS: &str = "Drafts";
+pub const ARCHIVE: &str = "Archive";
+pub const SENT: &str = "Sent";
+pub const TRASH: &str = "Trash";
+
+pub(crate) const MAILBOX_LIST_PK: &str = "mailboxes";
+pub(crate) const MAILBOX_LIST_SK: &str = "list";
+
+// ---- User's mailbox list (serialized in K2V) ----
+
+#[derive(Serialize, Deserialize)]
+pub(crate) struct MailboxList(BTreeMap<String, MailboxListEntry>);
+
+#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
+pub(crate) struct MailboxListEntry {
+ id_lww: (u64, Option<UniqueIdent>),
+ uidvalidity: ImapUidvalidity,
+}
+
+impl MailboxListEntry {
+ fn merge(&mut self, other: &Self) {
+ // Simple CRDT merge rule
+ if other.id_lww.0 > self.id_lww.0
+ || (other.id_lww.0 == self.id_lww.0 && other.id_lww.1 > self.id_lww.1)
+ {
+ self.id_lww = other.id_lww;
+ }
+ self.uidvalidity = std::cmp::max(self.uidvalidity, other.uidvalidity);
+ }
+}
+
+impl MailboxList {
+ pub(crate) fn new() -> Self {
+ Self(BTreeMap::new())
+ }
+
+ pub(crate) fn merge(&mut self, list2: Self) {
+ for (k, v) in list2.0.into_iter() {
+ if let Some(e) = self.0.get_mut(&k) {
+ e.merge(&v);
+ } else {
+ self.0.insert(k, v);
+ }
+ }
+ }
+
+ pub(crate) fn existing_mailbox_names(&self) -> Vec<String> {
+ self.0
+ .iter()
+ .filter(|(_, v)| v.id_lww.1.is_some())
+ .map(|(k, _)| k.to_string())
+ .collect()
+ }
+
+ pub(crate) fn has_mailbox(&self, name: &str) -> bool {
+ matches!(
+ self.0.get(name),
+ Some(MailboxListEntry {
+ id_lww: (_, Some(_)),
+ ..
+ })
+ )
+ }
+
+ pub(crate) fn get_mailbox(&self, name: &str) -> Option<(ImapUidvalidity, Option<UniqueIdent>)> {
+ self.0.get(name).map(
+ |MailboxListEntry {
+ id_lww: (_, mailbox_id),
+ uidvalidity,
+ }| (*uidvalidity, *mailbox_id),
+ )
+ }
+
+ /// Ensures mailbox `name` maps to id `id`.
+ /// If it already mapped to that, returns None.
+ /// If a change had to be done, returns Some(new uidvalidity in mailbox).
+ pub(crate) fn set_mailbox(
+ &mut self,
+ name: &str,
+ id: Option<UniqueIdent>,
+ ) -> Option<ImapUidvalidity> {
+ let (ts, id, uidvalidity) = match self.0.get_mut(name) {
+ None => {
+ if id.is_none() {
+ return None;
+ } else {
+ (now_msec(), id, ImapUidvalidity::new(1).unwrap())
+ }
+ }
+ Some(MailboxListEntry {
+ id_lww,
+ uidvalidity,
+ }) => {
+ if id_lww.1 == id {
+ return None;
+ } else {
+ (
+ std::cmp::max(id_lww.0 + 1, now_msec()),
+ id,
+ ImapUidvalidity::new(uidvalidity.get() + 1).unwrap(),
+ )
+ }
+ }
+ };
+
+ self.0.insert(
+ name.into(),
+ MailboxListEntry {
+ id_lww: (ts, id),
+ uidvalidity,
+ },
+ );
+ Some(uidvalidity)
+ }
+
+ pub(crate) fn update_uidvalidity(&mut self, name: &str, new_uidvalidity: ImapUidvalidity) {
+ match self.0.get_mut(name) {
+ None => {
+ self.0.insert(
+ name.into(),
+ MailboxListEntry {
+ id_lww: (now_msec(), None),
+ uidvalidity: new_uidvalidity,
+ },
+ );
+ }
+ Some(MailboxListEntry { uidvalidity, .. }) => {
+ *uidvalidity = std::cmp::max(*uidvalidity, new_uidvalidity);
+ }
+ }
+ }
+
+ pub(crate) fn create_mailbox(&mut self, name: &str) -> CreatedMailbox {
+ if let Some(MailboxListEntry {
+ id_lww: (_, Some(id)),
+ uidvalidity,
+ }) = self.0.get(name)
+ {
+ return CreatedMailbox::Existed(*id, *uidvalidity);
+ }
+
+ let id = gen_ident();
+ let uidvalidity = self.set_mailbox(name, Some(id)).unwrap();
+ CreatedMailbox::Created(id, uidvalidity)
+ }
+
+ pub(crate) fn rename_mailbox(&mut self, old_name: &str, new_name: &str) -> Result<()> {
+ if let Some((uidvalidity, Some(mbid))) = self.get_mailbox(old_name) {
+ if self.has_mailbox(new_name) {
+ bail!(
+ "Cannot rename {} into {}: {} already exists",
+ old_name,
+ new_name,
+ new_name
+ );
+ }
+
+ self.set_mailbox(old_name, None);
+ self.set_mailbox(new_name, Some(mbid));
+ self.update_uidvalidity(new_name, uidvalidity);
+ Ok(())
+ } else {
+ bail!(
+ "Cannot rename {} into {}: {} doesn't exist",
+ old_name,
+ new_name,
+ old_name
+ );
+ }
+ }
+}
+
+pub(crate) enum CreatedMailbox {
+ Created(UniqueIdent, ImapUidvalidity),
+ Existed(UniqueIdent, ImapUidvalidity),
+}
diff --git a/src/mail/query.rs b/aero-collections/src/mail/query.rs
index 3e6fe99..7faba41 100644
--- a/src/mail/query.rs
+++ b/aero-collections/src/mail/query.rs
@@ -1,6 +1,6 @@
use super::mailbox::MailMeta;
use super::snapshot::FrozenMailbox;
-use super::unique_ident::UniqueIdent;
+use crate::unique_ident::UniqueIdent;
use anyhow::Result;
use futures::future::FutureExt;
use futures::stream::{BoxStream, Stream, StreamExt};
diff --git a/src/mail/snapshot.rs b/aero-collections/src/mail/snapshot.rs
index ed756b5..6f8a8a8 100644
--- a/src/mail/snapshot.rs
+++ b/aero-collections/src/mail/snapshot.rs
@@ -5,7 +5,7 @@ use anyhow::Result;
use super::mailbox::Mailbox;
use super::query::{Query, QueryScope};
use super::uidindex::UidIndex;
-use super::unique_ident::UniqueIdent;
+use crate::unique_ident::UniqueIdent;
/// A Frozen Mailbox has a snapshot of the current mailbox
/// state that is desynchronized with the real mailbox state.
diff --git a/src/mail/uidindex.rs b/aero-collections/src/mail/uidindex.rs
index 5a06670..6df3206 100644
--- a/src/mail/uidindex.rs
+++ b/aero-collections/src/mail/uidindex.rs
@@ -3,8 +3,8 @@ use std::num::{NonZeroU32, NonZeroU64};
use im::{HashMap, OrdMap, OrdSet};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
-use crate::bayou::*;
-use crate::mail::unique_ident::UniqueIdent;
+use crate::unique_ident::UniqueIdent;
+use aero_bayou::*;
pub type ModSeq = NonZeroU64;
pub type ImapUid = NonZeroU32;
diff --git a/src/mail/unique_ident.rs b/aero-collections/src/unique_ident.rs
index 0e629db..e4eea7a 100644
--- a/src/mail/unique_ident.rs
+++ b/aero-collections/src/unique_ident.rs
@@ -5,9 +5,9 @@ use lazy_static::lazy_static;
use rand::prelude::*;
use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer};
-use crate::timestamp::now_msec;
+use aero_bayou::timestamp::now_msec;
-/// An internal Mail Identifier is composed of two components:
+/// An internal Aerogramme identifier is composed of two components:
/// - a process identifier, 128 bits, itself composed of:
/// - the timestamp of when the process started, 64 bits
/// - a 64-bit random number
@@ -15,7 +15,7 @@ use crate::timestamp::now_msec;
/// They are not part of the protocol but an internal representation
/// required by Aerogramme.
/// Their main property is to be unique without having to rely
-/// on synchronization between IMAP processes.
+/// on synchronization between (IMAP) processes.
#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct UniqueIdent(pub [u8; 24]);
diff --git a/src/mail/user.rs b/aero-collections/src/user.rs
index ad05615..f125c46 100644
--- a/src/mail/user.rs
+++ b/aero-collections/src/user.rs
@@ -1,54 +1,41 @@
-use std::collections::{BTreeMap, HashMap};
+use std::collections::HashMap;
use std::sync::{Arc, Weak};
use anyhow::{anyhow, bail, Result};
use lazy_static::lazy_static;
-use serde::{Deserialize, Serialize};
use tokio::sync::watch;
-use crate::cryptoblob::{open_deserialize, seal_serialize};
-use crate::login::Credentials;
+use aero_user::cryptoblob::{open_deserialize, seal_serialize};
+use aero_user::login::Credentials;
+use aero_user::storage;
+
+use crate::calendar::namespace::CalendarNs;
use crate::mail::incoming::incoming_mail_watch_process;
use crate::mail::mailbox::Mailbox;
+use crate::mail::namespace::{
+ CreatedMailbox, MailboxList, ARCHIVE, DRAFTS, INBOX, MAILBOX_HIERARCHY_DELIMITER,
+ MAILBOX_LIST_PK, MAILBOX_LIST_SK, SENT, TRASH,
+};
use crate::mail::uidindex::ImapUidvalidity;
-use crate::mail::unique_ident::{gen_ident, UniqueIdent};
-use crate::storage;
-use crate::timestamp::now_msec;
-
-pub const MAILBOX_HIERARCHY_DELIMITER: char = '.';
-
-/// INBOX is the only mailbox that must always exist.
-/// It is created automatically when the account is created.
-/// IMAP allows the user to rename INBOX to something else,
-/// in this case all messages from INBOX are moved to a mailbox
-/// with the new name and the INBOX mailbox still exists and is empty.
-/// In our implementation, we indeed move the underlying mailbox
-/// to the new name (i.e. the new name has the same id as the previous
-/// INBOX), and we create a new empty mailbox for INBOX.
-pub const INBOX: &str = "INBOX";
-
-/// For convenience purpose, we also create some special mailbox
-/// that are described in RFC6154 SPECIAL-USE
-/// @FIXME maybe it should be a configuration parameter
-/// @FIXME maybe we should have a per-mailbox flag mechanism, either an enum or a string, so we
-/// track which mailbox is used for what.
-/// @FIXME Junk could be useful but we don't have any antispam solution yet so...
-/// @FIXME IMAP supports virtual mailbox. \All or \Flagged are intended to be virtual mailboxes.
-/// \Trash might be one, or not one. I don't know what we should do there.
-pub const DRAFTS: &str = "Drafts";
-pub const ARCHIVE: &str = "Archive";
-pub const SENT: &str = "Sent";
-pub const TRASH: &str = "Trash";
-
-const MAILBOX_LIST_PK: &str = "mailboxes";
-const MAILBOX_LIST_SK: &str = "list";
+use crate::unique_ident::UniqueIdent;
+
+//@FIXME User should be totally rewriten
+// to extract the local mailbox list
+// to the mail/namespace.rs file (and mailbox list should be reworded as mail namespace)
+
+//@FIXME User should be run in a LocalSet
+// to remove most - if not all - synchronizations types.
+// Especially RwLock & co.
pub struct User {
pub username: String,
pub creds: Credentials,
pub storage: storage::Store,
pub mailboxes: std::sync::Mutex<HashMap<UniqueIdent, Weak<Mailbox>>>,
+ pub calendars: CalendarNs,
+ // Handle on worker processing received email
+ // (moving emails from the mailqueue to the user's INBOX)
tx_inbox_id: watch::Sender<Option<(UniqueIdent, ImapUidvalidity)>>,
}
@@ -202,6 +189,7 @@ impl User {
storage,
tx_inbox_id,
mailboxes: std::sync::Mutex::new(HashMap::new()),
+ calendars: CalendarNs::new(),
});
// Ensure INBOX exists (done inside load_mailbox_list)
@@ -228,6 +216,10 @@ impl User {
}
}
+ // The idea here is that:
+ // 1. Opening a mailbox that is not already opened takes a significant amount of time
+ // 2. We don't want to lock the whole HashMap that contain the mailboxes during this
+ // operation which is why we droppped the lock above but take it again below.
let mb = Arc::new(Mailbox::open(&self.creds, id, min_uidvalidity).await?);
let mut cache = self.mailboxes.lock().unwrap();
@@ -327,171 +319,6 @@ impl User {
}
}
-// ---- User's mailbox list (serialized in K2V) ----
-
-#[derive(Serialize, Deserialize)]
-struct MailboxList(BTreeMap<String, MailboxListEntry>);
-
-#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
-struct MailboxListEntry {
- id_lww: (u64, Option<UniqueIdent>),
- uidvalidity: ImapUidvalidity,
-}
-
-impl MailboxListEntry {
- fn merge(&mut self, other: &Self) {
- // Simple CRDT merge rule
- if other.id_lww.0 > self.id_lww.0
- || (other.id_lww.0 == self.id_lww.0 && other.id_lww.1 > self.id_lww.1)
- {
- self.id_lww = other.id_lww;
- }
- self.uidvalidity = std::cmp::max(self.uidvalidity, other.uidvalidity);
- }
-}
-
-impl MailboxList {
- fn new() -> Self {
- Self(BTreeMap::new())
- }
-
- fn merge(&mut self, list2: Self) {
- for (k, v) in list2.0.into_iter() {
- if let Some(e) = self.0.get_mut(&k) {
- e.merge(&v);
- } else {
- self.0.insert(k, v);
- }
- }
- }
-
- fn existing_mailbox_names(&self) -> Vec<String> {
- self.0
- .iter()
- .filter(|(_, v)| v.id_lww.1.is_some())
- .map(|(k, _)| k.to_string())
- .collect()
- }
-
- fn has_mailbox(&self, name: &str) -> bool {
- matches!(
- self.0.get(name),
- Some(MailboxListEntry {
- id_lww: (_, Some(_)),
- ..
- })
- )
- }
-
- fn get_mailbox(&self, name: &str) -> Option<(ImapUidvalidity, Option<UniqueIdent>)> {
- self.0.get(name).map(
- |MailboxListEntry {
- id_lww: (_, mailbox_id),
- uidvalidity,
- }| (*uidvalidity, *mailbox_id),
- )
- }
-
- /// Ensures mailbox `name` maps to id `id`.
- /// If it already mapped to that, returns None.
- /// If a change had to be done, returns Some(new uidvalidity in mailbox).
- fn set_mailbox(&mut self, name: &str, id: Option<UniqueIdent>) -> Option<ImapUidvalidity> {
- let (ts, id, uidvalidity) = match self.0.get_mut(name) {
- None => {
- if id.is_none() {
- return None;
- } else {
- (now_msec(), id, ImapUidvalidity::new(1).unwrap())
- }
- }
- Some(MailboxListEntry {
- id_lww,
- uidvalidity,
- }) => {
- if id_lww.1 == id {
- return None;
- } else {
- (
- std::cmp::max(id_lww.0 + 1, now_msec()),
- id,
- ImapUidvalidity::new(uidvalidity.get() + 1).unwrap(),
- )
- }
- }
- };
-
- self.0.insert(
- name.into(),
- MailboxListEntry {
- id_lww: (ts, id),
- uidvalidity,
- },
- );
- Some(uidvalidity)
- }
-
- fn update_uidvalidity(&mut self, name: &str, new_uidvalidity: ImapUidvalidity) {
- match self.0.get_mut(name) {
- None => {
- self.0.insert(
- name.into(),
- MailboxListEntry {
- id_lww: (now_msec(), None),
- uidvalidity: new_uidvalidity,
- },
- );
- }
- Some(MailboxListEntry { uidvalidity, .. }) => {
- *uidvalidity = std::cmp::max(*uidvalidity, new_uidvalidity);
- }
- }
- }
-
- fn create_mailbox(&mut self, name: &str) -> CreatedMailbox {
- if let Some(MailboxListEntry {
- id_lww: (_, Some(id)),
- uidvalidity,
- }) = self.0.get(name)
- {
- return CreatedMailbox::Existed(*id, *uidvalidity);
- }
-
- let id = gen_ident();
- let uidvalidity = self.set_mailbox(name, Some(id)).unwrap();
- CreatedMailbox::Created(id, uidvalidity)
- }
-
- fn rename_mailbox(&mut self, old_name: &str, new_name: &str) -> Result<()> {
- if let Some((uidvalidity, Some(mbid))) = self.get_mailbox(old_name) {
- if self.has_mailbox(new_name) {
- bail!(
- "Cannot rename {} into {}: {} already exists",
- old_name,
- new_name,
- new_name
- );
- }
-
- self.set_mailbox(old_name, None);
- self.set_mailbox(new_name, Some(mbid));
- self.update_uidvalidity(new_name, uidvalidity);
- Ok(())
- } else {
- bail!(
- "Cannot rename {} into {}: {} doesn't exist",
- old_name,
- new_name,
- old_name
- );
- }
- }
-}
-
-enum CreatedMailbox {
- Created(UniqueIdent, ImapUidvalidity),
- Existed(UniqueIdent, ImapUidvalidity),
-}
-
// ---- User cache ----
lazy_static! {
diff --git a/aero-dav/.gitignore b/aero-dav/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/aero-dav/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/aero-dav/Cargo.toml b/aero-dav/Cargo.toml
new file mode 100644
index 0000000..c847f68
--- /dev/null
+++ b/aero-dav/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "aero-dav"
+version = "0.3.0"
+authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
+edition = "2021"
+license = "EUPL-1.2"
+description = "A partial and standalone implementation of the WebDAV protocol and its extensions (eg. CalDAV or CardDAV)"
+
+[dependencies]
+quick-xml.workspace = true
+http.workspace = true
+chrono.workspace = true
+tokio.workspace = true
+futures.workspace = true
+tracing.workspace = true
diff --git a/aero-dav/fuzz/.gitignore b/aero-dav/fuzz/.gitignore
new file mode 100644
index 0000000..1a45eee
--- /dev/null
+++ b/aero-dav/fuzz/.gitignore
@@ -0,0 +1,4 @@
+target
+corpus
+artifacts
+coverage
diff --git a/aero-dav/fuzz/Cargo.lock b/aero-dav/fuzz/Cargo.lock
new file mode 100644
index 0000000..08fa951
--- /dev/null
+++ b/aero-dav/fuzz/Cargo.lock
@@ -0,0 +1,4249 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "abnf-core"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182d1f071b906a9f59269c89af101515a5cbe58f723eb6717e7fe7445c0dea"
+dependencies = [
+ "nom 7.1.3",
+]
+
+[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aerogramme"
+version = "0.3.0"
+dependencies = [
+ "anyhow",
+ "argon2",
+ "async-trait",
+ "aws-config",
+ "aws-sdk-s3",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "backtrace",
+ "base64 0.21.7",
+ "chrono",
+ "clap",
+ "console-subscriber",
+ "duplexify",
+ "eml-codec",
+ "futures",
+ "hex",
+ "http 1.1.0",
+ "http-body-util",
+ "hyper 1.2.0",
+ "hyper-rustls 0.26.0",
+ "hyper-util",
+ "im",
+ "imap-codec",
+ "imap-flow",
+ "itertools 0.10.5",
+ "k2v-client",
+ "lazy_static",
+ "ldap3",
+ "log",
+ "nix",
+ "nom 7.1.3",
+ "quick-xml",
+ "rand",
+ "rmp-serde",
+ "rpassword",
+ "rustls 0.22.2",
+ "rustls-pemfile 2.1.1",
+ "serde",
+ "smtp-message",
+ "smtp-server",
+ "sodiumoxide",
+ "thiserror",
+ "tokio",
+ "tokio-rustls 0.25.0",
+ "tokio-util",
+ "toml",
+ "tracing",
+ "tracing-subscriber",
+ "zstd",
+]
+
+[[package]]
+name = "aerogramme-fuzz"
+version = "0.0.0"
+dependencies = [
+ "aerogramme",
+ "libfuzzer-sys",
+ "quick-xml",
+ "tokio",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
+
+[[package]]
+name = "arbitrary"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
+
+[[package]]
+name = "argon2"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
+dependencies = [
+ "base64ct",
+ "blake2",
+ "cpufeatures",
+ "password-hash",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
+[[package]]
+name = "asn1-rs"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30ff05a702273012438132f449575dbc804e27b2f3cbe3069aa237d26c98fa33"
+dependencies = [
+ "asn1-rs-derive",
+ "asn1-rs-impl",
+ "displaydoc",
+ "nom 7.1.3",
+ "num-traits",
+ "rusticata-macros",
+ "thiserror",
+ "time",
+]
+
+[[package]]
+name = "asn1-rs-derive"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db8b7511298d5b7784b40b092d9e9dcd3a627a5707e4b5e507931ab0d44eeebf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "synstructure",
+]
+
+[[package]]
+name = "asn1-rs-impl"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 2.5.3",
+ "futures-core",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 5.2.0",
+ "event-listener-strategy 0.5.0",
+ "futures-core",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c"
+dependencies = [
+ "async-lock 3.3.0",
+ "async-task",
+ "concurrent-queue",
+ "fastrand 2.0.1",
+ "futures-lite 2.2.0",
+ "slab",
+]
+
+[[package]]
+name = "async-fs"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06"
+dependencies = [
+ "async-lock 2.8.0",
+ "autocfg",
+ "blocking",
+ "futures-lite 1.13.0",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
+dependencies = [
+ "async-channel 2.2.0",
+ "async-executor",
+ "async-io 2.3.1",
+ "async-lock 3.3.0",
+ "blocking",
+ "futures-lite 2.2.0",
+ "once_cell",
+]
+
+[[package]]
+name = "async-io"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
+dependencies = [
+ "async-lock 2.8.0",
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-lite 1.13.0",
+ "log",
+ "parking",
+ "polling 2.8.0",
+ "rustix 0.37.27",
+ "slab",
+ "socket2 0.4.10",
+ "waker-fn",
+]
+
+[[package]]
+name = "async-io"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65"
+dependencies = [
+ "async-lock 3.3.0",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite 2.2.0",
+ "parking",
+ "polling 3.5.0",
+ "rustix 0.38.31",
+ "slab",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
+dependencies = [
+ "event-listener 2.5.3",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b"
+dependencies = [
+ "event-listener 4.0.3",
+ "event-listener-strategy 0.4.0",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "async-net"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0434b1ed18ce1cf5769b8ac540e33f01fa9471058b5e89da9e06f3c882a8c12f"
+dependencies = [
+ "async-io 1.13.0",
+ "blocking",
+ "futures-lite 1.13.0",
+]
+
+[[package]]
+name = "async-process"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88"
+dependencies = [
+ "async-io 1.13.0",
+ "async-lock 2.8.0",
+ "async-signal",
+ "blocking",
+ "cfg-if",
+ "event-listener 3.1.0",
+ "futures-lite 1.13.0",
+ "rustix 0.38.31",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5"
+dependencies = [
+ "async-io 2.3.1",
+ "async-lock 2.8.0",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix 0.38.31",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "async-std"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
+dependencies = [
+ "async-channel 1.9.0",
+ "async-global-executor",
+ "async-io 1.13.0",
+ "async-lock 2.8.0",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite 1.13.0",
+ "gloo-timers",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "once_cell",
+ "pin-project-lite 0.2.13",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-stream"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
+
+[[package]]
+name = "async-trait"
+version = "0.1.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "auto_enums"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe0dfe45d75158751e195799f47ea02e81f570aa24bc5ef999cdd9e888c4b5c3"
+dependencies = [
+ "auto_enums_core",
+ "auto_enums_derive",
+]
+
+[[package]]
+name = "auto_enums_core"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da47c46001293a2c4b744d731958be22cff408a2ab76e2279328f9713b1267b4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "auto_enums_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41aed1da83ecdc799503b7cb94da1b45a34d72b49caf40a61d9cf5b88ec07cfd"
+dependencies = [
+ "autocfg",
+ "derive_utils",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "aws-config"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b96342ea8948ab9bef3e6234ea97fc32e2d8a88d8fb6a084e52267317f94b6b"
+dependencies = [
+ "aws-credential-types",
+ "aws-runtime",
+ "aws-sdk-sso",
+ "aws-sdk-ssooidc",
+ "aws-sdk-sts",
+ "aws-smithy-async",
+ "aws-smithy-http",
+ "aws-smithy-json",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "fastrand 2.0.1",
+ "hex",
+ "http 0.2.12",
+ "hyper 0.14.28",
+ "ring 0.17.8",
+ "time",
+ "tokio",
+ "tracing",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-credential-types"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "273fa47dafc9ef14c2c074ddddbea4561ff01b7f68d5091c0e9737ced605c01d"
+dependencies = [
+ "aws-smithy-async",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-runtime"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e38bab716c8bf07da24be07ecc02e0f5656ce8f30a891322ecdcb202f943b85"
+dependencies = [
+ "aws-credential-types",
+ "aws-sigv4",
+ "aws-smithy-async",
+ "aws-smithy-eventstream",
+ "aws-smithy-http",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "fastrand 2.0.1",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "percent-encoding",
+ "pin-project-lite 0.2.13",
+ "tracing",
+ "uuid",
+]
+
+[[package]]
+name = "aws-sdk-config"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07979fd68679736ba306d6ea2a4dc2fd835ac4d454942c5d8920ef83ed2f979f"
+dependencies = [
+ "aws-credential-types",
+ "aws-runtime",
+ "aws-smithy-async",
+ "aws-smithy-http",
+ "aws-smithy-json",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "http 0.2.12",
+ "once_cell",
+ "regex-lite",
+ "tracing",
+]
+
+[[package]]
+name = "aws-sdk-s3"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93d35d39379445970fc3e4ddf7559fff2c32935ce0b279f9cb27080d6b7c6d94"
+dependencies = [
+ "aws-credential-types",
+ "aws-runtime",
+ "aws-sigv4",
+ "aws-smithy-async",
+ "aws-smithy-checksums",
+ "aws-smithy-eventstream",
+ "aws-smithy-http",
+ "aws-smithy-json",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-smithy-xml",
+ "aws-types",
+ "bytes",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "once_cell",
+ "percent-encoding",
+ "regex-lite",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "aws-sdk-sso"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d84bd3925a17c9adbf6ec65d52104a44a09629d8f70290542beeee69a95aee7f"
+dependencies = [
+ "aws-credential-types",
+ "aws-runtime",
+ "aws-smithy-async",
+ "aws-smithy-http",
+ "aws-smithy-json",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "http 0.2.12",
+ "once_cell",
+ "regex-lite",
+ "tracing",
+]
+
+[[package]]
+name = "aws-sdk-ssooidc"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c2dae39e997f58bc4d6292e6244b26ba630c01ab671b6f9f44309de3eb80ab8"
+dependencies = [
+ "aws-credential-types",
+ "aws-runtime",
+ "aws-smithy-async",
+ "aws-smithy-http",
+ "aws-smithy-json",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "http 0.2.12",
+ "once_cell",
+ "regex-lite",
+ "tracing",
+]
+
+[[package]]
+name = "aws-sdk-sts"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17fd9a53869fee17cea77e352084e1aa71e2c5e323d974c13a9c2bcfd9544c7f"
+dependencies = [
+ "aws-credential-types",
+ "aws-runtime",
+ "aws-smithy-async",
+ "aws-smithy-http",
+ "aws-smithy-json",
+ "aws-smithy-query",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-smithy-xml",
+ "aws-types",
+ "http 0.2.12",
+ "once_cell",
+ "regex-lite",
+ "tracing",
+]
+
+[[package]]
+name = "aws-sigv4"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ada00a4645d7d89f296fe0ddbc3fe3554f03035937c849a05d37ddffc1f29a1"
+dependencies = [
+ "aws-credential-types",
+ "aws-smithy-eventstream",
+ "aws-smithy-http",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "bytes",
+ "crypto-bigint 0.5.5",
+ "form_urlencoded",
+ "hex",
+ "hmac",
+ "http 0.2.12",
+ "http 1.1.0",
+ "once_cell",
+ "p256",
+ "percent-encoding",
+ "ring 0.17.8",
+ "sha2",
+ "subtle",
+ "time",
+ "tracing",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-smithy-async"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcf7f09a27286d84315dfb9346208abb3b0973a692454ae6d0bc8d803fcce3b4"
+dependencies = [
+ "futures-util",
+ "pin-project-lite 0.2.13",
+ "tokio",
+]
+
+[[package]]
+name = "aws-smithy-checksums"
+version = "0.60.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fd4b66f2a8e7c84d7e97bda2666273d41d2a2e25302605bcf906b7b2661ae5e"
+dependencies = [
+ "aws-smithy-http",
+ "aws-smithy-types",
+ "bytes",
+ "crc32c",
+ "crc32fast",
+ "hex",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "md-5",
+ "pin-project-lite 0.2.13",
+ "sha1",
+ "sha2",
+ "tracing",
+]
+
+[[package]]
+name = "aws-smithy-eventstream"
+version = "0.60.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6363078f927f612b970edf9d1903ef5cef9a64d1e8423525ebb1f0a1633c858"
+dependencies = [
+ "aws-smithy-types",
+ "bytes",
+ "crc32fast",
+]
+
+[[package]]
+name = "aws-smithy-http"
+version = "0.60.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6ca214a6a26f1b7ebd63aa8d4f5e2194095643023f9608edf99a58247b9d80d"
+dependencies = [
+ "aws-smithy-eventstream",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "bytes",
+ "bytes-utils",
+ "futures-core",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite 0.2.13",
+ "pin-utils",
+ "tracing",
+]
+
+[[package]]
+name = "aws-smithy-json"
+version = "0.60.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1af80ecf3057fb25fe38d1687e94c4601a7817c6a1e87c1b0635f7ecb644ace5"
+dependencies = [
+ "aws-smithy-types",
+]
+
+[[package]]
+name = "aws-smithy-query"
+version = "0.60.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb27084f72ea5fc20033efe180618677ff4a2f474b53d84695cfe310a6526cbc"
+dependencies = [
+ "aws-smithy-types",
+ "urlencoding",
+]
+
+[[package]]
+name = "aws-smithy-runtime"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbb5fca54a532a36ff927fbd7407a7c8eb9c3b4faf72792ba2965ea2cad8ed55"
+dependencies = [
+ "aws-smithy-async",
+ "aws-smithy-http",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "bytes",
+ "fastrand 2.0.1",
+ "h2 0.3.24",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "hyper 0.14.28",
+ "hyper-rustls 0.24.2",
+ "once_cell",
+ "pin-project-lite 0.2.13",
+ "pin-utils",
+ "rustls 0.21.10",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "aws-smithy-runtime-api"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22389cb6f7cac64f266fb9f137745a9349ced7b47e0d2ba503e9e40ede4f7060"
+dependencies = [
+ "aws-smithy-async",
+ "aws-smithy-types",
+ "bytes",
+ "http 0.2.12",
+ "http 1.1.0",
+ "pin-project-lite 0.2.13",
+ "tokio",
+ "tracing",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-smithy-types"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f081da5481210523d44ffd83d9f0740320050054006c719eae0232d411f024d3"
+dependencies = [
+ "base64-simd",
+ "bytes",
+ "bytes-utils",
+ "futures-core",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "itoa",
+ "num-integer",
+ "pin-project-lite 0.2.13",
+ "pin-utils",
+ "ryu",
+ "serde",
+ "time",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "aws-smithy-xml"
+version = "0.60.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fccd8f595d0ca839f9f2548e66b99514a85f92feb4c01cf2868d93eb4888a42"
+dependencies = [
+ "xmlparser",
+]
+
+[[package]]
+name = "aws-types"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07c63521aa1ea9a9f92a701f1a08ce3fd20b46c6efc0d5c8947c1fd879e3df1"
+dependencies = [
+ "aws-credential-types",
+ "aws-smithy-async",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "http 0.2.12",
+ "rustc_version",
+ "tracing",
+]
+
+[[package]]
+name = "axum"
+version = "0.6.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "bitflags 1.3.2",
+ "bytes",
+ "futures-util",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "hyper 0.14.28",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite 0.2.13",
+ "rustversion",
+ "serde",
+ "sync_wrapper",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "mime",
+ "rustversion",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base16ct"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce"
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64-simd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
+dependencies = [
+ "outref",
+ "vsimd",
+]
+
+[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
+
+[[package]]
+name = "bitmaps"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "bitvec"
+version = "0.19.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
+dependencies = [
+ "async-channel 2.2.0",
+ "async-lock 3.3.0",
+ "async-task",
+ "fastrand 2.0.1",
+ "futures-io",
+ "futures-lite 2.2.0",
+ "piper",
+ "tracing",
+]
+
+[[package]]
+name = "bounded-static"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2325bd33fa7e3018e7e37f5b0591ba009124963b5a3f8b7cae6d0a8c1028ed4"
+dependencies = [
+ "bounded-static-derive",
+]
+
+[[package]]
+name = "bounded-static-derive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f10dd247355bf631d98d2753d87ae62c84c8dcb996ad9b24a4168e0aec29bd6b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
+
+[[package]]
+name = "bytes-utils"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35"
+dependencies = [
+ "bytes",
+ "either",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
+dependencies = [
+ "jobserver",
+ "libc",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-targets 0.52.4",
+]
+
+[[package]]
+name = "clap"
+version = "3.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
+dependencies = [
+ "atty",
+ "bitflags 1.3.2",
+ "clap_derive",
+ "clap_lex",
+ "indexmap 1.9.3",
+ "once_cell",
+ "strsim",
+ "termcolor",
+ "textwrap",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "console-api"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787"
+dependencies = [
+ "futures-core",
+ "prost",
+ "prost-types",
+ "tonic",
+ "tracing-core",
+]
+
+[[package]]
+name = "console-subscriber"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e"
+dependencies = [
+ "console-api",
+ "crossbeam-channel",
+ "crossbeam-utils",
+ "futures-task",
+ "hdrhistogram",
+ "humantime",
+ "prost-types",
+ "serde",
+ "serde_json",
+ "thread_local",
+ "tokio",
+ "tokio-stream",
+ "tonic",
+ "tracing",
+ "tracing-core",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32c"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89254598aa9b9fa608de44b3ae54c810f0f06d755e24c50177f1f8f31ff50ce2"
+dependencies = [
+ "rustc_version",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
+
+[[package]]
+name = "crypto-bigint"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef"
+dependencies = [
+ "generic-array",
+ "rand_core",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
+
+[[package]]
+name = "der"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
+[[package]]
+name = "der-parser"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe398ac75057914d7d07307bf67dc7f3f574a26783b4fc7805a20ffa9f506e82"
+dependencies = [
+ "asn1-rs",
+ "displaydoc",
+ "nom 7.1.3",
+ "num-bigint",
+ "num-traits",
+ "rusticata-macros",
+]
+
+[[package]]
+name = "deranged"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_utils"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "532b4c15dccee12c7044f1fcad956e98410860b22231e44a3b827464797ca7bf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "duplexify"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1cc346cd6db38ceab2d33f59b26024c3ddb8e75f047c6cafbcbc016ea8065d5"
+dependencies = [
+ "async-std",
+ "pin-project-lite 0.1.12",
+]
+
+[[package]]
+name = "ecdsa"
+version = "0.14.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c"
+dependencies = [
+ "der",
+ "elliptic-curve",
+ "rfc6979",
+ "signature",
+]
+
+[[package]]
+name = "ed25519"
+version = "1.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
+dependencies = [
+ "signature",
+]
+
+[[package]]
+name = "either"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
+
+[[package]]
+name = "elliptic-curve"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3"
+dependencies = [
+ "base16ct",
+ "crypto-bigint 0.4.9",
+ "der",
+ "digest",
+ "ff",
+ "generic-array",
+ "group",
+ "pkcs8",
+ "rand_core",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "eml-codec"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4499124d87abce26a57ef96ece800fa8babc38fbedd81c607c340ae83d46d2e"
+dependencies = [
+ "base64 0.21.7",
+ "chrono",
+ "encoding_rs",
+ "nom 7.1.3",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "event-listener"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "event-listener"
+version = "4.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3"
+dependencies = [
+ "event-listener 4.0.3",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291"
+dependencies = [
+ "event-listener 5.2.0",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
+
+[[package]]
+name = "ff"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160"
+dependencies = [
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "funty"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
+
+[[package]]
+name = "futures"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-lite"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
+dependencies = [
+ "fastrand 1.9.0",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite 0.2.13",
+ "waker-fn",
+]
+
+[[package]]
+name = "futures-lite"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba"
+dependencies = [
+ "fastrand 2.0.1",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite 0.2.13",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gimli"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+
+[[package]]
+name = "gloo-timers"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "group"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7"
+dependencies = [
+ "ff",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.12",
+ "indexmap 2.2.5",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 1.1.0",
+ "indexmap 2.2.5",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+
+[[package]]
+name = "hdrhistogram"
+version = "7.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d"
+dependencies = [
+ "base64 0.21.7",
+ "byteorder",
+ "flate2",
+ "nom 7.1.3",
+ "num-traits",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes",
+ "http 0.2.12",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
+dependencies = [
+ "bytes",
+ "http 1.1.0",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "hyper"
+version = "0.14.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2 0.3.24",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite 0.2.13",
+ "socket2 0.5.6",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2 0.4.2",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite 0.2.13",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
+dependencies = [
+ "futures-util",
+ "http 0.2.12",
+ "hyper 0.14.28",
+ "log",
+ "rustls 0.21.10",
+ "rustls-native-certs 0.6.3",
+ "tokio",
+ "tokio-rustls 0.24.1",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
+dependencies = [
+ "futures-util",
+ "http 1.1.0",
+ "hyper 1.2.0",
+ "hyper-util",
+ "log",
+ "rustls 0.22.2",
+ "rustls-native-certs 0.7.0",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls 0.25.0",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-timeout"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
+dependencies = [
+ "hyper 0.14.28",
+ "pin-project-lite 0.2.13",
+ "tokio",
+ "tokio-io-timeout",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.0",
+ "hyper 1.2.0",
+ "pin-project-lite 0.2.13",
+ "socket2 0.5.6",
+ "tokio",
+ "tower",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "im"
+version = "15.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9"
+dependencies = [
+ "bitmaps",
+ "rand_core",
+ "rand_xoshiro",
+ "sized-chunks",
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "imap-codec"
+version = "2.0.0"
+source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#d8a5afc03fb771232e94c73af6a05e79dc80bbed"
+dependencies = [
+ "abnf-core",
+ "base64 0.21.7",
+ "bounded-static",
+ "chrono",
+ "imap-types",
+ "log",
+ "nom 7.1.3",
+ "thiserror",
+]
+
+[[package]]
+name = "imap-flow"
+version = "0.1.0"
+source = "git+https://github.com/duesee/imap-flow.git?branch=main#dce759a8531f317e8d7311fb032b366db6698e38"
+dependencies = [
+ "bounded-static",
+ "bytes",
+ "imap-codec",
+ "imap-types",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "imap-types"
+version = "2.0.0"
+source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#d8a5afc03fb771232e94c73af6a05e79dc80bbed"
+dependencies = [
+ "base64 0.21.7",
+ "bounded-static",
+ "chrono",
+ "thiserror",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.3",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
+dependencies = [
+ "hermit-abi 0.3.9",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
+
+[[package]]
+name = "jobserver"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "k2v-client"
+version = "0.0.4"
+source = "git+https://git.deuxfleurs.fr/Deuxfleurs/garage.git?branch=k2v/shared_http_client#8b35a946d9f6b31b26b9783acbfab984316051f4"
+dependencies = [
+ "aws-sdk-config",
+ "aws-sigv4",
+ "base64 0.21.7",
+ "hex",
+ "http 1.1.0",
+ "http-body-util",
+ "hyper 1.2.0",
+ "hyper-rustls 0.26.0",
+ "hyper-util",
+ "log",
+ "percent-encoding",
+ "serde",
+ "serde_json",
+ "sha2",
+ "thiserror",
+ "tokio",
+]
+
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lber"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a99b520993b21a6faab32643cf4726573dc18ca4cf2d48cbeb24d248c86c930"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "nom 2.2.1",
+]
+
+[[package]]
+name = "ldap3"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce38dafca0608c64cc0146fb782b06abb8d946dae7a3af23c89a95da24f6b84d"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures",
+ "futures-util",
+ "lazy_static",
+ "lber",
+ "log",
+ "nom 2.2.1",
+ "percent-encoding",
+ "ring 0.16.20",
+ "rustls 0.20.9",
+ "rustls-native-certs 0.6.3",
+ "thiserror",
+ "tokio",
+ "tokio-rustls 0.23.4",
+ "tokio-stream",
+ "tokio-util",
+ "url",
+ "x509-parser",
+]
+
+[[package]]
+name = "lexical-core"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
+dependencies = [
+ "arrayvec",
+ "bitflags 1.3.2",
+ "cfg-if",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.153"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
+dependencies = [
+ "arbitrary",
+ "cc",
+ "once_cell",
+]
+
+[[package]]
+name = "libsodium-sys"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "walkdir",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+dependencies = [
+ "value-bag",
+]
+
+[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata 0.1.10",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "nix"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
+dependencies = [
+ "bitflags 2.4.2",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "nom"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf51a729ecf40266a2368ad335a5fdde43471f545a967109cd62146ecf8b66ff"
+
+[[package]]
+name = "nom"
+version = "6.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
+dependencies = [
+ "bitvec",
+ "funty",
+ "lexical-core",
+ "memchr",
+ "version_check",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi 0.3.9",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "oid-registry"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e20717fa0541f39bd146692035c37bedfa532b3e5071b35761082407546b2a"
+dependencies = [
+ "asn1-rs",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "os_str_bytes"
+version = "6.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
+
+[[package]]
+name = "outref"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a"
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "p256"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "sha2",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
+
+[[package]]
+name = "password-hash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+dependencies = [
+ "base64ct",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "piper"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4"
+dependencies = [
+ "atomic-waker",
+ "fastrand 2.0.1",
+ "futures-io",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+
+[[package]]
+name = "polling"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
+dependencies = [
+ "autocfg",
+ "bitflags 1.3.2",
+ "cfg-if",
+ "concurrent-queue",
+ "libc",
+ "log",
+ "pin-project-lite 0.2.13",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "polling"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "pin-project-lite 0.2.13",
+ "rustix 0.38.31",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prost"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
+dependencies = [
+ "anyhow",
+ "itertools 0.11.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e"
+dependencies = [
+ "prost",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
+dependencies = [
+ "memchr",
+ "tokio",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radium"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_xoshiro"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata 0.4.6",
+ "regex-syntax 0.8.2",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax 0.6.29",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.8.2",
+]
+
+[[package]]
+name = "regex-lite"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e"
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
+
+[[package]]
+name = "rfc6979"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb"
+dependencies = [
+ "crypto-bigint 0.4.9",
+ "hmac",
+ "zeroize",
+]
+
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin 0.5.2",
+ "untrusted 0.7.1",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom",
+ "libc",
+ "spin 0.9.8",
+ "untrusted 0.9.0",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rmp"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20"
+dependencies = [
+ "byteorder",
+ "num-traits",
+ "paste",
+]
+
+[[package]]
+name = "rmp-serde"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "723ecff9ad04f4ad92fe1c8ca6c20d2196d9286e9c60727c4cb5511629260e9d"
+dependencies = [
+ "byteorder",
+ "rmp",
+ "serde",
+]
+
+[[package]]
+name = "rpassword"
+version = "7.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f"
+dependencies = [
+ "libc",
+ "rtoolbox",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rtoolbox"
+version = "0.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e"
+dependencies = [
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rusticata-macros"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
+dependencies = [
+ "nom 7.1.3",
+]
+
+[[package]]
+name = "rustix"
+version = "0.37.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
+dependencies = [
+ "bitflags 1.3.2",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys 0.3.8",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
+dependencies = [
+ "bitflags 2.4.2",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.13",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.20.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99"
+dependencies = [
+ "log",
+ "ring 0.16.20",
+ "sct",
+ "webpki",
+]
+
+[[package]]
+name = "rustls"
+version = "0.21.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
+dependencies = [
+ "log",
+ "ring 0.17.8",
+ "rustls-webpki 0.101.7",
+ "sct",
+]
+
+[[package]]
+name = "rustls"
+version = "0.22.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41"
+dependencies = [
+ "log",
+ "ring 0.17.8",
+ "rustls-pki-types",
+ "rustls-webpki 0.102.2",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile 1.0.4",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile 2.1.1",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
+dependencies = [
+ "base64 0.21.7",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab"
+dependencies = [
+ "base64 0.21.7",
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.101.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
+dependencies = [
+ "ring 0.17.8",
+ "untrusted 0.9.0",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.102.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
+dependencies = [
+ "ring 0.17.8",
+ "rustls-pki-types",
+ "untrusted 0.9.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+
+[[package]]
+name = "ryu"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "sct"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
+dependencies = [
+ "ring 0.17.8",
+ "untrusted 0.9.0",
+]
+
+[[package]]
+name = "sec1"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "pkcs8",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
+
+[[package]]
+name = "serde"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "1.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
+[[package]]
+name = "sized-chunks"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e"
+dependencies = [
+ "bitmaps",
+ "typenum",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
+
+[[package]]
+name = "smol"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1"
+dependencies = [
+ "async-channel 1.9.0",
+ "async-executor",
+ "async-fs",
+ "async-io 1.13.0",
+ "async-lock 2.8.0",
+ "async-net",
+ "async-process",
+ "blocking",
+ "futures-lite 1.13.0",
+]
+
+[[package]]
+name = "smtp-message"
+version = "0.1.0"
+source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#0560e7c46af752344a3095add5f84b02400b1111"
+dependencies = [
+ "auto_enums",
+ "futures",
+ "idna 0.2.3",
+ "lazy_static",
+ "nom 6.1.2",
+ "pin-project",
+ "regex-automata 0.1.10",
+ "serde",
+]
+
+[[package]]
+name = "smtp-server"
+version = "0.1.0"
+source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#0560e7c46af752344a3095add5f84b02400b1111"
+dependencies = [
+ "async-trait",
+ "chrono",
+ "duplexify",
+ "futures",
+ "smol",
+ "smtp-message",
+ "smtp-server-types",
+]
+
+[[package]]
+name = "smtp-server-types"
+version = "0.1.0"
+source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#0560e7c46af752344a3095add5f84b02400b1111"
+dependencies = [
+ "serde",
+ "smtp-message",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "sodiumoxide"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028"
+dependencies = [
+ "ed25519",
+ "libc",
+ "libsodium-sys",
+ "serde",
+]
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
+name = "spki"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "subtle"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "synstructure"
+version = "0.12.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
+
+[[package]]
+name = "thiserror"
+version = "1.0.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "time"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+
+[[package]]
+name = "time-macros"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "num_cpus",
+ "pin-project-lite 0.2.13",
+ "signal-hook-registry",
+ "socket2 0.5.6",
+ "tokio-macros",
+ "tracing",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-io-timeout"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
+dependencies = [
+ "pin-project-lite 0.2.13",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.23.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
+dependencies = [
+ "rustls 0.20.9",
+ "tokio",
+ "webpki",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
+dependencies = [
+ "rustls 0.21.10",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
+dependencies = [
+ "rustls 0.22.2",
+ "rustls-pki-types",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
+dependencies = [
+ "futures-core",
+ "pin-project-lite 0.2.13",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "pin-project-lite 0.2.13",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "tonic"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "axum",
+ "base64 0.21.7",
+ "bytes",
+ "h2 0.3.24",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "hyper 0.14.28",
+ "hyper-timeout",
+ "percent-encoding",
+ "pin-project",
+ "prost",
+ "tokio",
+ "tokio-stream",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "indexmap 1.9.3",
+ "pin-project",
+ "pin-project-lite 0.2.13",
+ "rand",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "log",
+ "pin-project-lite 0.2.13",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+dependencies = [
+ "form_urlencoded",
+ "idna 0.5.0",
+ "percent-encoding",
+]
+
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
+[[package]]
+name = "uuid"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+
+[[package]]
+name = "value-bag"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "126e423afe2dd9ac52142e7e9d5ce4135d7e13776c529d27fd6bc49f19e3280b"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "vsimd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
+
+[[package]]
+name = "waker-fn"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
+[[package]]
+name = "web-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
+dependencies = [
+ "ring 0.17.8",
+ "untrusted 0.9.0",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.4",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.4",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.4",
+ "windows_aarch64_msvc 0.52.4",
+ "windows_i686_gnu 0.52.4",
+ "windows_i686_msvc 0.52.4",
+ "windows_x86_64_gnu 0.52.4",
+ "windows_x86_64_gnullvm 0.52.4",
+ "windows_x86_64_msvc 0.52.4",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
+
+[[package]]
+name = "wyz"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
+
+[[package]]
+name = "x509-parser"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb9bace5b5589ffead1afb76e43e34cff39cd0f3ce7e170ae0c29e53b88eb1c"
+dependencies = [
+ "asn1-rs",
+ "base64 0.13.1",
+ "data-encoding",
+ "der-parser",
+ "lazy_static",
+ "nom 7.1.3",
+ "oid-registry",
+ "rusticata-macros",
+ "thiserror",
+ "time",
+]
+
+[[package]]
+name = "xmlparser"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
+
+[[package]]
+name = "zeroize"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
+
+[[package]]
+name = "zstd"
+version = "0.9.2+zstd.1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "4.1.3+zstd.1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79"
+dependencies = [
+ "libc",
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "1.6.2+zstd.1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f"
+dependencies = [
+ "cc",
+ "libc",
+]
diff --git a/aero-dav/fuzz/Cargo.toml b/aero-dav/fuzz/Cargo.toml
new file mode 100644
index 0000000..a450853
--- /dev/null
+++ b/aero-dav/fuzz/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "aerogramme-fuzz"
+version = "0.0.0"
+publish = false
+edition = "2021"
+
+[package.metadata]
+cargo-fuzz = true
+
+[dependencies]
+arbitrary = { version = "1", optional = true, features = ["derive"] }
+libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] }
+tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
+quick-xml = { version = "0.31", features = ["async-tokio"] }
+
+[dependencies.aero-dav]
+path = ".."
+
+[[bin]]
+name = "dav"
+path = "fuzz_targets/dav.rs"
+test = false
+doc = false
+bench = false
diff --git a/aero-dav/fuzz/dav.dict b/aero-dav/fuzz/dav.dict
new file mode 100644
index 0000000..3ef5b69
--- /dev/null
+++ b/aero-dav/fuzz/dav.dict
@@ -0,0 +1,126 @@
+#
+# AFL dictionary for XML
+# ----------------------
+#
+# Several basic syntax elements and attributes, modeled on libxml2.
+#
+# Created by Michal Zalewski
+#
+
+attr_encoding=" encoding=\"1\""
+attr_generic=" a=\"1\""
+attr_href=" href=\"1\""
+attr_standalone=" standalone=\"no\""
+attr_version=" version=\"1\""
+attr_xml_base=" xml:base=\"1\""
+attr_xml_id=" xml:id=\"1\""
+attr_xml_lang=" xml:lang=\"1\""
+attr_xml_space=" xml:space=\"1\""
+attr_xmlns=" xmlns=\"1\""
+
+entity_builtin="&lt;"
+entity_decimal="&#1;"
+entity_external="&a;"
+entity_hex="&#x1;"
+
+string_any="ANY"
+string_brackets="[]"
+string_cdata="CDATA"
+string_col_fallback=":fallback"
+string_col_generic=":a"
+string_col_include=":include"
+string_dashes="--"
+string_empty="EMPTY"
+string_empty_dblquotes="\"\""
+string_empty_quotes="''"
+string_entities="ENTITIES"
+string_entity="ENTITY"
+string_fixed="#FIXED"
+string_id="ID"
+string_idref="IDREF"
+string_idrefs="IDREFS"
+string_implied="#IMPLIED"
+string_nmtoken="NMTOKEN"
+string_nmtokens="NMTOKENS"
+string_notation="NOTATION"
+string_parentheses="()"
+string_pcdata="#PCDATA"
+string_percent="%a"
+string_public="PUBLIC"
+string_required="#REQUIRED"
+string_schema=":schema"
+string_system="SYSTEM"
+string_ucs4="UCS-4"
+string_utf16="UTF-16"
+string_utf8="UTF-8"
+string_xmlns="xmlns:"
+
+tag_attlist="<!ATTLIST"
+tag_cdata="<![CDATA["
+tag_close="</a>"
+tag_doctype="<!DOCTYPE"
+tag_element="<!ELEMENT"
+tag_entity="<!ENTITY"
+tag_ignore="<![IGNORE["
+tag_include="<![INCLUDE["
+tag_notation="<!NOTATION"
+tag_open="<a>"
+tag_open_close="<a />"
+tag_open_exclamation="<!"
+tag_open_q="<?"
+tag_sq2_close="]]>"
+tag_xml_q="<?xml?>"
+
+"0"
+"1"
+"activelock"
+"allprop"
+"cannot-modify-protected-property"
+"collection"
+"creationdate"
+"DAV:"
+"depth"
+"displayname"
+"error"
+"exclusive"
+"getcontentlanguage"
+"getcontentlength"
+"getcontenttype"
+"getetag"
+"getlastmodified"
+"href"
+"include"
+"Infinite"
+"infinity"
+"location"
+"lockdiscovery"
+"lockentry"
+"lockinfo"
+"lockroot"
+"lockscope"
+"locktoken"
+"lock-token-matches-request-uri"
+"lock-token-submitted"
+"locktype"
+"multistatus"
+"no-conflicting-lock"
+"no-external-entities"
+"owner"
+"preserved-live-properties"
+"prop"
+"propertyupdate"
+"propfind"
+"propfind-finite-depth"
+"propname"
+"propstat"
+"remove"
+"resourcetype"
+"response"
+"responsedescription"
+"set"
+"shared"
+"status"
+"supportedlock"
+"text/html"
+"timeout"
+"write"
diff --git a/aero-dav/fuzz/fuzz_targets/dav.rs b/aero-dav/fuzz/fuzz_targets/dav.rs
new file mode 100644
index 0000000..a303401
--- /dev/null
+++ b/aero-dav/fuzz/fuzz_targets/dav.rs
@@ -0,0 +1,209 @@
+#![no_main]
+
+use libfuzzer_sys::arbitrary;
+use libfuzzer_sys::arbitrary::Arbitrary;
+use libfuzzer_sys::fuzz_target;
+
+use aero_dav::{realization, types, xml};
+use quick_xml::reader::NsReader;
+use tokio::io::AsyncWriteExt;
+use tokio::runtime::Runtime;
+
+// Split this file
+const tokens: [&str; 63] = [
+ "0",
+ "1",
+ "activelock",
+ "allprop",
+ "encoding",
+ "utf-8",
+ "http://ns.example.com/boxschema/",
+ "HTTP/1.1 200 OK",
+ "1997-12-01T18:27:21-08:00",
+ "Mon, 12 Jan 1998 09:25:56 GMT",
+ "\"abcdef\"",
+ "cannot-modify-protected-property",
+ "collection",
+ "creationdate",
+ "DAV:",
+ "D",
+ "C",
+ "xmlns:D",
+ "depth",
+ "displayname",
+ "error",
+ "exclusive",
+ "getcontentlanguage",
+ "getcontentlength",
+ "getcontenttype",
+ "getetag",
+ "getlastmodified",
+ "href",
+ "include",
+ "Infinite",
+ "infinity",
+ "location",
+ "lockdiscovery",
+ "lockentry",
+ "lockinfo",
+ "lockroot",
+ "lockscope",
+ "locktoken",
+ "lock-token-matches-request-uri",
+ "lock-token-submitted",
+ "locktype",
+ "multistatus",
+ "no-conflicting-lock",
+ "no-external-entities",
+ "owner",
+ "preserved-live-properties",
+ "prop",
+ "propertyupdate",
+ "propfind",
+ "propfind-finite-depth",
+ "propname",
+ "propstat",
+ "remove",
+ "resourcetype",
+ "response",
+ "responsedescription",
+ "set",
+ "shared",
+ "status",
+ "supportedlock",
+ "text/html",
+ "timeout",
+ "write",
+];
+
+#[derive(Arbitrary)]
+enum Token {
+ Known(usize),
+ //Unknown(String),
+}
+impl Token {
+ fn serialize(&self) -> String {
+ match self {
+ Self::Known(i) => tokens[i % tokens.len()].to_string(),
+ //Self::Unknown(v) => v.to_string(),
+ }
+ }
+}
+
+#[derive(Arbitrary)]
+struct Tag {
+ //prefix: Option<Token>,
+ name: Token,
+ attr: Option<(Token, Token)>,
+}
+impl Tag {
+ fn start(&self) -> String {
+ let mut acc = String::new();
+ /*if let Some(p) = &self.prefix {
+ acc.push_str(p.serialize().as_str());
+ acc.push_str(":");
+ }*/
+ acc.push_str("D:");
+ acc.push_str(self.name.serialize().as_str());
+
+ if let Some((k, v)) = &self.attr {
+ acc.push_str(" ");
+ acc.push_str(k.serialize().as_str());
+ acc.push_str("=\"");
+ acc.push_str(v.serialize().as_str());
+ acc.push_str("\"");
+ }
+ acc
+ }
+ fn end(&self) -> String {
+ let mut acc = String::new();
+ acc.push_str("D:");
+ acc.push_str(self.name.serialize().as_str());
+ acc
+ }
+}
+
+#[derive(Arbitrary)]
+enum XmlNode {
+ //@FIXME: build RFC3339 and RFC822 Dates with chrono based on timestamps
+ //@FIXME: add small numbers
+ //@FIXME: add http status code
+ Node(Tag, Vec<Self>),
+ Number(u64),
+ Text(Token),
+}
+impl std::fmt::Debug for XmlNode {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.serialize())
+ }
+}
+impl XmlNode {
+ fn serialize(&self) -> String {
+ match self {
+ Self::Node(tag, children) => {
+ let stag = tag.start();
+ match children.is_empty() {
+ true => format!("<{}/>", stag),
+ false => format!(
+ "<{}>{}</{}>",
+ stag,
+ children.iter().map(|v| v.serialize()).collect::<String>(),
+ tag.end()
+ ),
+ }
+ }
+ Self::Number(v) => format!("{}", v),
+ Self::Text(v) => v.serialize(),
+ }
+ }
+}
+
+async fn serialize(elem: &impl xml::QWrite) -> Vec<u8> {
+ let mut buffer = Vec::new();
+ let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
+ let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
+ let ns_to_apply = vec![("xmlns:D".into(), "DAV:".into())];
+ let mut writer = xml::Writer { q, ns_to_apply };
+
+ elem.qwrite(&mut writer).await.expect("xml serialization");
+ tokio_buffer.flush().await.expect("tokio buffer flush");
+
+ return buffer;
+}
+
+type Object = types::Multistatus<realization::Core, types::PropValue<realization::Core>>;
+
+fuzz_target!(|nodes: XmlNode| {
+ let gen = format!(
+ "<D:multistatus xmlns:D=\"DAV:\">{}<D:/multistatus>",
+ nodes.serialize()
+ );
+ //println!("--------\n{}", gen);
+ let data = gen.as_bytes();
+
+ let rt = Runtime::new().expect("tokio runtime initialization");
+
+ rt.block_on(async {
+ // 1. Setup fuzzing by finding an input that seems correct, do not crash yet then.
+ let mut rdr = match xml::Reader::new(NsReader::from_reader(data)).await {
+ Err(_) => return,
+ Ok(r) => r,
+ };
+ let reference = match rdr.find::<Object>().await {
+ Err(_) => return,
+ Ok(m) => m,
+ };
+
+ // 2. Re-serialize the input
+ let my_serialization = serialize(&reference).await;
+
+ // 3. De-serialize my serialization
+ let mut rdr2 = xml::Reader::new(NsReader::from_reader(my_serialization.as_slice()))
+ .await
+ .expect("XML Reader init");
+ let comparison = rdr2.find::<Object>().await.expect("Deserialize again");
+
+ // 4. Both the first decoding and last decoding must be identical
+ assert_eq!(reference, comparison);
+ })
+});
diff --git a/aero-dav/src/acldecoder.rs b/aero-dav/src/acldecoder.rs
new file mode 100644
index 0000000..405286e
--- /dev/null
+++ b/aero-dav/src/acldecoder.rs
@@ -0,0 +1,84 @@
+use super::acltypes::*;
+use super::error::ParsingError;
+use super::types as dav;
+use super::xml::{IRead, QRead, Reader, DAV_URN};
+
+impl QRead<Property> for Property {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open_start(DAV_URN, "owner").await?.is_some() {
+ let href = xml.find().await?;
+ xml.close().await?;
+ return Ok(Self::Owner(href));
+ }
+ if xml
+ .maybe_open_start(DAV_URN, "current-user-principal")
+ .await?
+ .is_some()
+ {
+ let user = xml.find().await?;
+ xml.close().await?;
+ return Ok(Self::CurrentUserPrincipal(user));
+ }
+ if xml
+ .maybe_open_start(DAV_URN, "current-user-privilege-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CurrentUserPrivilegeSet(vec![]));
+ }
+
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "owner").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Owner);
+ }
+
+ if xml
+ .maybe_open(DAV_URN, "current-user-principal")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CurrentUserPrincipal);
+ }
+
+ if xml
+ .maybe_open(DAV_URN, "current-user-privilege-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CurrentUserPrivilegeSet);
+ }
+
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<ResourceType> for ResourceType {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "principal").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Principal);
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+// -----
+impl QRead<User> for User {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "unauthenticated").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Unauthenticated);
+ }
+
+ dav::Href::qread(xml).await.map(Self::Authenticated)
+ }
+}
diff --git a/aero-dav/src/aclencoder.rs b/aero-dav/src/aclencoder.rs
new file mode 100644
index 0000000..28c01a7
--- /dev/null
+++ b/aero-dav/src/aclencoder.rs
@@ -0,0 +1,71 @@
+use quick_xml::events::Event;
+use quick_xml::Error as QError;
+
+use super::acltypes::*;
+use super::error::ParsingError;
+use super::xml::{IWrite, QWrite, Writer};
+
+impl QWrite for Property {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Owner(href) => {
+ let start = xml.create_dav_element("owner");
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ href.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::CurrentUserPrincipal(user) => {
+ let start = xml.create_dav_element("current-user-principal");
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ user.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::CurrentUserPrivilegeSet(_) => {
+ let empty_tag = xml.create_dav_element("current-user-privilege-set");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ }
+ }
+}
+
+impl QWrite for PropertyRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut atom = async |c| {
+ let empty_tag = xml.create_dav_element(c);
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ };
+
+ match self {
+ Self::Owner => atom("owner").await,
+ Self::CurrentUserPrincipal => atom("current-user-principal").await,
+ Self::CurrentUserPrivilegeSet => atom("current-user-privilege-set").await,
+ }
+ }
+}
+
+impl QWrite for ResourceType {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Principal => {
+ let empty_tag = xml.create_dav_element("principal");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ }
+ }
+}
+
+// -----
+
+impl QWrite for User {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Unauthenticated => {
+ let tag = xml.create_dav_element("unauthenticated");
+ xml.q.write_event_async(Event::Empty(tag)).await
+ }
+ Self::Authenticated(href) => href.qwrite(xml).await,
+ }
+ }
+}
diff --git a/aero-dav/src/acltypes.rs b/aero-dav/src/acltypes.rs
new file mode 100644
index 0000000..0af3c8a
--- /dev/null
+++ b/aero-dav/src/acltypes.rs
@@ -0,0 +1,38 @@
+use super::types as dav;
+
+//RFC covered: RFC3744 (ACL core) + RFC5397 (ACL Current Principal Extension)
+
+//@FIXME required for a full CalDAV implementation
+// See section 6. of the CalDAV RFC
+// It seems mainly required for free-busy that I will not implement now.
+// It can also be used for discovering main calendar, not sure it is used.
+// Note: it is used by Thunderbird
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ Owner,
+ CurrentUserPrincipal,
+ CurrentUserPrivilegeSet,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property {
+ Owner(dav::Href),
+ CurrentUserPrincipal(User),
+ CurrentUserPrivilegeSet(Vec<Privilege>),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ResourceType {
+ Principal,
+}
+
+/// Not implemented, it's a placeholder
+#[derive(Debug, PartialEq, Clone)]
+pub struct Privilege(());
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum User {
+ Unauthenticated,
+ Authenticated(dav::Href),
+}
diff --git a/aero-dav/src/caldecoder.rs b/aero-dav/src/caldecoder.rs
new file mode 100644
index 0000000..9ed783a
--- /dev/null
+++ b/aero-dav/src/caldecoder.rs
@@ -0,0 +1,1421 @@
+use chrono::NaiveDateTime;
+use quick_xml::events::Event;
+
+use super::caltypes::*;
+use super::error::ParsingError;
+use super::types as dav;
+use super::xml::{IRead, QRead, Reader, CAL_URN, DAV_URN};
+
+// ---- ROOT ELEMENTS ---
+impl<E: dav::Extension> QRead<MkCalendar<E>> for MkCalendar<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "mkcalendar").await?;
+ let set = xml.find().await?;
+ xml.close().await?;
+ Ok(MkCalendar(set))
+ }
+}
+
+impl<E: dav::Extension> QRead<MkCalendarResponse<E>> for MkCalendarResponse<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "mkcalendar-response").await?;
+ let propstats = xml.collect().await?;
+ xml.close().await?;
+ Ok(MkCalendarResponse(propstats))
+ }
+}
+
+impl<E: dav::Extension> QRead<ReportType<E>> for ReportType<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ match CalendarQuery::<E>::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Self::Query),
+ }
+
+ match CalendarMultiget::<E>::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Self::Multiget),
+ }
+
+ FreeBusyQuery::qread(xml).await.map(Self::FreeBusy)
+ }
+}
+
+impl<E: dav::Extension> QRead<CalendarQuery<E>> for CalendarQuery<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-query").await?;
+ let (mut selector, mut filter, mut timezone) = (None, None, None);
+ loop {
+ let mut dirty = false;
+ xml.maybe_read(&mut selector, &mut dirty).await?;
+ xml.maybe_read(&mut filter, &mut dirty).await?;
+ xml.maybe_read(&mut timezone, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+ xml.close().await?;
+
+ match filter {
+ Some(filter) => Ok(CalendarQuery {
+ selector,
+ filter,
+ timezone,
+ }),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+impl<E: dav::Extension> QRead<CalendarMultiget<E>> for CalendarMultiget<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-multiget").await?;
+ let mut selector = None;
+ let mut href = Vec::new();
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read(&mut selector, &mut dirty).await?;
+ xml.maybe_push(&mut href, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ Ok(CalendarMultiget { selector, href })
+ }
+}
+
+impl QRead<FreeBusyQuery> for FreeBusyQuery {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "free-busy-query").await?;
+ let range = xml.find().await?;
+ xml.close().await?;
+ Ok(FreeBusyQuery(range))
+ }
+}
+
+impl QRead<ReportTypeName> for ReportTypeName {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "calendar-query").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Query);
+ }
+ if xml
+ .maybe_open(DAV_URN, "calendar-multiget")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::Multiget);
+ }
+ if xml.maybe_open(DAV_URN, "free-busy-query").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::FreeBusy);
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+// ---- EXTENSIONS ---
+impl QRead<Violation> for Violation {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open(DAV_URN, "resource-must-be-null")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::ResourceMustBeNull)
+ } else if xml.maybe_open(DAV_URN, "need-privileges").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::NeedPrivileges)
+ } else if xml
+ .maybe_open(CAL_URN, "calendar-collection-location-ok")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::CalendarCollectionLocationOk)
+ } else if xml
+ .maybe_open(CAL_URN, "valid-calendar-data")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::ValidCalendarData)
+ } else if xml
+ .maybe_open(CAL_URN, "initialize-calendar-collection")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::InitializeCalendarCollection)
+ } else if xml
+ .maybe_open(CAL_URN, "supported-calendar-data")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::SupportedCalendarData)
+ } else if xml
+ .maybe_open(CAL_URN, "valid-calendar-object-resource")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::ValidCalendarObjectResource)
+ } else if xml
+ .maybe_open(CAL_URN, "supported-calendar-component")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::SupportedCalendarComponent)
+ } else if xml.maybe_open(CAL_URN, "no-uid-conflict").await?.is_some() {
+ let href = xml.find().await?;
+ xml.close().await?;
+ Ok(Self::NoUidConflict(href))
+ } else if xml
+ .maybe_open(CAL_URN, "max-resource-size")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::MaxResourceSize)
+ } else if xml.maybe_open(CAL_URN, "min-date-time").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::MinDateTime)
+ } else if xml.maybe_open(CAL_URN, "max-date-time").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::MaxDateTime)
+ } else if xml.maybe_open(CAL_URN, "max-instances").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::MaxInstances)
+ } else if xml
+ .maybe_open(CAL_URN, "max-attendees-per-instance")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::MaxAttendeesPerInstance)
+ } else if xml.maybe_open(CAL_URN, "valid-filter").await?.is_some() {
+ xml.close().await?;
+ Ok(Self::ValidFilter)
+ } else if xml.maybe_open(CAL_URN, "supported-filter").await?.is_some() {
+ let (mut comp, mut prop, mut param) = (Vec::new(), Vec::new(), Vec::new());
+ loop {
+ let mut dirty = false;
+ xml.maybe_push(&mut comp, &mut dirty).await?;
+ xml.maybe_push(&mut prop, &mut dirty).await?;
+ xml.maybe_push(&mut param, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+ xml.close().await?;
+ Ok(Self::SupportedFilter { comp, prop, param })
+ } else if xml
+ .maybe_open(CAL_URN, "number-of-matches-within-limits")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Self::NumberOfMatchesWithinLimits)
+ } else {
+ Err(ParsingError::Recoverable)
+ }
+ }
+}
+
+impl QRead<Property> for Property {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open_start(CAL_URN, "calendar-home-set")
+ .await?
+ .is_some()
+ {
+ let href = xml.find().await?;
+ xml.close().await?;
+ return Ok(Property::CalendarHomeSet(href));
+ }
+ if xml
+ .maybe_open_start(CAL_URN, "calendar-description")
+ .await?
+ .is_some()
+ {
+ let lang = xml.prev_attr("xml:lang");
+ let text = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::CalendarDescription { lang, text });
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "calendar-timezone")
+ .await?
+ .is_some()
+ {
+ let tz = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::CalendarTimezone(tz));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "supported-calendar-component-set")
+ .await?
+ .is_some()
+ {
+ let comp = xml.collect().await?;
+ xml.close().await?;
+ return Ok(Property::SupportedCalendarComponentSet(comp));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "supported-calendar-data")
+ .await?
+ .is_some()
+ {
+ let mime = xml.collect().await?;
+ xml.close().await?;
+ return Ok(Property::SupportedCalendarData(mime));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "max-resource-size")
+ .await?
+ .is_some()
+ {
+ let sz = xml.tag_string().await?.parse::<u64>()?;
+ xml.close().await?;
+ return Ok(Property::MaxResourceSize(sz));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "max-date-time")
+ .await?
+ .is_some()
+ {
+ let dtstr = xml.tag_string().await?;
+ let dt = NaiveDateTime::parse_from_str(dtstr.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ xml.close().await?;
+ return Ok(Property::MaxDateTime(dt));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "max-instances")
+ .await?
+ .is_some()
+ {
+ let sz = xml.tag_string().await?.parse::<u64>()?;
+ xml.close().await?;
+ return Ok(Property::MaxInstances(sz));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "max-attendees-per-instance")
+ .await?
+ .is_some()
+ {
+ let sz = xml.tag_string().await?.parse::<u64>()?;
+ xml.close().await?;
+ return Ok(Property::MaxAttendeesPerInstance(sz));
+ }
+
+ if xml
+ .maybe_open_start(CAL_URN, "supported-collation-set")
+ .await?
+ .is_some()
+ {
+ let cols = xml.collect().await?;
+ xml.close().await?;
+ return Ok(Property::SupportedCollationSet(cols));
+ }
+
+ let mut dirty = false;
+ let mut caldata: Option<CalendarDataPayload> = None;
+ xml.maybe_read(&mut caldata, &mut dirty).await?;
+ if let Some(cal) = caldata {
+ return Ok(Property::CalendarData(cal));
+ }
+
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open(CAL_URN, "calendar-home-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CalendarHomeSet);
+ }
+ if xml
+ .maybe_open(CAL_URN, "calendar-description")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CalendarDescription);
+ }
+ if xml
+ .maybe_open(CAL_URN, "calendar-timezone")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::CalendarTimezone);
+ }
+ if xml
+ .maybe_open(CAL_URN, "supported-calendar-component-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::SupportedCalendarComponentSet);
+ }
+ if xml
+ .maybe_open(CAL_URN, "supported-calendar-data")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::SupportedCalendarData);
+ }
+ if xml
+ .maybe_open(CAL_URN, "max-resource-size")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::MaxResourceSize);
+ }
+ if xml.maybe_open(CAL_URN, "min-date-time").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::MinDateTime);
+ }
+ if xml.maybe_open(CAL_URN, "max-date-time").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::MaxDateTime);
+ }
+ if xml.maybe_open(CAL_URN, "max-instances").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::MaxInstances);
+ }
+ if xml
+ .maybe_open(CAL_URN, "max-attendees-per-instance")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::MaxAttendeesPerInstance);
+ }
+ if xml
+ .maybe_open(CAL_URN, "supported-collation-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::SupportedCollationSet);
+ }
+ let mut dirty = false;
+ let mut m_cdr = None;
+ xml.maybe_read(&mut m_cdr, &mut dirty).await?;
+ m_cdr
+ .ok_or(ParsingError::Recoverable)
+ .map(Self::CalendarData)
+ }
+}
+
+impl QRead<ResourceType> for ResourceType {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(CAL_URN, "calendar").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::Calendar);
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+// ---- INNER XML ----
+impl QRead<SupportedCollation> for SupportedCollation {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "supported-collation").await?;
+ let col = Collation::new(xml.tag_string().await?);
+ xml.close().await?;
+ Ok(SupportedCollation(col))
+ }
+}
+
+impl QRead<CalendarDataPayload> for CalendarDataPayload {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-data").await?;
+ let mime = CalendarDataSupport::qread(xml).await.ok();
+ let payload = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(CalendarDataPayload { mime, payload })
+ }
+}
+
+impl QRead<CalendarDataSupport> for CalendarDataSupport {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let ct = xml.prev_attr("content-type");
+ let vs = xml.prev_attr("version");
+ match (ct, vs) {
+ (Some(content_type), Some(version)) => Ok(Self {
+ content_type,
+ version,
+ }),
+ _ => Err(ParsingError::Recoverable),
+ }
+ }
+}
+
+impl QRead<CalendarDataRequest> for CalendarDataRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-data").await?;
+ let mime = CalendarDataSupport::qread(xml).await.ok();
+ let (mut comp, mut recurrence, mut limit_freebusy_set) = (None, None, None);
+
+ if !xml.parent_has_child() {
+ return Ok(Self {
+ mime,
+ comp,
+ recurrence,
+ limit_freebusy_set,
+ });
+ }
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read(&mut comp, &mut dirty).await?;
+ xml.maybe_read(&mut recurrence, &mut dirty).await?;
+ xml.maybe_read(&mut limit_freebusy_set, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ Ok(Self {
+ mime,
+ comp,
+ recurrence,
+ limit_freebusy_set,
+ })
+ }
+}
+
+impl QRead<CalendarDataEmpty> for CalendarDataEmpty {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "calendar-data").await?;
+ let mime = CalendarDataSupport::qread(xml).await.ok();
+ xml.close().await?;
+ Ok(Self(mime))
+ }
+}
+
+impl QRead<Comp> for Comp {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let (mut prop_kind, mut comp_kind) = (None, None);
+
+ let bs = xml.open(CAL_URN, "comp").await?;
+ let name = Component::new(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+
+ // Return early if it's an empty tag
+ if matches!(bs, Event::Empty(_)) {
+ xml.close().await?;
+ return Ok(Self {
+ name,
+ prop_kind,
+ comp_kind,
+ });
+ }
+
+ loop {
+ let mut dirty = false;
+ let (mut tmp_prop_kind, mut tmp_comp_kind): (Option<PropKind>, Option<CompKind>) =
+ (None, None);
+
+ xml.maybe_read(&mut tmp_prop_kind, &mut dirty).await?;
+ Box::pin(xml.maybe_read(&mut tmp_comp_kind, &mut dirty)).await?;
+
+ //@FIXME hack
+ // Merge
+ match (tmp_prop_kind, &mut prop_kind) {
+ (Some(PropKind::Prop(mut a)), Some(PropKind::Prop(ref mut b))) => b.append(&mut a),
+ (Some(PropKind::AllProp), v) => *v = Some(PropKind::AllProp),
+ (Some(x), b) => *b = Some(x),
+ (None, _) => (),
+ };
+ match (tmp_comp_kind, &mut comp_kind) {
+ (Some(CompKind::Comp(mut a)), Some(CompKind::Comp(ref mut b))) => b.append(&mut a),
+ (Some(CompKind::AllComp), v) => *v = Some(CompKind::AllComp),
+ (Some(a), b) => *b = Some(a),
+ (None, _) => (),
+ };
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ Ok(Self {
+ name,
+ prop_kind,
+ comp_kind,
+ })
+ }
+}
+
+impl QRead<CompSupport> for CompSupport {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "comp").await?;
+ let inner = Component::new(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ xml.close().await?;
+ Ok(Self(inner))
+ }
+}
+
+impl QRead<CompKind> for CompKind {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut comp = Vec::new();
+ loop {
+ let mut dirty = false;
+
+ if xml.maybe_open(CAL_URN, "allcomp").await?.is_some() {
+ xml.close().await?;
+ return Ok(CompKind::AllComp);
+ }
+
+ xml.maybe_push(&mut comp, &mut dirty).await?;
+
+ if !dirty {
+ break;
+ }
+ }
+ match &comp[..] {
+ [] => Err(ParsingError::Recoverable),
+ _ => Ok(CompKind::Comp(comp)),
+ }
+ }
+}
+
+impl QRead<PropKind> for PropKind {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut prop = Vec::new();
+ loop {
+ let mut dirty = false;
+
+ if xml.maybe_open(CAL_URN, "allprop").await?.is_some() {
+ xml.close().await?;
+ return Ok(PropKind::AllProp);
+ }
+
+ xml.maybe_push(&mut prop, &mut dirty).await?;
+
+ if !dirty {
+ break;
+ }
+ }
+
+ match &prop[..] {
+ [] => Err(ParsingError::Recoverable),
+ _ => Ok(PropKind::Prop(prop)),
+ }
+ }
+}
+
+impl QRead<RecurrenceModifier> for RecurrenceModifier {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ match Expand::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(RecurrenceModifier::Expand),
+ }
+ LimitRecurrenceSet::qread(xml)
+ .await
+ .map(RecurrenceModifier::LimitRecurrenceSet)
+ }
+}
+
+impl QRead<Expand> for Expand {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "expand").await?;
+ let (rstart, rend) = match (xml.prev_attr("start"), xml.prev_attr("end")) {
+ (Some(start), Some(end)) => (start, end),
+ _ => return Err(ParsingError::MissingAttribute),
+ };
+
+ let start = NaiveDateTime::parse_from_str(rstart.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ let end = NaiveDateTime::parse_from_str(rend.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ if start > end {
+ return Err(ParsingError::InvalidValue);
+ }
+
+ xml.close().await?;
+ Ok(Expand(start, end))
+ }
+}
+
+impl QRead<LimitRecurrenceSet> for LimitRecurrenceSet {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "limit-recurrence-set").await?;
+ let (rstart, rend) = match (xml.prev_attr("start"), xml.prev_attr("end")) {
+ (Some(start), Some(end)) => (start, end),
+ _ => return Err(ParsingError::MissingAttribute),
+ };
+
+ let start = NaiveDateTime::parse_from_str(rstart.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ let end = NaiveDateTime::parse_from_str(rend.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ if start > end {
+ return Err(ParsingError::InvalidValue);
+ }
+
+ xml.close().await?;
+ Ok(LimitRecurrenceSet(start, end))
+ }
+}
+
+impl QRead<LimitFreebusySet> for LimitFreebusySet {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "limit-freebusy-set").await?;
+ let (rstart, rend) = match (xml.prev_attr("start"), xml.prev_attr("end")) {
+ (Some(start), Some(end)) => (start, end),
+ _ => return Err(ParsingError::MissingAttribute),
+ };
+
+ let start = NaiveDateTime::parse_from_str(rstart.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ let end = NaiveDateTime::parse_from_str(rend.as_str(), UTC_DATETIME_FMT)?.and_utc();
+ if start > end {
+ return Err(ParsingError::InvalidValue);
+ }
+
+ xml.close().await?;
+ Ok(LimitFreebusySet(start, end))
+ }
+}
+
+impl<E: dav::Extension> QRead<CalendarSelector<E>> for CalendarSelector<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ // allprop
+ if let Some(_) = xml.maybe_open(DAV_URN, "allprop").await? {
+ xml.close().await?;
+ return Ok(Self::AllProp);
+ }
+
+ // propname
+ if let Some(_) = xml.maybe_open(DAV_URN, "propname").await? {
+ xml.close().await?;
+ return Ok(Self::PropName);
+ }
+
+ // prop
+ let (mut maybe_prop, mut dirty) = (None, false);
+ xml.maybe_read::<dav::PropName<E>>(&mut maybe_prop, &mut dirty)
+ .await?;
+ if let Some(prop) = maybe_prop {
+ return Ok(Self::Prop(prop));
+ }
+
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<CompFilter> for CompFilter {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "comp-filter").await?;
+ let name = Component::new(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ let additional_rules = Box::pin(xml.maybe_find()).await?;
+ xml.close().await?;
+ Ok(Self {
+ name,
+ additional_rules,
+ })
+ }
+}
+
+impl QRead<CompFilterRules> for CompFilterRules {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut time_range = None;
+ let mut prop_filter = Vec::new();
+ let mut comp_filter = Vec::new();
+
+ loop {
+ let mut dirty = false;
+
+ if xml.maybe_open(CAL_URN, "is-not-defined").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::IsNotDefined);
+ }
+
+ xml.maybe_read(&mut time_range, &mut dirty).await?;
+ xml.maybe_push(&mut prop_filter, &mut dirty).await?;
+ xml.maybe_push(&mut comp_filter, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ match (&time_range, &prop_filter[..], &comp_filter[..]) {
+ (None, [], []) => Err(ParsingError::Recoverable),
+ _ => Ok(Self::Matches(CompFilterMatch {
+ time_range,
+ prop_filter,
+ comp_filter,
+ })),
+ }
+ }
+}
+
+impl QRead<CompFilterMatch> for CompFilterMatch {
+ async fn qread(_xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ unreachable!();
+ }
+}
+
+impl QRead<PropFilter> for PropFilter {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "prop-filter").await?;
+ let name = ComponentProperty(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ let additional_rules = xml.maybe_find().await?;
+ xml.close().await?;
+ Ok(Self {
+ name,
+ additional_rules,
+ })
+ }
+}
+
+impl QRead<PropFilterRules> for PropFilterRules {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut time_or_text = None;
+ let mut param_filter = Vec::new();
+
+ loop {
+ let mut dirty = false;
+
+ if xml.maybe_open(CAL_URN, "is-not-defined").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::IsNotDefined);
+ }
+
+ xml.maybe_read(&mut time_or_text, &mut dirty).await?;
+ xml.maybe_push(&mut param_filter, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ match (&time_or_text, &param_filter[..]) {
+ (None, []) => Err(ParsingError::Recoverable),
+ _ => Ok(PropFilterRules::Match(PropFilterMatch {
+ time_or_text,
+ param_filter,
+ })),
+ }
+ }
+}
+
+impl QRead<PropFilterMatch> for PropFilterMatch {
+ async fn qread(_xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ unreachable!();
+ }
+}
+
+impl QRead<ParamFilter> for ParamFilter {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "param-filter").await?;
+ let name = PropertyParameter(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ let additional_rules = xml.maybe_find().await?;
+ xml.close().await?;
+ Ok(Self {
+ name,
+ additional_rules,
+ })
+ }
+}
+
+impl QRead<TimeOrText> for TimeOrText {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ match TimeRange::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Self::Time),
+ }
+ TextMatch::qread(xml).await.map(Self::Text)
+ }
+}
+
+impl QRead<TextMatch> for TextMatch {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "text-match").await?;
+ let collation = xml.prev_attr("collation").map(Collation::new);
+ let negate_condition = xml.prev_attr("negate-condition").map(|v| v == "yes");
+ let text = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(Self {
+ collation,
+ negate_condition,
+ text,
+ })
+ }
+}
+
+impl QRead<ParamFilterMatch> for ParamFilterMatch {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(CAL_URN, "is-not-defined").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::IsNotDefined);
+ }
+ TextMatch::qread(xml).await.map(Self::Match)
+ }
+}
+
+impl QRead<TimeZone> for TimeZone {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "timezone").await?;
+ let inner = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(Self(inner))
+ }
+}
+
+impl QRead<Filter> for Filter {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "filter").await?;
+ let comp_filter = xml.find().await?;
+ xml.close().await?;
+ Ok(Self(comp_filter))
+ }
+}
+
+impl QRead<TimeRange> for TimeRange {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "time-range").await?;
+
+ let start = match xml.prev_attr("start") {
+ Some(r) => Some(NaiveDateTime::parse_from_str(r.as_str(), UTC_DATETIME_FMT)?.and_utc()),
+ _ => None,
+ };
+ let end = match xml.prev_attr("end") {
+ Some(r) => Some(NaiveDateTime::parse_from_str(r.as_str(), UTC_DATETIME_FMT)?.and_utc()),
+ _ => None,
+ };
+
+ xml.close().await?;
+
+ match (start, end) {
+ (Some(start), Some(end)) => {
+ if start > end {
+ return Err(ParsingError::InvalidValue);
+ }
+ Ok(TimeRange::FullRange(start, end))
+ }
+ (Some(start), None) => Ok(TimeRange::OnlyStart(start)),
+ (None, Some(end)) => Ok(TimeRange::OnlyEnd(end)),
+ (None, None) => Err(ParsingError::MissingAttribute),
+ }
+ }
+}
+
+impl QRead<CalProp> for CalProp {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(CAL_URN, "prop").await?;
+ let name = ComponentProperty(
+ xml.prev_attr("name")
+ .ok_or(ParsingError::MissingAttribute)?,
+ );
+ let novalue = xml.prev_attr("novalue").map(|v| v == "yes");
+ xml.close().await?;
+ Ok(Self { name, novalue })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::Calendar;
+ use crate::xml::Node;
+ use chrono::{TimeZone, Utc};
+ //use quick_reader::NsReader;
+
+ async fn deserialize<T: Node<T>>(src: &str) -> T {
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn simple_comp_filter() {
+ let expected = CompFilter {
+ name: Component::VEvent,
+ additional_rules: None,
+ };
+ let src = r#"<C:comp-filter name="VEVENT" xmlns:C="urn:ietf:params:xml:ns:caldav" />"#;
+ let got = deserialize::<CompFilter>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ #[tokio::test]
+ async fn basic_mkcalendar() {
+ let expected = MkCalendar(dav::Set(dav::PropValue(vec![dav::Property::DisplayName(
+ "Lisa's Events".into(),
+ )])));
+
+ let src = r#"
+<?xml version="1.0" encoding="utf-8" ?>
+<C:mkcalendar xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:set>
+ <D:prop>
+ <D:displayname>Lisa's Events</D:displayname>
+ </D:prop>
+ </D:set>
+ </C:mkcalendar>
+"#;
+ let got = deserialize::<MkCalendar<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_mkcalendar() {
+ let expected = MkCalendar(dav::Set(dav::PropValue(vec![
+ dav::Property::DisplayName("Lisa's Events".into()),
+ dav::Property::Extension(Property::CalendarDescription {
+ lang: Some("en".into()),
+ text: "Calendar restricted to events.".into(),
+ }),
+ dav::Property::Extension(Property::SupportedCalendarComponentSet(vec![
+ CompSupport(Component::VEvent)
+ ])),
+ dav::Property::Extension(Property::CalendarTimezone("BEGIN:VCALENDAR\nPRODID:-//Example Corp.//CalDAV Client//EN\nVERSION:2.0\nEND:VCALENDAR".into())),
+ ])));
+
+ let src = r#"
+ <?xml version="1.0" encoding="utf-8" ?>
+ <C:mkcalendar xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:set>
+ <D:prop>
+ <D:displayname>Lisa's Events</D:displayname>
+ <C:calendar-description xml:lang="en"
+ >Calendar restricted to events.</C:calendar-description>
+ <C:supported-calendar-component-set>
+ <C:comp name="VEVENT"/>
+ </C:supported-calendar-component-set>
+ <C:calendar-timezone><![CDATA[BEGIN:VCALENDAR
+PRODID:-//Example Corp.//CalDAV Client//EN
+VERSION:2.0
+END:VCALENDAR]]></C:calendar-timezone>
+ </D:prop>
+ </D:set>
+ </C:mkcalendar>"#;
+
+ let got = deserialize::<MkCalendar<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_calendar_query() {
+ let expected = CalendarQuery {
+ selector: Some(CalendarSelector::Prop(dav::PropName(vec![
+ dav::PropertyRequest::GetEtag,
+ dav::PropertyRequest::Extension(PropertyRequest::CalendarData(
+ CalendarDataRequest {
+ mime: None,
+ comp: Some(Comp {
+ name: Component::VCalendar,
+ prop_kind: Some(PropKind::Prop(vec![CalProp {
+ name: ComponentProperty("VERSION".into()),
+ novalue: None,
+ }])),
+ comp_kind: Some(CompKind::Comp(vec![
+ Comp {
+ name: Component::VEvent,
+ prop_kind: Some(PropKind::Prop(vec![
+ CalProp {
+ name: ComponentProperty("SUMMARY".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("UID".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("DTSTART".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("DTEND".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("DURATION".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("RRULE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("RDATE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("EXRULE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("EXDATE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("RECURRENCE-ID".into()),
+ novalue: None,
+ },
+ ])),
+ comp_kind: None,
+ },
+ Comp {
+ name: Component::VTimeZone,
+ prop_kind: None,
+ comp_kind: None,
+ },
+ ])),
+ }),
+ recurrence: None,
+ limit_freebusy_set: None,
+ },
+ )),
+ ]))),
+ filter: Filter(CompFilter {
+ name: Component::VCalendar,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ prop_filter: vec![],
+ comp_filter: vec![CompFilter {
+ name: Component::VEvent,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ prop_filter: vec![],
+ comp_filter: vec![],
+ time_range: Some(TimeRange::FullRange(
+ Utc.with_ymd_and_hms(2006, 1, 4, 0, 0, 0).unwrap(),
+ Utc.with_ymd_and_hms(2006, 1, 5, 0, 0, 0).unwrap(),
+ )),
+ })),
+ }],
+ time_range: None,
+ })),
+ }),
+ timezone: None,
+ };
+
+ let src = r#"
+<?xml version="1.0" encoding="utf-8" ?>
+<C:calendar-query xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop>
+ <D:getetag/>
+ <C:calendar-data>
+ <C:comp name="VCALENDAR">
+ <C:prop name="VERSION"/>
+ <C:comp name="VEVENT">
+ <C:prop name="SUMMARY"/>
+ <C:prop name="UID"/>
+ <C:prop name="DTSTART"/>
+ <C:prop name="DTEND"/>
+ <C:prop name="DURATION"/>
+ <C:prop name="RRULE"/>
+ <C:prop name="RDATE"/>
+ <C:prop name="EXRULE"/>
+ <C:prop name="EXDATE"/>
+ <C:prop name="RECURRENCE-ID"/>
+ </C:comp>
+ <C:comp name="VTIMEZONE"/>
+ </C:comp>
+ </C:calendar-data>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VEVENT">
+ <C:time-range start="20060104T000000Z"
+ end="20060105T000000Z"/>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+</C:calendar-query>
+"#;
+
+ let got = deserialize::<CalendarQuery<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_calendar_query_res() {
+ let expected = dav::Multistatus::<Calendar> {
+ extension: None,
+ responses: vec![
+ dav::Response {
+ status_or_propstat: dav::StatusOrPropstat::PropStat(
+ dav::Href("http://cal.example.com/bernard/work/abcd2.ics".into()),
+ vec![dav::PropStat {
+ prop: dav::AnyProp(vec![
+ dav::AnyProperty::Value(dav::Property::GetEtag(
+ "\"fffff-abcd2\"".into(),
+ )),
+ dav::AnyProperty::Value(dav::Property::Extension(
+ Property::CalendarData(CalendarDataPayload {
+ mime: None,
+ payload: "BEGIN:VCALENDAR".into(),
+ }),
+ )),
+ ]),
+ status: dav::Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ location: None,
+ responsedescription: None,
+ },
+ dav::Response {
+ status_or_propstat: dav::StatusOrPropstat::PropStat(
+ dav::Href("http://cal.example.com/bernard/work/abcd3.ics".into()),
+ vec![dav::PropStat {
+ prop: dav::AnyProp(vec![
+ dav::AnyProperty::Value(dav::Property::GetEtag(
+ "\"fffff-abcd3\"".into(),
+ )),
+ dav::AnyProperty::Value(dav::Property::Extension(
+ Property::CalendarData(CalendarDataPayload {
+ mime: None,
+ payload: "BEGIN:VCALENDAR".into(),
+ }),
+ )),
+ ]),
+ status: dav::Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ location: None,
+ responsedescription: None,
+ },
+ ],
+ responsedescription: None,
+ };
+
+ let src = r#"<D:multistatus xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:response>
+ <D:href>http://cal.example.com/bernard/work/abcd2.ics</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:getetag>"fffff-abcd2"</D:getetag>
+ <C:calendar-data>BEGIN:VCALENDAR</C:calendar-data>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>http://cal.example.com/bernard/work/abcd3.ics</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:getetag>"fffff-abcd3"</D:getetag>
+ <C:calendar-data>BEGIN:VCALENDAR</C:calendar-data>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ </D:multistatus>
+"#;
+
+ let got = deserialize::<dav::Multistatus<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_recurring_evt() {
+ let expected = CalendarQuery::<Calendar> {
+ selector: Some(CalendarSelector::Prop(dav::PropName(vec![
+ dav::PropertyRequest::Extension(PropertyRequest::CalendarData(
+ CalendarDataRequest {
+ mime: None,
+ comp: None,
+ recurrence: Some(RecurrenceModifier::LimitRecurrenceSet(
+ LimitRecurrenceSet(
+ Utc.with_ymd_and_hms(2006, 1, 3, 0, 0, 0).unwrap(),
+ Utc.with_ymd_and_hms(2006, 1, 5, 0, 0, 0).unwrap(),
+ ),
+ )),
+ limit_freebusy_set: None,
+ },
+ )),
+ ]))),
+ filter: Filter(CompFilter {
+ name: Component::VCalendar,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ prop_filter: vec![],
+ comp_filter: vec![CompFilter {
+ name: Component::VEvent,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ prop_filter: vec![],
+ comp_filter: vec![],
+ time_range: Some(TimeRange::FullRange(
+ Utc.with_ymd_and_hms(2006, 1, 3, 0, 0, 0).unwrap(),
+ Utc.with_ymd_and_hms(2006, 1, 5, 0, 0, 0).unwrap(),
+ )),
+ })),
+ }],
+ time_range: None,
+ })),
+ }),
+ timezone: None,
+ };
+
+ let src = r#"
+ <?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:D="DAV:"
+ xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop>
+ <C:calendar-data>
+ <C:limit-recurrence-set start="20060103T000000Z"
+ end="20060105T000000Z"/>
+ </C:calendar-data>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VEVENT">
+ <C:time-range start="20060103T000000Z"
+ end="20060105T000000Z"/>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+
+ let got = deserialize::<CalendarQuery<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+
+ #[tokio::test]
+ async fn rfc_pending_todos() {
+ let expected = CalendarQuery::<Calendar> {
+ selector: Some(CalendarSelector::Prop(dav::PropName(vec![
+ dav::PropertyRequest::GetEtag,
+ dav::PropertyRequest::Extension(PropertyRequest::CalendarData(
+ CalendarDataRequest {
+ mime: None,
+ comp: None,
+ recurrence: None,
+ limit_freebusy_set: None,
+ },
+ )),
+ ]))),
+ filter: Filter(CompFilter {
+ name: Component::VCalendar,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ time_range: None,
+ prop_filter: vec![],
+ comp_filter: vec![CompFilter {
+ name: Component::VTodo,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ time_range: None,
+ comp_filter: vec![],
+ prop_filter: vec![
+ PropFilter {
+ name: ComponentProperty("COMPLETED".into()),
+ additional_rules: Some(PropFilterRules::IsNotDefined),
+ },
+ PropFilter {
+ name: ComponentProperty("STATUS".into()),
+ additional_rules: Some(PropFilterRules::Match(
+ PropFilterMatch {
+ param_filter: vec![],
+ time_or_text: Some(TimeOrText::Text(TextMatch {
+ collation: None,
+ negate_condition: Some(true),
+ text: "CANCELLED".into(),
+ })),
+ },
+ )),
+ },
+ ],
+ })),
+ }],
+ })),
+ }),
+ timezone: None,
+ };
+
+ let src = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop xmlns:D="DAV:">
+ <D:getetag/>
+ <C:calendar-data/>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VTODO">
+ <C:prop-filter name="COMPLETED">
+ <C:is-not-defined/>
+ </C:prop-filter>
+ <C:prop-filter name="STATUS">
+ <C:text-match
+ negate-condition="yes">CANCELLED</C:text-match>
+ </C:prop-filter>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+
+ let got = deserialize::<CalendarQuery<Calendar>>(src).await;
+ assert_eq!(got, expected)
+ }
+}
diff --git a/aero-dav/src/calencoder.rs b/aero-dav/src/calencoder.rs
new file mode 100644
index 0000000..15df965
--- /dev/null
+++ b/aero-dav/src/calencoder.rs
@@ -0,0 +1,1036 @@
+use quick_xml::events::{BytesText, Event};
+use quick_xml::Error as QError;
+
+use super::caltypes::*;
+use super::types::Extension;
+use super::xml::{IWrite, Node, QWrite, Writer};
+
+// ==================== Calendar Types Serialization =========================
+
+// -------------------- MKCALENDAR METHOD ------------------------------------
+impl<E: Extension> QWrite for MkCalendar<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("mkcalendar");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for MkCalendarResponse<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("mkcalendar-response");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for propstat in self.0.iter() {
+ propstat.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+// ----------------------- REPORT METHOD -------------------------------------
+impl QWrite for ReportTypeName {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Query => {
+ let start = xml.create_dav_element("calendar-query");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ Self::Multiget => {
+ let start = xml.create_dav_element("calendar-multiget");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ Self::FreeBusy => {
+ let start = xml.create_dav_element("free-busy-query");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ }
+ }
+}
+
+impl<E: Extension> QWrite for ReportType<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Query(v) => v.qwrite(xml).await,
+ Self::Multiget(v) => v.qwrite(xml).await,
+ Self::FreeBusy(v) => v.qwrite(xml).await,
+ }
+ }
+}
+
+impl<E: Extension> QWrite for CalendarQuery<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("calendar-query");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ if let Some(selector) = &self.selector {
+ selector.qwrite(xml).await?;
+ }
+ self.filter.qwrite(xml).await?;
+ if let Some(tz) = &self.timezone {
+ tz.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for CalendarMultiget<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("calendar-multiget");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ if let Some(selector) = &self.selector {
+ selector.qwrite(xml).await?;
+ }
+ for href in self.href.iter() {
+ href.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for FreeBusyQuery {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("free-busy-query");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+// -------------------------- DAV::prop --------------------------------------
+impl QWrite for PropertyRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut atom = async |c| {
+ let empty_tag = xml.create_cal_element(c);
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ };
+
+ match self {
+ Self::CalendarHomeSet => atom("calendar-home-set").await,
+ Self::CalendarDescription => atom("calendar-description").await,
+ Self::CalendarTimezone => atom("calendar-timezone").await,
+ Self::SupportedCalendarComponentSet => atom("supported-calendar-component-set").await,
+ Self::SupportedCalendarData => atom("supported-calendar-data").await,
+ Self::MaxResourceSize => atom("max-resource-size").await,
+ Self::MinDateTime => atom("min-date-time").await,
+ Self::MaxDateTime => atom("max-date-time").await,
+ Self::MaxInstances => atom("max-instances").await,
+ Self::MaxAttendeesPerInstance => atom("max-attendees-per-instance").await,
+ Self::SupportedCollationSet => atom("supported-collation-set").await,
+ Self::CalendarData(req) => req.qwrite(xml).await,
+ }
+ }
+}
+impl QWrite for Property {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::CalendarHomeSet(href) => {
+ let start = xml.create_cal_element("calendar-home-set");
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ href.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::CalendarDescription { lang, text } => {
+ let mut start = xml.create_cal_element("calendar-description");
+ if let Some(the_lang) = lang {
+ start.push_attribute(("xml:lang", the_lang.as_str()));
+ }
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(text)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::CalendarTimezone(payload) => {
+ let start = xml.create_cal_element("calendar-timezone");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(payload)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::SupportedCalendarComponentSet(many_comp) => {
+ let start = xml.create_cal_element("supported-calendar-component-set");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for comp in many_comp.iter() {
+ comp.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::SupportedCalendarData(many_mime) => {
+ let start = xml.create_cal_element("supported-calendar-data");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for mime in many_mime.iter() {
+ mime.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::MaxResourceSize(bytes) => {
+ let start = xml.create_cal_element("max-resource-size");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(bytes.to_string().as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::MinDateTime(dt) => {
+ let start = xml.create_cal_element("min-date-time");
+ let end = start.to_end();
+
+ let dtstr = format!("{}", dt.format(UTC_DATETIME_FMT));
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(dtstr.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::MaxDateTime(dt) => {
+ let start = xml.create_cal_element("max-date-time");
+ let end = start.to_end();
+
+ let dtstr = format!("{}", dt.format(UTC_DATETIME_FMT));
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(dtstr.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::MaxInstances(count) => {
+ let start = xml.create_cal_element("max-instances");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(count.to_string().as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::MaxAttendeesPerInstance(count) => {
+ let start = xml.create_cal_element("max-attendees-per-instance");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(count.to_string().as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::SupportedCollationSet(many_collations) => {
+ let start = xml.create_cal_element("supported-collation-set");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for collation in many_collations.iter() {
+ collation.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::CalendarData(inner) => inner.qwrite(xml).await,
+ }
+ }
+}
+
+// ---------------------- DAV::resourcetype ----------------------------------
+impl QWrite for ResourceType {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Calendar => {
+ let empty_tag = xml.create_cal_element("calendar");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ }
+ }
+}
+
+// --------------------------- DAV::error ------------------------------------
+impl QWrite for Violation {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut atom = async |c| {
+ let empty_tag = xml.create_cal_element(c);
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ };
+
+ match self {
+ //@FIXME
+ // DAV elements, should not be here but in RFC3744 on ACLs
+ // (we do not use atom as this error is in the DAV namespace, not the caldav one)
+ Self::NeedPrivileges => {
+ let empty_tag = xml.create_dav_element("need-privileges");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+
+ // Regular CalDAV errors
+ Self::ResourceMustBeNull => atom("resource-must-be-null").await,
+ Self::CalendarCollectionLocationOk => atom("calendar-collection-location-ok").await,
+ Self::ValidCalendarData => atom("valid-calendar-data").await,
+ Self::InitializeCalendarCollection => atom("initialize-calendar-collection").await,
+ Self::SupportedCalendarData => atom("supported-calendar-data").await,
+ Self::ValidCalendarObjectResource => atom("valid-calendar-object-resource").await,
+ Self::SupportedCalendarComponent => atom("supported-calendar-component").await,
+ Self::NoUidConflict(href) => {
+ let start = xml.create_cal_element("no-uid-conflict");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ href.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::MaxResourceSize => atom("max-resource-size").await,
+ Self::MinDateTime => atom("min-date-time").await,
+ Self::MaxDateTime => atom("max-date-time").await,
+ Self::MaxInstances => atom("max-instances").await,
+ Self::MaxAttendeesPerInstance => atom("max-attendees-per-instance").await,
+ Self::ValidFilter => atom("valid-filter").await,
+ Self::SupportedFilter { comp, prop, param } => {
+ let start = xml.create_cal_element("supported-filter");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for comp_item in comp.iter() {
+ comp_item.qwrite(xml).await?;
+ }
+ for prop_item in prop.iter() {
+ prop_item.qwrite(xml).await?;
+ }
+ for param_item in param.iter() {
+ param_item.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Self::NumberOfMatchesWithinLimits => atom("number-of-matches-within-limits").await,
+ }
+ }
+}
+
+// ---------------------------- Inner XML ------------------------------------
+impl QWrite for SupportedCollation {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("supported-collation");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for Collation {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let col = match self {
+ Self::AsciiCaseMap => "i;ascii-casemap",
+ Self::Octet => "i;octet",
+ Self::Unknown(v) => v.as_str(),
+ };
+
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(col)))
+ .await
+ }
+}
+
+impl QWrite for CalendarDataSupport {
+ async fn qwrite(&self, _xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ unreachable!();
+ }
+}
+
+impl QWrite for CalendarDataPayload {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut start = xml.create_cal_element("calendar-data");
+ if let Some(mime) = &self.mime {
+ start.push_attribute(("content-type", mime.content_type.as_str()));
+ start.push_attribute(("version", mime.version.as_str()));
+ }
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(self.payload.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for CalendarDataRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut start = xml.create_cal_element("calendar-data");
+ if let Some(mime) = &self.mime {
+ start.push_attribute(("content-type", mime.content_type.as_str()));
+ start.push_attribute(("version", mime.version.as_str()));
+ }
+
+ // Empty tag
+ if self.comp.is_none() && self.recurrence.is_none() && self.limit_freebusy_set.is_none() {
+ return xml.q.write_event_async(Event::Empty(start.clone())).await;
+ }
+
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ if let Some(comp) = &self.comp {
+ comp.qwrite(xml).await?;
+ }
+ if let Some(recurrence) = &self.recurrence {
+ recurrence.qwrite(xml).await?;
+ }
+ if let Some(freebusy) = &self.limit_freebusy_set {
+ freebusy.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for CalendarDataEmpty {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut empty = xml.create_cal_element("calendar-data");
+ if let Some(mime) = &self.0 {
+ empty.push_attribute(("content-type", mime.content_type.as_str()));
+ empty.push_attribute(("version", mime.version.as_str()));
+ }
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+impl QWrite for Comp {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut start = xml.create_cal_element("comp");
+ start.push_attribute(("name", self.name.as_str()));
+ match (&self.prop_kind, &self.comp_kind) {
+ (None, None) => xml.q.write_event_async(Event::Empty(start)).await,
+ _ => {
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ if let Some(prop_kind) = &self.prop_kind {
+ prop_kind.qwrite(xml).await?;
+ }
+ if let Some(comp_kind) = &self.comp_kind {
+ comp_kind.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ }
+ }
+}
+
+impl QWrite for CompSupport {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut empty = xml.create_cal_element("comp");
+ empty.push_attribute(("name", self.0.as_str()));
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+impl QWrite for CompKind {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::AllComp => {
+ let empty_tag = xml.create_cal_element("allcomp");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ Self::Comp(many_comp) => {
+ for comp in many_comp.iter() {
+ // Required: recursion in an async fn requires boxing
+ // rustc --explain E0733
+ // Cycle detected when computing type of ...
+ // For more information about this error, try `rustc --explain E0391`.
+ // https://github.com/rust-lang/rust/issues/78649
+ #[inline(always)]
+ fn recurse<'a>(
+ comp: &'a Comp,
+ xml: &'a mut Writer<impl IWrite>,
+ ) -> futures::future::BoxFuture<'a, Result<(), QError>> {
+ Box::pin(comp.qwrite(xml))
+ }
+ recurse(comp, xml).await?;
+ }
+ Ok(())
+ }
+ }
+ }
+}
+
+impl QWrite for PropKind {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::AllProp => {
+ let empty_tag = xml.create_cal_element("allprop");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ Self::Prop(many_prop) => {
+ for prop in many_prop.iter() {
+ prop.qwrite(xml).await?;
+ }
+ Ok(())
+ }
+ }
+ }
+}
+
+impl QWrite for CalProp {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut empty = xml.create_cal_element("prop");
+ empty.push_attribute(("name", self.name.0.as_str()));
+ match self.novalue {
+ None => (),
+ Some(true) => empty.push_attribute(("novalue", "yes")),
+ Some(false) => empty.push_attribute(("novalue", "no")),
+ }
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+impl QWrite for RecurrenceModifier {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Expand(exp) => exp.qwrite(xml).await,
+ Self::LimitRecurrenceSet(lrs) => lrs.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for Expand {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut empty = xml.create_cal_element("expand");
+ empty.push_attribute((
+ "start",
+ format!("{}", self.0.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ empty.push_attribute((
+ "end",
+ format!("{}", self.1.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+impl QWrite for LimitRecurrenceSet {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut empty = xml.create_cal_element("limit-recurrence-set");
+ empty.push_attribute((
+ "start",
+ format!("{}", self.0.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ empty.push_attribute((
+ "end",
+ format!("{}", self.1.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+impl QWrite for LimitFreebusySet {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut empty = xml.create_cal_element("limit-freebusy-set");
+ empty.push_attribute((
+ "start",
+ format!("{}", self.0.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ empty.push_attribute((
+ "end",
+ format!("{}", self.1.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+impl<E: Extension> QWrite for CalendarSelector<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::AllProp => {
+ let empty_tag = xml.create_dav_element("allprop");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ Self::PropName => {
+ let empty_tag = xml.create_dav_element("propname");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ Self::Prop(prop) => prop.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for CompFilter {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut start = xml.create_cal_element("comp-filter");
+ start.push_attribute(("name", self.name.as_str()));
+
+ match &self.additional_rules {
+ None => xml.q.write_event_async(Event::Empty(start)).await,
+ Some(rules) => {
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ rules.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ }
+ }
+}
+
+impl QWrite for CompFilterRules {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::IsNotDefined => {
+ let empty_tag = xml.create_dav_element("is-not-defined");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ Self::Matches(cfm) => cfm.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for CompFilterMatch {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ if let Some(time_range) = &self.time_range {
+ time_range.qwrite(xml).await?;
+ }
+
+ for prop_item in self.prop_filter.iter() {
+ prop_item.qwrite(xml).await?;
+ }
+ for comp_item in self.comp_filter.iter() {
+ // Required: recursion in an async fn requires boxing
+ // rustc --explain E0733
+ // Cycle detected when computing type of ...
+ // For more information about this error, try `rustc --explain E0391`.
+ // https://github.com/rust-lang/rust/issues/78649
+ #[inline(always)]
+ fn recurse<'a>(
+ comp: &'a CompFilter,
+ xml: &'a mut Writer<impl IWrite>,
+ ) -> futures::future::BoxFuture<'a, Result<(), QError>> {
+ Box::pin(comp.qwrite(xml))
+ }
+ recurse(comp_item, xml).await?;
+ }
+ Ok(())
+ }
+}
+
+impl QWrite for PropFilter {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut start = xml.create_cal_element("prop-filter");
+ start.push_attribute(("name", self.name.0.as_str()));
+
+ match &self.additional_rules {
+ None => xml.q.write_event_async(Event::Empty(start.clone())).await,
+ Some(rules) => {
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ rules.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ }
+ }
+}
+
+impl QWrite for PropFilterRules {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::IsNotDefined => {
+ let empty_tag = xml.create_dav_element("is-not-defined");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ Self::Match(prop_match) => prop_match.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for PropFilterMatch {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ if let Some(time_or_text) = &self.time_or_text {
+ time_or_text.qwrite(xml).await?;
+ }
+ for param_item in self.param_filter.iter() {
+ param_item.qwrite(xml).await?;
+ }
+ Ok(())
+ }
+}
+
+impl QWrite for TimeOrText {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Time(time) => time.qwrite(xml).await,
+ Self::Text(txt) => txt.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for TextMatch {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut start = xml.create_cal_element("text-match");
+ if let Some(collation) = &self.collation {
+ start.push_attribute(("collation", collation.as_str()));
+ }
+ match self.negate_condition {
+ None => (),
+ Some(true) => start.push_attribute(("negate-condition", "yes")),
+ Some(false) => start.push_attribute(("negate-condition", "no")),
+ }
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(self.text.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for ParamFilter {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut start = xml.create_cal_element("param-filter");
+ start.push_attribute(("name", self.name.as_str()));
+
+ match &self.additional_rules {
+ None => xml.q.write_event_async(Event::Empty(start)).await,
+ Some(rules) => {
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ rules.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ }
+ }
+}
+
+impl QWrite for ParamFilterMatch {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::IsNotDefined => {
+ let empty_tag = xml.create_dav_element("is-not-defined");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ }
+ Self::Match(tm) => tm.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for TimeZone {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("timezone");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(self.0.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for Filter {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_cal_element("filter");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for TimeRange {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut empty = xml.create_cal_element("time-range");
+ match self {
+ Self::OnlyStart(start) => empty.push_attribute((
+ "start",
+ format!("{}", start.format(UTC_DATETIME_FMT)).as_str(),
+ )),
+ Self::OnlyEnd(end) => {
+ empty.push_attribute(("end", format!("{}", end.format(UTC_DATETIME_FMT)).as_str()))
+ }
+ Self::FullRange(start, end) => {
+ empty.push_attribute((
+ "start",
+ format!("{}", start.format(UTC_DATETIME_FMT)).as_str(),
+ ));
+ empty.push_attribute(("end", format!("{}", end.format(UTC_DATETIME_FMT)).as_str()));
+ }
+ }
+ xml.q.write_event_async(Event::Empty(empty)).await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::Calendar;
+ use crate::types as dav;
+ use chrono::{TimeZone, Utc};
+ use tokio::io::AsyncWriteExt;
+
+ async fn serialize(elem: &impl QWrite) -> String {
+ let mut buffer = Vec::new();
+ let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
+ let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
+ let ns_to_apply = vec![
+ ("xmlns:D".into(), "DAV:".into()),
+ ("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()),
+ ];
+ let mut writer = Writer { q, ns_to_apply };
+
+ elem.qwrite(&mut writer).await.expect("xml serialization");
+ tokio_buffer.flush().await.expect("tokio buffer flush");
+ let got = std::str::from_utf8(buffer.as_slice()).unwrap();
+
+ return got.into();
+ }
+
+ #[tokio::test]
+ async fn basic_violation() {
+ let got = serialize(&dav::Error::<Calendar>(vec![dav::Violation::Extension(
+ Violation::ResourceMustBeNull,
+ )]))
+ .await;
+
+ let expected = r#"<D:error xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <C:resource-must-be-null/>
+</D:error>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ }
+
+ #[tokio::test]
+ async fn rfc_calendar_query1_req() {
+ let got = serialize(&CalendarQuery::<Calendar> {
+ selector: Some(CalendarSelector::Prop(dav::PropName(vec![
+ dav::PropertyRequest::GetEtag,
+ dav::PropertyRequest::Extension(PropertyRequest::CalendarData(
+ CalendarDataRequest {
+ mime: None,
+ comp: Some(Comp {
+ name: Component::VCalendar,
+ prop_kind: Some(PropKind::Prop(vec![CalProp {
+ name: ComponentProperty("VERSION".into()),
+ novalue: None,
+ }])),
+ comp_kind: Some(CompKind::Comp(vec![
+ Comp {
+ name: Component::VEvent,
+ prop_kind: Some(PropKind::Prop(vec![
+ CalProp {
+ name: ComponentProperty("SUMMARY".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("UID".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("DTSTART".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("DTEND".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("DURATION".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("RRULE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("RDATE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("EXRULE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("EXDATE".into()),
+ novalue: None,
+ },
+ CalProp {
+ name: ComponentProperty("RECURRENCE-ID".into()),
+ novalue: None,
+ },
+ ])),
+ comp_kind: None,
+ },
+ Comp {
+ name: Component::VTimeZone,
+ prop_kind: None,
+ comp_kind: None,
+ },
+ ])),
+ }),
+ recurrence: None,
+ limit_freebusy_set: None,
+ },
+ )),
+ ]))),
+ filter: Filter(CompFilter {
+ name: Component::VCalendar,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ time_range: None,
+ prop_filter: vec![],
+ comp_filter: vec![CompFilter {
+ name: Component::VEvent,
+ additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
+ time_range: Some(TimeRange::FullRange(
+ Utc.with_ymd_and_hms(2006, 1, 4, 0, 0, 0).unwrap(),
+ Utc.with_ymd_and_hms(2006, 1, 5, 0, 0, 0).unwrap(),
+ )),
+ prop_filter: vec![],
+ comp_filter: vec![],
+ })),
+ }],
+ })),
+ }),
+ timezone: None,
+ })
+ .await;
+
+ let expected = r#"<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop>
+ <D:getetag/>
+ <C:calendar-data>
+ <C:comp name="VCALENDAR">
+ <C:prop name="VERSION"/>
+ <C:comp name="VEVENT">
+ <C:prop name="SUMMARY"/>
+ <C:prop name="UID"/>
+ <C:prop name="DTSTART"/>
+ <C:prop name="DTEND"/>
+ <C:prop name="DURATION"/>
+ <C:prop name="RRULE"/>
+ <C:prop name="RDATE"/>
+ <C:prop name="EXRULE"/>
+ <C:prop name="EXDATE"/>
+ <C:prop name="RECURRENCE-ID"/>
+ </C:comp>
+ <C:comp name="VTIMEZONE"/>
+ </C:comp>
+ </C:calendar-data>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VEVENT">
+ <C:time-range start="20060104T000000Z" end="20060105T000000Z"/>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+</C:calendar-query>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ }
+
+ #[tokio::test]
+ async fn rfc_calendar_query1_res() {
+ let got = serialize(&dav::Multistatus::<Calendar> {
+ extension: None,
+ responses: vec![
+ dav::Response {
+ status_or_propstat: dav::StatusOrPropstat::PropStat(
+ dav::Href("http://cal.example.com/bernard/work/abcd2.ics".into()),
+ vec![dav::PropStat {
+ prop: dav::AnyProp(vec![
+ dav::AnyProperty::Value(dav::Property::GetEtag(
+ "\"fffff-abcd2\"".into(),
+ )),
+ dav::AnyProperty::Value(dav::Property::Extension(
+ Property::CalendarData(CalendarDataPayload {
+ mime: None,
+ payload: "PLACEHOLDER".into(),
+ }),
+ )),
+ ]),
+ status: dav::Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ location: None,
+ error: None,
+ responsedescription: None,
+ },
+ dav::Response {
+ status_or_propstat: dav::StatusOrPropstat::PropStat(
+ dav::Href("http://cal.example.com/bernard/work/abcd3.ics".into()),
+ vec![dav::PropStat {
+ prop: dav::AnyProp(vec![
+ dav::AnyProperty::Value(dav::Property::GetEtag(
+ "\"fffff-abcd3\"".into(),
+ )),
+ dav::AnyProperty::Value(dav::Property::Extension(
+ Property::CalendarData(CalendarDataPayload {
+ mime: None,
+ payload: "PLACEHOLDER".into(),
+ }),
+ )),
+ ]),
+ status: dav::Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ location: None,
+ error: None,
+ responsedescription: None,
+ },
+ ],
+ responsedescription: None,
+ })
+ .await;
+
+ let expected = r#"<D:multistatus xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:response>
+ <D:href>http://cal.example.com/bernard/work/abcd2.ics</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:getetag>&quot;fffff-abcd2&quot;</D:getetag>
+ <C:calendar-data>PLACEHOLDER</C:calendar-data>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>http://cal.example.com/bernard/work/abcd3.ics</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:getetag>&quot;fffff-abcd3&quot;</D:getetag>
+ <C:calendar-data>PLACEHOLDER</C:calendar-data>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+</D:multistatus>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ }
+}
diff --git a/aero-dav/src/caltypes.rs b/aero-dav/src/caltypes.rs
new file mode 100644
index 0000000..a4f6fef
--- /dev/null
+++ b/aero-dav/src/caltypes.rs
@@ -0,0 +1,1500 @@
+#![allow(dead_code)]
+
+use super::types as dav;
+use chrono::{DateTime, Utc};
+
+pub const FLOATING_DATETIME_FMT: &str = "%Y%m%dT%H%M%S";
+pub const UTC_DATETIME_FMT: &str = "%Y%m%dT%H%M%SZ";
+
+//@FIXME ACL (rfc3744) is missing, required
+//@FIXME Versioning (rfc3253) is missing, required
+//@FIXME WebDAV sync (rfc6578) is missing, optional
+// For reference, SabreDAV guide gives high-level & real-world overview:
+// https://sabre.io/dav/building-a-caldav-client/
+// For reference, non-official extensions documented by SabreDAV:
+// https://github.com/apple/ccs-calendarserver/tree/master/doc/Extensions
+
+// ----- Root elements -----
+
+// --- (MKCALENDAR PART) ---
+
+/// If a request body is included, it MUST be a CALDAV:mkcalendar XML
+/// element. Instruction processing MUST occur in the order
+/// instructions are received (i.e., from top to bottom).
+/// Instructions MUST either all be executed or none executed. Thus,
+/// if any error occurs during processing, all executed instructions
+/// MUST be undone and a proper error result returned. Instruction
+/// processing details can be found in the definition of the DAV:set
+/// instruction in Section 12.13.2 of [RFC2518].
+///
+/// ```xmlschema
+/// <!ELEMENT mkcalendar (DAV:set)>
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+pub struct MkCalendar<E: dav::Extension>(pub dav::Set<E>);
+
+/// If a response body for a successful request is included, it MUST
+/// be a CALDAV:mkcalendar-response XML element.
+///
+/// <!ELEMENT mkcalendar-response ANY>
+///
+/// ----
+///
+/// ANY is not satisfying, so looking at RFC5689
+/// https://www.rfc-editor.org/rfc/rfc5689.html#section-5.2
+///
+/// Definition:
+///
+/// <!ELEMENT mkcol-response (propstat+)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct MkCalendarResponse<E: dav::Extension>(pub Vec<dav::PropStat<E>>);
+
+// --- (REPORT PART) ---
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportTypeName {
+ Query,
+ Multiget,
+ FreeBusy,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportType<E: dav::Extension> {
+ Query(CalendarQuery<E>),
+ Multiget(CalendarMultiget<E>),
+ FreeBusy(FreeBusyQuery),
+}
+
+/// Name: calendar-query
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Defines a report for querying calendar object resources.
+///
+/// Description: See Section 7.8.
+///
+/// Definition:
+///
+/// <!ELEMENT calendar-query ((DAV:allprop |
+/// DAV:propname |
+/// DAV:prop)?, filter, timezone?)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct CalendarQuery<E: dav::Extension> {
+ pub selector: Option<CalendarSelector<E>>,
+ pub filter: Filter,
+ pub timezone: Option<TimeZone>,
+}
+
+/// Name: calendar-multiget
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: CalDAV report used to retrieve specific calendar object
+/// resources.
+///
+/// Description: See Section 7.9.
+///
+/// Definition:
+///
+/// <!ELEMENT calendar-multiget ((DAV:allprop |
+/// DAV:propname |
+/// DAV:prop)?, DAV:href+)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct CalendarMultiget<E: dav::Extension> {
+ pub selector: Option<CalendarSelector<E>>,
+ pub href: Vec<dav::Href>,
+}
+
+/// Name: free-busy-query
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: CalDAV report used to generate a VFREEBUSY to determine
+/// busy time over a specific time range.
+///
+/// Description: See Section 7.10.
+///
+/// Definition:
+/// <!ELEMENT free-busy-query (time-range)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct FreeBusyQuery(pub TimeRange);
+
+// ----- Hooks -----
+#[derive(Debug, PartialEq, Clone)]
+pub enum ResourceType {
+ Calendar,
+}
+
+/// Check the matching Property object for documentation
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ CalendarHomeSet,
+ CalendarDescription,
+ CalendarTimezone,
+ SupportedCalendarComponentSet,
+ SupportedCalendarData,
+ MaxResourceSize,
+ MinDateTime,
+ MaxDateTime,
+ MaxInstances,
+ MaxAttendeesPerInstance,
+ SupportedCollationSet,
+ CalendarData(CalendarDataRequest),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property {
+ /// Name: calendar-home-set
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Identifies the URL of any WebDAV collections that contain
+ /// calendar collections owned by the associated principal resource.
+ ///
+ /// Conformance: This property SHOULD be defined on a principal
+ /// resource. If defined, it MAY be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:calendar-home-set property is meant to allow
+ /// users to easily find the calendar collections owned by the
+ /// principal. Typically, users will group all the calendar
+ /// collections that they own under a common collection. This
+ /// property specifies the URL of collections that are either calendar
+ /// collections or ordinary collections that have child or descendant
+ /// calendar collections owned by the principal.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT calendar-home-set (DAV:href*)>
+ CalendarHomeSet(dav::Href),
+
+ /// Name: calendar-description
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Provides a human-readable description of the calendar
+ /// collection.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MAY be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]). An xml:lang attribute indicating the human
+ /// language of the description SHOULD be set for this property by
+ /// clients or through server provisioning. Servers MUST return any
+ /// xml:lang attribute if set for the property.
+ ///
+ /// Description: If present, the property contains a description of the
+ /// calendar collection that is suitable for presentation to a user.
+ /// If not present, the client should assume no description for the
+ /// calendar collection.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT calendar-description (#PCDATA)>
+ /// PCDATA value: string
+ ///
+ /// Example:
+ ///
+ /// <C:calendar-description xml:lang="fr-CA"
+ /// xmlns:C="urn:ietf:params:xml:ns:caldav"
+ /// >Calendrier de Mathilde Desruisseaux</C:calendar-description>
+ CalendarDescription { lang: Option<String>, text: String },
+
+ /// 5.2.2. CALDAV:calendar-timezone Property
+ ///
+ /// Name: calendar-timezone
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Specifies a time zone on a calendar collection.
+ ///
+ /// Conformance: This property SHOULD be defined on all calendar
+ /// collections. If defined, it SHOULD NOT be returned by a PROPFIND
+ /// DAV:allprop request (as defined in Section 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:calendar-timezone property is used to
+ /// specify the time zone the server should rely on to resolve "date"
+ /// values and "date with local time" values (i.e., floating time) to
+ /// "date with UTC time" values. The server will require this
+ /// information to determine if a calendar component scheduled with
+ /// "date" values or "date with local time" values overlaps a CALDAV:
+ /// time-range specified in a CALDAV:calendar-query REPORT. The
+ /// server will also require this information to compute the proper
+ /// FREEBUSY time period as "date with UTC time" in the VFREEBUSY
+ /// component returned in a response to a CALDAV:free-busy-query
+ /// REPORT request that takes into account calendar components
+ /// scheduled with "date" values or "date with local time" values. In
+ /// the absence of this property, the server MAY rely on the time zone
+ /// of their choice.
+ ///
+ /// Note: The iCalendar data embedded within the CALDAV:calendar-
+ /// timezone XML element MUST follow the standard XML character data
+ /// encoding rules, including use of &lt;, &gt;, &amp; etc. entity
+ /// encoding or the use of a <![CDATA[ ... ]]> construct. In the
+ /// later case, the iCalendar data cannot contain the character
+ /// sequence "]]>", which is the end delimiter for the CDATA section.
+ ///
+ /// Definition:
+ ///
+ /// ```xmlschema
+ /// <!ELEMENT calendar-timezone (#PCDATA)>
+ /// PCDATA value: an iCalendar object with exactly one VTIMEZONE component.
+ /// ```
+ ///
+ /// Example:
+ ///
+ /// ```xmlschema
+ /// <C:calendar-timezone
+ /// xmlns:C="urn:ietf:params:xml:ns:caldav">BEGIN:VCALENDAR
+ /// PRODID:-//Example Corp.//CalDAV Client//EN
+ /// VERSION:2.0
+ /// BEGIN:VTIMEZONE
+ /// TZID:US-Eastern
+ /// LAST-MODIFIED:19870101T000000Z
+ /// BEGIN:STANDARD
+ /// DTSTART:19671029T020000
+ /// RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+ /// TZOFFSETFROM:-0400
+ /// TZOFFSETTO:-0500
+ /// TZNAME:Eastern Standard Time (US &amp; Canada)
+ /// END:STANDARD
+ /// BEGIN:DAYLIGHT
+ /// DTSTART:19870405T020000
+ /// RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+ /// TZOFFSETFROM:-0500
+ /// TZOFFSETTO:-0400
+ /// TZNAME:Eastern Daylight Time (US &amp; Canada)
+ /// END:DAYLIGHT
+ /// END:VTIMEZONE
+ /// END:VCALENDAR
+ /// </C:calendar-timezone>
+ /// ```
+ //@FIXME we might want to put a buffer here or an iCal parsed object
+ CalendarTimezone(String),
+
+ /// Name: supported-calendar-component-set
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Specifies the calendar component types (e.g., VEVENT,
+ /// VTODO, etc.) that calendar object resources can contain in the
+ /// calendar collection.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MUST be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:supported-calendar-component-set property is
+ /// used to specify restrictions on the calendar component types that
+ /// calendar object resources may contain in a calendar collection.
+ /// Any attempt by the client to store calendar object resources with
+ /// component types not listed in this property, if it exists, MUST
+ /// result in an error, with the CALDAV:supported-calendar-component
+ /// precondition (Section 5.3.2.1) being violated. Since this
+ /// property is protected, it cannot be changed by clients using a
+ /// PROPPATCH request. However, clients can initialize the value of
+ /// this property when creating a new calendar collection with
+ /// MKCALENDAR. The empty-element tag <C:comp name="VTIMEZONE"/> MUST
+ /// only be specified if support for calendar object resources that
+ /// only contain VTIMEZONE components is provided or desired. Support
+ /// for VTIMEZONE components in calendar object resources that contain
+ /// VEVENT or VTODO components is always assumed. In the absence of
+ /// this property, the server MUST accept all component types, and the
+ /// client can assume that all component types are accepted.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT supported-calendar-component-set (comp+)>
+ ///
+ /// Example:
+ ///
+ /// <C:supported-calendar-component-set
+ /// xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// <C:comp name="VEVENT"/>
+ /// <C:comp name="VTODO"/>
+ /// </C:supported-calendar-component-set>
+ SupportedCalendarComponentSet(Vec<CompSupport>),
+
+ /// Name: supported-calendar-data
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Specifies what media types are allowed for calendar object
+ /// resources in a calendar collection.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MUST be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:supported-calendar-data property is used to
+ /// specify the media type supported for the calendar object resources
+ /// contained in a given calendar collection (e.g., iCalendar version
+ /// 2.0). Any attempt by the client to store calendar object
+ /// resources with a media type not listed in this property MUST
+ /// result in an error, with the CALDAV:supported-calendar-data
+ /// precondition (Section 5.3.2.1) being violated. In the absence of
+ /// this property, the server MUST only accept data with the media
+ /// type "text/calendar" and iCalendar version 2.0, and clients can
+ /// assume that the server will only accept this data.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT supported-calendar-data (calendar-data+)>
+ ///
+ /// Example:
+ ///
+ /// <C:supported-calendar-data
+ /// xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// <C:calendar-data content-type="text/calendar" version="2.0"/>
+ /// </C:supported-calendar-data>
+ ///
+ /// -----
+ ///
+ /// <!ELEMENT calendar-data EMPTY>
+ ///
+ /// when nested in the CALDAV:supported-calendar-data property
+ /// to specify a supported media type for calendar object
+ /// resources;
+ SupportedCalendarData(Vec<CalendarDataEmpty>),
+
+ /// Name: max-resource-size
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Provides a numeric value indicating the maximum size of a
+ /// resource in octets that the server is willing to accept when a
+ /// calendar object resource is stored in a calendar collection.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MUST be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:max-resource-size is used to specify a
+ /// numeric value that represents the maximum size in octets that the
+ /// server is willing to accept when a calendar object resource is
+ /// stored in a calendar collection. Any attempt to store a calendar
+ /// object resource exceeding this size MUST result in an error, with
+ /// the CALDAV:max-resource-size precondition (Section 5.3.2.1) being
+ /// violated. In the absence of this property, the client can assume
+ /// that the server will allow storing a resource of any reasonable
+ /// size.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT max-resource-size (#PCDATA)>
+ /// PCDATA value: a numeric value (positive integer)
+ ///
+ /// Example:
+ ///
+ /// <C:max-resource-size xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// 102400
+ /// </C:max-resource-size>
+ MaxResourceSize(u64),
+
+ /// CALDAV:min-date-time Property
+ ///
+ /// Name: min-date-time
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Provides a DATE-TIME value indicating the earliest date and
+ /// time (in UTC) that the server is willing to accept for any DATE or
+ /// DATE-TIME value in a calendar object resource stored in a calendar
+ /// collection.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MUST be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:min-date-time is used to specify an
+ /// iCalendar DATE-TIME value in UTC that indicates the earliest
+ /// inclusive date that the server is willing to accept for any
+ /// explicit DATE or DATE-TIME value in a calendar object resource
+ /// stored in a calendar collection. Any attempt to store a calendar
+ /// object resource using a DATE or DATE-TIME value earlier than this
+ /// value MUST result in an error, with the CALDAV:min-date-time
+ /// precondition (Section 5.3.2.1) being violated. Note that servers
+ /// MUST accept recurring components that specify instances beyond
+ /// this limit, provided none of those instances have been overridden.
+ /// In that case, the server MAY simply ignore those instances outside
+ /// of the acceptable range when processing reports on the calendar
+ /// object resource. In the absence of this property, the client can
+ /// assume any valid iCalendar date may be used at least up to the
+ /// CALDAV:max-date-time value, if that is defined.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT min-date-time (#PCDATA)>
+ /// PCDATA value: an iCalendar format DATE-TIME value in UTC
+ ///
+ /// Example:
+ ///
+ /// <C:min-date-time xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// 19000101T000000Z
+ /// </C:min-date-time>
+ MinDateTime(DateTime<Utc>),
+
+ /// CALDAV:max-date-time Property
+ ///
+ /// Name: max-date-time
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Provides a DATE-TIME value indicating the latest date and
+ /// time (in UTC) that the server is willing to accept for any DATE or
+ /// DATE-TIME value in a calendar object resource stored in a calendar
+ /// collection.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MUST be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:max-date-time is used to specify an
+ /// iCalendar DATE-TIME value in UTC that indicates the inclusive
+ /// latest date that the server is willing to accept for any date or
+ /// time value in a calendar object resource stored in a calendar
+ /// collection. Any attempt to store a calendar object resource using
+ /// a DATE or DATE-TIME value later than this value MUST result in an
+ /// error, with the CALDAV:max-date-time precondition
+ /// (Section 5.3.2.1) being violated. Note that servers MUST accept
+ /// recurring components that specify instances beyond this limit,
+ /// provided none of those instances have been overridden. In that
+ /// case, the server MAY simply ignore those instances outside of the
+ /// acceptable range when processing reports on the calendar object
+ /// resource. In the absence of this property, the client can assume
+ /// any valid iCalendar date may be used at least down to the CALDAV:
+ /// min-date-time value, if that is defined.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT max-date-time (#PCDATA)>
+ /// PCDATA value: an iCalendar format DATE-TIME value in UTC
+ ///
+ /// Example:
+ ///
+ /// <C:max-date-time xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// 20491231T235959Z
+ /// </C:max-date-time>
+ MaxDateTime(DateTime<Utc>),
+
+ /// CALDAV:max-instances Property
+ ///
+ /// Name: max-instances
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Provides a numeric value indicating the maximum number of
+ /// recurrence instances that a calendar object resource stored in a
+ /// calendar collection can generate.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MUST be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:max-instances is used to specify a numeric
+ /// value that indicates the maximum number of recurrence instances
+ /// that a calendar object resource stored in a calendar collection
+ /// can generate. Any attempt to store a calendar object resource
+ /// with a recurrence pattern that generates more instances than this
+ /// value MUST result in an error, with the CALDAV:max-instances
+ /// precondition (Section 5.3.2.1) being violated. In the absence of
+ /// this property, the client can assume that the server has no limits
+ /// on the number of recurrence instances it can handle or expand.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT max-instances (#PCDATA)>
+ /// PCDATA value: a numeric value (integer greater than zero)
+ ///
+ /// Example:
+ ///
+ /// <C:max-instances xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// 100
+ /// </C:max-instances>
+ MaxInstances(u64),
+
+ /// CALDAV:max-attendees-per-instance Property
+ ///
+ /// Name: max-attendees-per-instance
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Provides a numeric value indicating the maximum number of
+ /// ATTENDEE properties in any instance of a calendar object resource
+ /// stored in a calendar collection.
+ ///
+ /// Conformance: This property MAY be defined on any calendar
+ /// collection. If defined, it MUST be protected and SHOULD NOT be
+ /// returned by a PROPFIND DAV:allprop request (as defined in Section
+ /// 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:max-attendees-per-instance is used to
+ /// specify a numeric value that indicates the maximum number of
+ /// iCalendar ATTENDEE properties on any one instance of a calendar
+ /// object resource stored in a calendar collection. Any attempt to
+ /// store a calendar object resource with more ATTENDEE properties per
+ /// instance than this value MUST result in an error, with the CALDAV:
+ /// max-attendees-per-instance precondition (Section 5.3.2.1) being
+ /// violated. In the absence of this property, the client can assume
+ /// that the server can handle any number of ATTENDEE properties in a
+ /// calendar component.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT max-attendees-per-instance (#PCDATA)>
+ /// PCDATA value: a numeric value (integer greater than zero)
+ ///
+ /// Example:
+ ///
+ /// <C:max-attendees-per-instance
+ /// xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// 25
+ /// </C:max-attendees-per-instance>
+ MaxAttendeesPerInstance(u64),
+
+ /// Name: supported-collation-set
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Identifies the set of collations supported by the server
+ /// for text matching operations.
+ ///
+ /// Conformance: This property MUST be defined on any resource that
+ /// supports a report that does text matching. If defined, it MUST be
+ /// protected and SHOULD NOT be returned by a PROPFIND DAV:allprop
+ /// request (as defined in Section 12.14.1 of [RFC2518]).
+ ///
+ /// Description: The CALDAV:supported-collation-set property contains
+ /// zero or more CALDAV:supported-collation elements, which specify
+ /// the collection identifiers of the collations supported by the
+ /// server.
+ ///
+ /// Definition:
+ ///
+ /// <!ELEMENT supported-collation-set (supported-collation*)>
+ /// <!ELEMENT supported-collation (#PCDATA)>
+ ///
+ /// Example:
+ ///
+ /// <C:supported-collation-set
+ /// xmlns:C="urn:ietf:params:xml:ns:caldav">
+ /// <C:supported-collation>i;ascii-casemap</C:supported-collation>
+ /// <C:supported-collation>i;octet</C:supported-collation>
+ /// </C:supported-collation-set>
+ SupportedCollationSet(Vec<SupportedCollation>),
+
+ /// Name: calendar-data
+ ///
+ /// Namespace: urn:ietf:params:xml:ns:caldav
+ ///
+ /// Purpose: Specified one of the following:
+ ///
+ /// 1. A supported media type for calendar object resources when
+ /// nested in the CALDAV:supported-calendar-data property;
+ ///
+ /// 2. The parts of a calendar object resource should be returned by
+ /// a calendaring report;
+ ///
+ /// 3. The content of a calendar object resource in a response to a
+ /// calendaring report.
+ ///
+ /// Description: When nested in the CALDAV:supported-calendar-data
+ /// property, the CALDAV:calendar-data XML element specifies a media
+ /// type supported by the CalDAV server for calendar object resources.
+ ///
+ /// When used in a calendaring REPORT request, the CALDAV:calendar-
+ /// data XML element specifies which parts of calendar object
+ /// resources need to be returned in the response. If the CALDAV:
+ /// calendar-data XML element doesn't contain any CALDAV:comp element,
+ /// calendar object resources will be returned in their entirety.
+ ///
+ /// Finally, when used in a calendaring REPORT response, the CALDAV:
+ /// calendar-data XML element specifies the content of a calendar
+ /// object resource. Given that XML parsers normalize the two-
+ /// character sequence CRLF (US-ASCII decimal 13 and US-ASCII decimal
+ /// 10) to a single LF character (US-ASCII decimal 10), the CR
+ /// character (US-ASCII decimal 13) MAY be omitted in calendar object
+ /// resources specified in the CALDAV:calendar-data XML element.
+ /// Furthermore, calendar object resources specified in the CALDAV:
+ /// calendar-data XML element MAY be invalid per their media type
+ /// specification if the CALDAV:calendar-data XML element part of the
+ /// calendaring REPORT request did not specify required properties
+ /// (e.g., UID, DTSTAMP, etc.), or specified a CALDAV:prop XML element
+ /// with the "novalue" attribute set to "yes".
+ ///
+ /// Note: The CALDAV:calendar-data XML element is specified in requests
+ /// and responses inside the DAV:prop XML element as if it were a
+ /// WebDAV property. However, the CALDAV:calendar-data XML element is
+ /// not a WebDAV property and, as such, is not returned in PROPFIND
+ /// responses, nor used in PROPPATCH requests.
+ ///
+ /// Note: The iCalendar data embedded within the CALDAV:calendar-data
+ /// XML element MUST follow the standard XML character data encoding
+ /// rules, including use of &lt;, &gt;, &amp; etc. entity encoding or
+ /// the use of a <![CDATA[ ... ]]> construct. In the later case, the
+ /// iCalendar data cannot contain the character sequence "]]>", which
+ /// is the end delimiter for the CDATA section.
+ CalendarData(CalendarDataPayload),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Violation {
+ /// (DAV:resource-must-be-null): A resource MUST NOT exist at the
+ /// Request-URI;
+ ResourceMustBeNull,
+
+ /// (CALDAV:calendar-collection-location-ok): The Request-URI MUST
+ /// identify a location where a calendar collection can be created;
+ CalendarCollectionLocationOk,
+
+ /// (CALDAV:valid-calendar-data): The time zone specified in CALDAV:
+ /// calendar-timezone property MUST be a valid iCalendar object
+ /// containing a single valid VTIMEZONE component.
+ ValidCalendarData,
+
+ ///@FIXME should not be here but in RFC3744
+ /// !!! ERRATA 1002 !!!
+ /// (DAV:need-privileges): The DAV:bind privilege MUST be granted to
+ /// the current user on the parent collection of the Request-URI.
+ NeedPrivileges,
+
+ /// (CALDAV:initialize-calendar-collection): A new calendar collection
+ /// exists at the Request-URI. The DAV:resourcetype of the calendar
+ /// collection MUST contain both DAV:collection and CALDAV:calendar
+ /// XML elements.
+ InitializeCalendarCollection,
+
+ /// (CALDAV:supported-calendar-data): The resource submitted in the
+ /// PUT request, or targeted by a COPY or MOVE request, MUST be a
+ /// supported media type (i.e., iCalendar) for calendar object
+ /// resources;
+ SupportedCalendarData,
+
+ /// (CALDAV:valid-calendar-object-resource): The resource submitted in
+ /// the PUT request, or targeted by a COPY or MOVE request, MUST obey
+ /// all restrictions specified in Section 4.1 (e.g., calendar object
+ /// resources MUST NOT contain more than one type of calendar
+ /// component, calendar object resources MUST NOT specify the
+ /// iCalendar METHOD property, etc.);
+ ValidCalendarObjectResource,
+
+ /// (CALDAV:supported-calendar-component): The resource submitted in
+ /// the PUT request, or targeted by a COPY or MOVE request, MUST
+ /// contain a type of calendar component that is supported in the
+ /// targeted calendar collection;
+ SupportedCalendarComponent,
+
+ /// (CALDAV:no-uid-conflict): The resource submitted in the PUT
+ /// request, or targeted by a COPY or MOVE request, MUST NOT specify
+ /// an iCalendar UID property value already in use in the targeted
+ /// calendar collection or overwrite an existing calendar object
+ /// resource with one that has a different UID property value.
+ /// Servers SHOULD report the URL of the resource that is already
+ /// making use of the same UID property value in the DAV:href element;
+ ///
+ /// <!ELEMENT no-uid-conflict (DAV:href)>
+ NoUidConflict(dav::Href),
+
+ /// (CALDAV:max-resource-size): The resource submitted in the PUT
+ /// request, or targeted by a COPY or MOVE request, MUST have an octet
+ /// size less than or equal to the value of the CALDAV:max-resource-
+ /// size property value (Section 5.2.5) on the calendar collection
+ /// where the resource will be stored;
+ MaxResourceSize,
+
+ /// (CALDAV:min-date-time): The resource submitted in the PUT request,
+ /// or targeted by a COPY or MOVE request, MUST have all of its
+ /// iCalendar DATE or DATE-TIME property values (for each recurring
+ /// instance) greater than or equal to the value of the CALDAV:min-
+ /// date-time property value (Section 5.2.6) on the calendar
+ /// collection where the resource will be stored;
+ MinDateTime,
+
+ /// (CALDAV:max-date-time): The resource submitted in the PUT request,
+ /// or targeted by a COPY or MOVE request, MUST have all of its
+ /// iCalendar DATE or DATE-TIME property values (for each recurring
+ /// instance) less than the value of the CALDAV:max-date-time property
+ /// value (Section 5.2.7) on the calendar collection where the
+ /// resource will be stored;
+ MaxDateTime,
+
+ /// (CALDAV:max-instances): The resource submitted in the PUT request,
+ /// or targeted by a COPY or MOVE request, MUST generate a number of
+ /// recurring instances less than or equal to the value of the CALDAV:
+ /// max-instances property value (Section 5.2.8) on the calendar
+ /// collection where the resource will be stored;
+ MaxInstances,
+
+ /// (CALDAV:max-attendees-per-instance): The resource submitted in the
+ /// PUT request, or targeted by a COPY or MOVE request, MUST have a
+ /// number of ATTENDEE properties on any one instance less than or
+ /// equal to the value of the CALDAV:max-attendees-per-instance
+ /// property value (Section 5.2.9) on the calendar collection where
+ /// the resource will be stored;
+ MaxAttendeesPerInstance,
+
+ /// (CALDAV:valid-filter): The CALDAV:filter XML element (see
+ /// Section 9.7) specified in the REPORT request MUST be valid. For
+ /// instance, a CALDAV:filter cannot nest a <C:comp name="VEVENT">
+ /// element in a <C:comp name="VTODO"> element, and a CALDAV:filter
+ /// cannot nest a <C:time-range start="..." end="..."> element in a
+ /// <C:prop name="SUMMARY"> element.
+ ValidFilter,
+
+ /// (CALDAV:supported-filter): The CALDAV:comp-filter (see
+ /// Section 9.7.1), CALDAV:prop-filter (see Section 9.7.2), and
+ /// CALDAV:param-filter (see Section 9.7.3) XML elements used in the
+ /// CALDAV:filter XML element (see Section 9.7) in the REPORT request
+ /// only make reference to components, properties, and parameters for
+ /// which queries are supported by the server, i.e., if the CALDAV:
+ /// filter element attempts to reference an unsupported component,
+ /// property, or parameter, this precondition is violated. Servers
+ /// SHOULD report the CALDAV:comp-filter, CALDAV:prop-filter, or
+ /// CALDAV:param-filter for which it does not provide support.
+ ///
+ /// <!ELEMENT supported-filter (comp-filter*,
+ /// prop-filter*,
+ /// param-filter*)>
+ SupportedFilter {
+ comp: Vec<CompFilter>,
+ prop: Vec<PropFilter>,
+ param: Vec<ParamFilter>,
+ },
+
+ /// (DAV:number-of-matches-within-limits): The number of matching
+ /// calendar object resources must fall within server-specific,
+ /// predefined limits. For example, this condition might be triggered
+ /// if a search specification would cause the return of an extremely
+ /// large number of responses.
+ NumberOfMatchesWithinLimits,
+}
+
+// -------- Inner XML elements ---------
+
+/// Some of the reports defined in this section do text matches of
+/// character strings provided by the client and are compared to stored
+/// calendar data. Since iCalendar data is, by default, encoded in the
+/// UTF-8 charset and may include characters outside the US-ASCII charset
+/// range in some property and parameter values, there is a need to
+/// ensure that text matching follows well-defined rules.
+///
+/// To deal with this, this specification makes use of the IANA Collation
+/// Registry defined in [RFC4790] to specify collations that may be used
+/// to carry out the text comparison operations with a well-defined rule.
+///
+/// The comparisons used in CalDAV are all "substring" matches, as per
+/// [RFC4790], Section 4.2. Collations supported by the server MUST
+/// support "substring" match operations.
+///
+/// CalDAV servers are REQUIRED to support the "i;ascii-casemap" and
+/// "i;octet" collations, as described in [RFC4790], and MAY support
+/// other collations.
+///
+/// Servers MUST advertise the set of collations that they support via
+/// the CALDAV:supported-collation-set property defined on any resource
+/// that supports reports that use collations.
+///
+/// Clients MUST only use collations from the list advertised by the
+/// server.
+///
+/// In the absence of a collation explicitly specified by the client, or
+/// if the client specifies the "default" collation identifier (as
+/// defined in [RFC4790], Section 3.1), the server MUST default to using
+/// "i;ascii-casemap" as the collation.
+///
+/// Wildcards (as defined in [RFC4790], Section 3.2) MUST NOT be used in
+/// the collation identifier.
+///
+/// If the client chooses a collation not supported by the server, the
+/// server MUST respond with a CALDAV:supported-collation precondition
+/// error response.
+#[derive(Debug, PartialEq, Clone)]
+pub struct SupportedCollation(pub Collation);
+
+/// <!ELEMENT calendar-data (#PCDATA)>
+/// PCDATA value: iCalendar object
+///
+/// when nested in the DAV:prop XML element in a calendaring
+/// REPORT response to specify the content of a returned
+/// calendar object resource.
+#[derive(Debug, PartialEq, Clone)]
+pub struct CalendarDataPayload {
+ pub mime: Option<CalendarDataSupport>,
+ pub payload: String,
+}
+
+/// <!ELEMENT calendar-data (comp?,
+/// (expand | limit-recurrence-set)?,
+/// limit-freebusy-set?)>
+///
+/// when nested in the DAV:prop XML element in a calendaring
+/// REPORT request to specify which parts of calendar object
+/// resources should be returned in the response;
+#[derive(Debug, PartialEq, Clone, Default)]
+pub struct CalendarDataRequest {
+ pub mime: Option<CalendarDataSupport>,
+ pub comp: Option<Comp>,
+ pub recurrence: Option<RecurrenceModifier>,
+ pub limit_freebusy_set: Option<LimitFreebusySet>,
+}
+
+/// calendar-data specialization for Property
+///
+/// <!ELEMENT calendar-data EMPTY>
+///
+/// when nested in the CALDAV:supported-calendar-data property
+/// to specify a supported media type for calendar object
+/// resources;
+#[derive(Debug, PartialEq, Clone)]
+pub struct CalendarDataEmpty(pub Option<CalendarDataSupport>);
+
+/// <!ATTLIST calendar-data content-type CDATA "text/calendar"
+/// version CDATA "2.0">
+/// content-type value: a MIME media type
+/// version value: a version string
+/// attributes can be used on all three variants of the
+/// CALDAV:calendar-data XML element.
+#[derive(Debug, PartialEq, Clone)]
+pub struct CalendarDataSupport {
+ pub content_type: String,
+ pub version: String,
+}
+
+/// Name: comp
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Defines which component types to return.
+///
+/// Description: The name value is a calendar component name (e.g.,
+/// VEVENT).
+///
+/// Definition:
+///
+/// <!ELEMENT comp ((allprop | prop*), (allcomp | comp*))>
+/// <!ATTLIST comp name CDATA #REQUIRED>
+/// name value: a calendar component name
+///
+/// Note: The CALDAV:prop and CALDAV:allprop elements have the same name
+/// as the DAV:prop and DAV:allprop elements defined in [RFC2518].
+/// However, the CALDAV:prop and CALDAV:allprop elements are defined
+/// in the "urn:ietf:params:xml:ns:caldav" namespace instead of the
+/// "DAV:" namespace.
+#[derive(Debug, PartialEq, Clone)]
+pub struct Comp {
+ pub name: Component,
+ pub prop_kind: Option<PropKind>,
+ pub comp_kind: Option<CompKind>,
+}
+
+/// For SupportedCalendarComponentSet
+///
+/// Definition:
+///
+/// <!ELEMENT supported-calendar-component-set (comp+)>
+///
+/// Example:
+///
+/// <C:supported-calendar-component-set
+/// xmlns:C="urn:ietf:params:xml:ns:caldav">
+/// <C:comp name="VEVENT"/>
+/// <C:comp name="VTODO"/>
+/// </C:supported-calendar-component-set>
+#[derive(Debug, PartialEq, Clone)]
+pub struct CompSupport(pub Component);
+
+/// Name: allcomp
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies that all components shall be returned.
+///
+/// Description: The CALDAV:allcomp XML element can be used when the
+/// client wants all types of components returned by a calendaring
+/// REPORT request.
+///
+/// Definition:
+///
+/// <!ELEMENT allcomp EMPTY>
+#[derive(Debug, PartialEq, Clone)]
+pub enum CompKind {
+ AllComp,
+ Comp(Vec<Comp>),
+}
+
+/// Name: allprop
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies that all properties shall be returned.
+///
+/// Description: The CALDAV:allprop XML element can be used when the
+/// client wants all properties of components returned by a
+/// calendaring REPORT request.
+///
+/// Definition:
+///
+/// <!ELEMENT allprop EMPTY>
+///
+/// Note: The CALDAV:allprop element has the same name as the DAV:
+/// allprop element defined in [RFC2518]. However, the CALDAV:allprop
+/// element is defined in the "urn:ietf:params:xml:ns:caldav"
+/// namespace instead of the "DAV:" namespace.
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropKind {
+ AllProp,
+ Prop(Vec<CalProp>),
+}
+
+/// Name: prop
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Defines which properties to return in the response.
+///
+/// Description: The "name" attribute specifies the name of the calendar
+/// property to return (e.g., ATTENDEE). The "novalue" attribute can
+/// be used by clients to request that the actual value of the
+/// property not be returned (if the "novalue" attribute is set to
+/// "yes"). In that case, the server will return just the iCalendar
+/// property name and any iCalendar parameters and a trailing ":"
+/// without the subsequent value data.
+///
+/// Definition:
+/// <!ELEMENT prop EMPTY>
+/// <!ATTLIST prop name CDATA #REQUIRED novalue (yes | no) "no">
+/// name value: a calendar property name
+/// novalue value: "yes" or "no"
+///
+/// Note: The CALDAV:prop element has the same name as the DAV:prop
+/// element defined in [RFC2518]. However, the CALDAV:prop element is
+/// defined in the "urn:ietf:params:xml:ns:caldav" namespace instead
+/// of the "DAV:" namespace.
+#[derive(Debug, PartialEq, Clone)]
+pub struct CalProp {
+ pub name: ComponentProperty,
+ pub novalue: Option<bool>,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum RecurrenceModifier {
+ Expand(Expand),
+ LimitRecurrenceSet(LimitRecurrenceSet),
+}
+
+/// Name: expand
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Forces the server to expand recurring components into
+/// individual recurrence instances.
+///
+/// Description: The CALDAV:expand XML element specifies that for a
+/// given calendaring REPORT request, the server MUST expand the
+/// recurrence set into calendar components that define exactly one
+/// recurrence instance, and MUST return only those whose scheduled
+/// time intersect a specified time range.
+///
+/// The "start" attribute specifies the inclusive start of the time
+/// range, and the "end" attribute specifies the non-inclusive end of
+/// the time range. Both attributes are specified as date with UTC
+/// time value. The value of the "end" attribute MUST be greater than
+/// the value of the "start" attribute.
+///
+/// The server MUST use the same logic as defined for CALDAV:time-
+/// range to determine if a recurrence instance intersects the
+/// specified time range.
+///
+/// Recurring components, other than the initial instance, MUST
+/// include a RECURRENCE-ID property indicating which instance they
+/// refer to.
+///
+/// The returned calendar components MUST NOT use recurrence
+/// properties (i.e., EXDATE, EXRULE, RDATE, and RRULE) and MUST NOT
+/// have reference to or include VTIMEZONE components. Date and local
+/// time with reference to time zone information MUST be converted
+/// into date with UTC time.
+///
+/// Definition:
+///
+/// <!ELEMENT expand EMPTY>
+/// <!ATTLIST expand start CDATA #REQUIRED
+/// end CDATA #REQUIRED>
+/// start value: an iCalendar "date with UTC time"
+/// end value: an iCalendar "date with UTC time"
+#[derive(Debug, PartialEq, Clone)]
+pub struct Expand(pub DateTime<Utc>, pub DateTime<Utc>);
+
+/// CALDAV:limit-recurrence-set XML Element
+///
+/// Name: limit-recurrence-set
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies a time range to limit the set of "overridden
+/// components" returned by the server.
+///
+/// Description: The CALDAV:limit-recurrence-set XML element specifies
+/// that for a given calendaring REPORT request, the server MUST
+/// return, in addition to the "master component", only the
+/// "overridden components" that impact a specified time range. An
+/// overridden component impacts a time range if its current start and
+/// end times overlap the time range, or if the original start and end
+/// times -- the ones that would have been used if the instance were
+/// not overridden -- overlap the time range.
+///
+/// The "start" attribute specifies the inclusive start of the time
+/// range, and the "end" attribute specifies the non-inclusive end of
+/// the time range. Both attributes are specified as date with UTC
+/// time value. The value of the "end" attribute MUST be greater than
+/// the value of the "start" attribute.
+///
+/// The server MUST use the same logic as defined for CALDAV:time-
+/// range to determine if the current or original scheduled time of an
+/// "overridden" recurrence instance intersects the specified time
+/// range.
+///
+/// Overridden components that have a RANGE parameter on their
+/// RECURRENCE-ID property may specify one or more instances in the
+/// recurrence set, and some of those instances may fall within the
+/// specified time range or may have originally fallen within the
+/// specified time range prior to being overridden. If that is the
+/// case, the overridden component MUST be included in the results, as
+/// it has a direct impact on the interpretation of instances within
+/// the specified time range.
+///
+/// Definition:
+///
+/// <!ELEMENT limit-recurrence-set EMPTY>
+/// <!ATTLIST limit-recurrence-set start CDATA #REQUIRED
+/// end CDATA #REQUIRED>
+/// start value: an iCalendar "date with UTC time"
+/// end value: an iCalendar "date with UTC time"
+#[derive(Debug, PartialEq, Clone)]
+pub struct LimitRecurrenceSet(pub DateTime<Utc>, pub DateTime<Utc>);
+
+/// Name: limit-freebusy-set
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies a time range to limit the set of FREEBUSY values
+/// returned by the server.
+///
+/// Description: The CALDAV:limit-freebusy-set XML element specifies
+/// that for a given calendaring REPORT request, the server MUST only
+/// return the FREEBUSY property values of a VFREEBUSY component that
+/// intersects a specified time range.
+///
+/// The "start" attribute specifies the inclusive start of the time
+/// range, and the "end" attribute specifies the non-inclusive end of
+/// the time range. Both attributes are specified as "date with UTC
+/// time" value. The value of the "end" attribute MUST be greater
+/// than the value of the "start" attribute.
+///
+/// The server MUST use the same logic as defined for CALDAV:time-
+/// range to determine if a FREEBUSY property value intersects the
+/// specified time range.
+///
+/// Definition:
+/// <!ELEMENT limit-freebusy-set EMPTY>
+/// <!ATTLIST limit-freebusy-set start CDATA #REQUIRED
+/// end CDATA #REQUIRED>
+/// start value: an iCalendar "date with UTC time"
+/// end value: an iCalendar "date with UTC time"
+#[derive(Debug, PartialEq, Clone)]
+pub struct LimitFreebusySet(pub DateTime<Utc>, pub DateTime<Utc>);
+
+/// Used by CalendarQuery & CalendarMultiget
+#[derive(Debug, PartialEq, Clone)]
+pub enum CalendarSelector<E: dav::Extension> {
+ AllProp,
+ PropName,
+ Prop(dav::PropName<E>),
+}
+
+/// Name: comp-filter
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies search criteria on calendar components.
+///
+/// Description: The CALDAV:comp-filter XML element specifies a query
+/// targeted at the calendar object (i.e., VCALENDAR) or at a specific
+/// calendar component type (e.g., VEVENT). The scope of the
+/// CALDAV:comp-filter XML element is the calendar object when used as
+/// a child of the CALDAV:filter XML element. The scope of the
+/// CALDAV:comp-filter XML element is the enclosing calendar component
+/// when used as a child of another CALDAV:comp-filter XML element. A
+/// CALDAV:comp-filter is said to match if:
+///
+/// * The CALDAV:comp-filter XML element is empty and the calendar
+/// object or calendar component type specified by the "name"
+/// attribute exists in the current scope;
+///
+/// or:
+///
+/// * The CALDAV:comp-filter XML element contains a CALDAV:is-not-
+/// defined XML element and the calendar object or calendar
+/// component type specified by the "name" attribute does not exist
+/// in the current scope;
+///
+/// or:
+///
+/// * The CALDAV:comp-filter XML element contains a CALDAV:time-range
+/// XML element and at least one recurrence instance in the
+/// targeted calendar component is scheduled to overlap the
+/// specified time range, and all specified CALDAV:prop-filter and
+/// CALDAV:comp-filter child XML elements also match the targeted
+/// calendar component;
+///
+/// or:
+///
+/// * The CALDAV:comp-filter XML element only contains CALDAV:prop-
+/// filter and CALDAV:comp-filter child XML elements that all match
+/// the targeted calendar component.
+///
+/// Definition:
+///
+/// ```xmlschema
+/// <!ELEMENT comp-filter (is-not-defined | (time-range?,
+/// prop-filter*, comp-filter*))>
+///
+/// <!ATTLIST comp-filter name CDATA #REQUIRED>
+/// name value: a calendar object or calendar component
+/// type (e.g., VEVENT)
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+pub struct CompFilter {
+ pub name: Component,
+ // Option 1 = None, Option 2, 3, 4 = Some
+ pub additional_rules: Option<CompFilterRules>,
+}
+#[derive(Debug, PartialEq, Clone)]
+pub enum CompFilterRules {
+ // Option 2
+ IsNotDefined,
+ // Options 3 & 4
+ Matches(CompFilterMatch),
+}
+#[derive(Debug, PartialEq, Clone)]
+pub struct CompFilterMatch {
+ pub time_range: Option<TimeRange>,
+ pub prop_filter: Vec<PropFilter>,
+ pub comp_filter: Vec<CompFilter>,
+}
+
+/// Name: prop-filter
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies search criteria on calendar properties.
+///
+/// Description: The CALDAV:prop-filter XML element specifies a query
+/// targeted at a specific calendar property (e.g., CATEGORIES) in the
+/// scope of the enclosing calendar component. A calendar property is
+/// said to match a CALDAV:prop-filter if:
+///
+/// * The CALDAV:prop-filter XML element is empty and a property of
+/// the type specified by the "name" attribute exists in the
+/// enclosing calendar component;
+///
+/// or:
+///
+/// * The CALDAV:prop-filter XML element contains a CALDAV:is-not-
+/// defined XML element and no property of the type specified by
+/// the "name" attribute exists in the enclosing calendar
+/// component;
+///
+/// or:
+///
+/// * The CALDAV:prop-filter XML element contains a CALDAV:time-range
+/// XML element and the property value overlaps the specified time
+/// range, and all specified CALDAV:param-filter child XML elements
+/// also match the targeted property;
+///
+/// or:
+///
+/// * The CALDAV:prop-filter XML element contains a CALDAV:text-match
+/// XML element and the property value matches it, and all
+/// specified CALDAV:param-filter child XML elements also match the
+/// targeted property;
+///
+/// Definition:
+///
+/// ```xmlschema
+/// <!ELEMENT prop-filter (is-not-defined |
+/// ((time-range | text-match)?,
+/// param-filter*))>
+///
+/// <!ATTLIST prop-filter name CDATA #REQUIRED>
+/// name value: a calendar property name (e.g., ATTENDEE)
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropFilter {
+ pub name: ComponentProperty,
+ // None = Option 1, Some() = Option 2, 3 & 4
+ pub additional_rules: Option<PropFilterRules>,
+}
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropFilterRules {
+ // Option 2
+ IsNotDefined,
+ // Options 3 & 4
+ Match(PropFilterMatch),
+}
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropFilterMatch {
+ pub time_or_text: Option<TimeOrText>,
+ pub param_filter: Vec<ParamFilter>,
+}
+#[derive(Debug, PartialEq, Clone)]
+pub enum TimeOrText {
+ Time(TimeRange),
+ Text(TextMatch),
+}
+
+/// Name: text-match
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies a substring match on a property or parameter
+/// value.
+///
+/// Description: The CALDAV:text-match XML element specifies text used
+/// for a substring match against the property or parameter value
+/// specified in a calendaring REPORT request.
+///
+/// The "collation" attribute is used to select the collation that the
+/// server MUST use for character string matching. In the absence of
+/// this attribute, the server MUST use the "i;ascii-casemap"
+/// collation.
+///
+/// The "negate-condition" attribute is used to indicate that this
+/// test returns a match if the text matches when the attribute value
+/// is set to "no", or return a match if the text does not match, if
+/// the attribute value is set to "yes". For example, this can be
+/// used to match components with a STATUS property not set to
+/// CANCELLED.
+///
+/// Definition:
+/// <!ELEMENT text-match (#PCDATA)>
+/// PCDATA value: string
+/// <!ATTLIST text-match collation CDATA "i;ascii-casemap"
+/// negate-condition (yes | no) "no">
+#[derive(Debug, PartialEq, Clone)]
+pub struct TextMatch {
+ pub collation: Option<Collation>,
+ pub negate_condition: Option<bool>,
+ pub text: String,
+}
+
+/// Name: param-filter
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Limits the search to specific parameter values.
+///
+/// Description: The CALDAV:param-filter XML element specifies a query
+/// targeted at a specific calendar property parameter (e.g.,
+/// PARTSTAT) in the scope of the calendar property on which it is
+/// defined. A calendar property parameter is said to match a CALDAV:
+/// param-filter if:
+///
+/// * The CALDAV:param-filter XML element is empty and a parameter of
+/// the type specified by the "name" attribute exists on the
+/// calendar property being examined;
+///
+/// or:
+///
+/// * The CALDAV:param-filter XML element contains a CALDAV:is-not-
+/// defined XML element and no parameter of the type specified by
+/// the "name" attribute exists on the calendar property being
+/// examined;
+///
+/// Definition:
+///
+/// ```xmlschema
+/// <!ELEMENT param-filter (is-not-defined | text-match?)>
+///
+/// <!ATTLIST param-filter name CDATA #REQUIRED>
+/// name value: a property parameter name (e.g., PARTSTAT)
+/// ```
+#[derive(Debug, PartialEq, Clone)]
+pub struct ParamFilter {
+ pub name: PropertyParameter,
+ pub additional_rules: Option<ParamFilterMatch>,
+}
+#[derive(Debug, PartialEq, Clone)]
+pub enum ParamFilterMatch {
+ IsNotDefined,
+ Match(TextMatch),
+}
+
+/// CALDAV:is-not-defined XML Element
+///
+/// Name: is-not-defined
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies that a match should occur if the enclosing
+/// component, property, or parameter does not exist.
+///
+/// Description: The CALDAV:is-not-defined XML element specifies that a
+/// match occurs if the enclosing component, property, or parameter
+/// value specified in a calendaring REPORT request does not exist in
+/// the calendar data being tested.
+///
+/// Definition:
+/// <!ELEMENT is-not-defined EMPTY>
+/* CURRENTLY INLINED */
+
+/// Name: timezone
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies the time zone component to use when determining
+/// the results of a report.
+///
+/// Description: The CALDAV:timezone XML element specifies that for a
+/// given calendaring REPORT request, the server MUST rely on the
+/// specified VTIMEZONE component instead of the CALDAV:calendar-
+/// timezone property of the calendar collection, in which the
+/// calendar object resource is contained to resolve "date" values and
+/// "date with local time" values (i.e., floating time) to "date with
+/// UTC time" values. The server will require this information to
+/// determine if a calendar component scheduled with "date" values or
+/// "date with local time" values intersects a CALDAV:time-range
+/// specified in a CALDAV:calendar-query REPORT.
+///
+/// Note: The iCalendar data embedded within the CALDAV:timezone XML
+/// element MUST follow the standard XML character data encoding
+/// rules, including use of &lt;, &gt;, &amp; etc. entity encoding or
+/// the use of a <![CDATA[ ... ]]> construct. In the later case, the
+///
+/// iCalendar data cannot contain the character sequence "]]>", which
+/// is the end delimiter for the CDATA section.
+///
+/// Definition:
+///
+/// <!ELEMENT timezone (#PCDATA)>
+/// PCDATA value: an iCalendar object with exactly one VTIMEZONE
+#[derive(Debug, PartialEq, Clone)]
+pub struct TimeZone(pub String);
+
+/// Name: filter
+///
+/// Namespace: urn:ietf:params:xml:ns:caldav
+///
+/// Purpose: Specifies a filter to limit the set of calendar components
+/// returned by the server.
+///
+/// Description: The CALDAV:filter XML element specifies the search
+/// filter used to limit the calendar components returned by a
+/// calendaring REPORT request.
+///
+/// Definition:
+/// <!ELEMENT filter (comp-filter)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct Filter(pub CompFilter);
+
+/// Name: time-range
+///
+/// Definition:
+///
+/// <!ELEMENT time-range EMPTY>
+/// <!ATTLIST time-range start CDATA #IMPLIED
+/// end CDATA #IMPLIED>
+/// start value: an iCalendar "date with UTC time"
+/// end value: an iCalendar "date with UTC time"
+#[derive(Debug, PartialEq, Clone)]
+pub enum TimeRange {
+ OnlyStart(DateTime<Utc>),
+ OnlyEnd(DateTime<Utc>),
+ FullRange(DateTime<Utc>, DateTime<Utc>),
+}
+
+// ----------------------- ENUM ATTRIBUTES ---------------------
+
+/// Known components
+#[derive(Debug, PartialEq, Clone)]
+pub enum Component {
+ VCalendar,
+ VJournal,
+ VFreeBusy,
+ VEvent,
+ VTodo,
+ VAlarm,
+ VTimeZone,
+ Unknown(String),
+}
+impl Component {
+ pub fn as_str<'a>(&'a self) -> &'a str {
+ match self {
+ Self::VCalendar => "VCALENDAR",
+ Self::VJournal => "VJOURNAL",
+ Self::VFreeBusy => "VFREEBUSY",
+ Self::VEvent => "VEVENT",
+ Self::VTodo => "VTODO",
+ Self::VAlarm => "VALARM",
+ Self::VTimeZone => "VTIMEZONE",
+ Self::Unknown(c) => c,
+ }
+ }
+ pub fn new(v: String) -> Self {
+ match v.as_str() {
+ "VCALENDAR" => Self::VCalendar,
+ "VJOURNAL" => Self::VJournal,
+ "VFREEBUSY" => Self::VFreeBusy,
+ "VEVENT" => Self::VEvent,
+ "VTODO" => Self::VTodo,
+ "VALARM" => Self::VAlarm,
+ "VTIMEZONE" => Self::VTimeZone,
+ _ => Self::Unknown(v),
+ }
+ }
+}
+
+/// name="VERSION", name="SUMMARY", etc.
+/// Can be set on different objects: VCalendar, VEvent, etc.
+/// Might be replaced by an enum later
+#[derive(Debug, PartialEq, Clone)]
+pub struct ComponentProperty(pub String);
+
+/// like PARSTAT
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropertyParameter(pub String);
+impl PropertyParameter {
+ pub fn as_str<'a>(&'a self) -> &'a str {
+ self.0.as_str()
+ }
+}
+
+#[derive(Default, Debug, PartialEq, Clone)]
+pub enum Collation {
+ #[default]
+ AsciiCaseMap,
+ Octet,
+ Unknown(String),
+}
+impl Collation {
+ pub fn as_str<'a>(&'a self) -> &'a str {
+ match self {
+ Self::AsciiCaseMap => "i;ascii-casemap",
+ Self::Octet => "i;octet",
+ Self::Unknown(c) => c.as_str(),
+ }
+ }
+ pub fn new(v: String) -> Self {
+ match v.as_str() {
+ "i;ascii-casemap" => Self::AsciiCaseMap,
+ "i;octet" => Self::Octet,
+ _ => Self::Unknown(v),
+ }
+ }
+}
diff --git a/aero-dav/src/decoder.rs b/aero-dav/src/decoder.rs
new file mode 100644
index 0000000..bb64455
--- /dev/null
+++ b/aero-dav/src/decoder.rs
@@ -0,0 +1,1152 @@
+use chrono::DateTime;
+use quick_xml::events::Event;
+
+use super::error::ParsingError;
+use super::types::*;
+use super::xml::{IRead, Node, QRead, Reader, DAV_URN};
+
+//@TODO (1) Rewrite all objects as Href,
+// where we return Ok(None) instead of trying to find the object at any cost.
+// Add a xml.find<E: Qread>() -> Result<Option<E>, ParsingError> or similar for the cases we
+// really need the object
+// (2) Rewrite QRead and replace Result<Option<_>, _> with Result<_, _>, not found being a possible
+// error.
+// (3) Rewrite vectors with xml.collect<E: QRead>() -> Result<Vec<E>, _>
+// (4) Something for alternatives like xml::choices on some lib would be great but no idea yet
+
+// ---- ROOT ----
+
+/// Propfind request
+impl<E: Extension> QRead<PropFind<E>> for PropFind<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "propfind").await?;
+ let propfind: PropFind<E> = loop {
+ // allprop
+ if let Some(_) = xml.maybe_open(DAV_URN, "allprop").await? {
+ xml.close().await?;
+ let includ = xml.maybe_find::<Include<E>>().await?;
+ break PropFind::AllProp(includ);
+ }
+
+ // propname
+ if let Some(_) = xml.maybe_open(DAV_URN, "propname").await? {
+ xml.close().await?;
+ break PropFind::PropName;
+ }
+
+ // prop
+ let (mut maybe_prop, mut dirty) = (None, false);
+ xml.maybe_read::<PropName<E>>(&mut maybe_prop, &mut dirty)
+ .await?;
+ if let Some(prop) = maybe_prop {
+ break PropFind::Prop(prop);
+ }
+
+ // not found, skipping
+ xml.skip().await?;
+ };
+ xml.close().await?;
+
+ Ok(propfind)
+ }
+}
+
+/// PROPPATCH request
+impl<E: Extension> QRead<PropertyUpdate<E>> for PropertyUpdate<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "propertyupdate").await?;
+ let collected_items = xml.collect::<PropertyUpdateItem<E>>().await?;
+ xml.close().await?;
+ Ok(PropertyUpdate(collected_items))
+ }
+}
+
+/// Generic response
+impl<E: Extension> QRead<Multistatus<E>> for Multistatus<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "multistatus").await?;
+ let mut responses = Vec::new();
+ let mut responsedescription = None;
+ let mut extension = None;
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_push(&mut responses, &mut dirty).await?;
+ xml.maybe_read(&mut responsedescription, &mut dirty).await?;
+ xml.maybe_read(&mut extension, &mut dirty).await?;
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ Ok(Multistatus {
+ responses,
+ responsedescription,
+ extension,
+ })
+ }
+}
+
+// LOCK REQUEST
+impl QRead<LockInfo> for LockInfo {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "lockinfo").await?;
+ let (mut m_scope, mut m_type, mut owner) = (None, None, None);
+ loop {
+ let mut dirty = false;
+ xml.maybe_read::<LockScope>(&mut m_scope, &mut dirty)
+ .await?;
+ xml.maybe_read::<LockType>(&mut m_type, &mut dirty).await?;
+ xml.maybe_read::<Owner>(&mut owner, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+ xml.close().await?;
+ match (m_scope, m_type) {
+ (Some(lockscope), Some(locktype)) => Ok(LockInfo {
+ lockscope,
+ locktype,
+ owner,
+ }),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+// LOCK RESPONSE
+impl<E: Extension> QRead<PropValue<E>> for PropValue<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ println!("---- propvalue");
+ xml.open(DAV_URN, "prop").await?;
+ let acc = xml.collect::<Property<E>>().await?;
+ xml.close().await?;
+ Ok(PropValue(acc))
+ }
+}
+
+/// Error response
+impl<E: Extension> QRead<Error<E>> for Error<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "error").await?;
+ let violations = xml.collect::<Violation<E>>().await?;
+ xml.close().await?;
+ Ok(Error(violations))
+ }
+}
+
+// ---- INNER XML
+impl<E: Extension> QRead<Response<E>> for Response<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "response").await?;
+ let (mut status, mut error, mut responsedescription, mut location) =
+ (None, None, None, None);
+ let mut href = Vec::new();
+ let mut propstat = Vec::new();
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read::<Status>(&mut status, &mut dirty).await?;
+ xml.maybe_push::<Href>(&mut href, &mut dirty).await?;
+ xml.maybe_push::<PropStat<E>>(&mut propstat, &mut dirty)
+ .await?;
+ xml.maybe_read::<Error<E>>(&mut error, &mut dirty).await?;
+ xml.maybe_read::<ResponseDescription>(&mut responsedescription, &mut dirty)
+ .await?;
+ xml.maybe_read::<Location>(&mut location, &mut dirty)
+ .await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ match (status, &propstat[..], &href[..]) {
+ (Some(status), &[], &[_, ..]) => Ok(Response {
+ status_or_propstat: StatusOrPropstat::Status(href, status),
+ error,
+ responsedescription,
+ location,
+ }),
+ (None, &[_, ..], &[_, ..]) => Ok(Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ href.into_iter().next().unwrap(),
+ propstat,
+ ),
+ error,
+ responsedescription,
+ location,
+ }),
+ (Some(_), &[_, ..], _) => Err(ParsingError::InvalidValue),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+impl<E: Extension> QRead<PropStat<E>> for PropStat<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "propstat").await?;
+
+ let (mut m_any_prop, mut m_status, mut error, mut responsedescription) =
+ (None, None, None, None);
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read::<AnyProp<E>>(&mut m_any_prop, &mut dirty)
+ .await?;
+ xml.maybe_read::<Status>(&mut m_status, &mut dirty).await?;
+ xml.maybe_read::<Error<E>>(&mut error, &mut dirty).await?;
+ xml.maybe_read::<ResponseDescription>(&mut responsedescription, &mut dirty)
+ .await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ match (m_any_prop, m_status) {
+ (Some(prop), Some(status)) => Ok(PropStat {
+ prop,
+ status,
+ error,
+ responsedescription,
+ }),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+impl QRead<Status> for Status {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "status").await?;
+ let fullcode = xml.tag_string().await?;
+ let txtcode = fullcode
+ .splitn(3, ' ')
+ .nth(1)
+ .ok_or(ParsingError::InvalidValue)?;
+ let code = http::status::StatusCode::from_bytes(txtcode.as_bytes())
+ .or(Err(ParsingError::InvalidValue))?;
+ xml.close().await?;
+ Ok(Status(code))
+ }
+}
+
+impl QRead<ResponseDescription> for ResponseDescription {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "responsedescription").await?;
+ let cnt = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(ResponseDescription(cnt))
+ }
+}
+
+impl QRead<Location> for Location {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "location").await?;
+ let href = xml.find::<Href>().await?;
+ xml.close().await?;
+ Ok(Location(href))
+ }
+}
+
+impl<E: Extension> QRead<PropertyUpdateItem<E>> for PropertyUpdateItem<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ match Remove::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(PropertyUpdateItem::Remove),
+ }
+ Set::qread(xml).await.map(PropertyUpdateItem::Set)
+ }
+}
+
+impl<E: Extension> QRead<Remove<E>> for Remove<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "remove").await?;
+ let propname = xml.find::<PropName<E>>().await?;
+ xml.close().await?;
+ Ok(Remove(propname))
+ }
+}
+
+impl<E: Extension> QRead<Set<E>> for Set<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "set").await?;
+ let propvalue = xml.find::<PropValue<E>>().await?;
+ xml.close().await?;
+ Ok(Set(propvalue))
+ }
+}
+
+impl<E: Extension> QRead<Violation<E>> for Violation<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open(DAV_URN, "lock-token-matches-request-uri")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Violation::LockTokenMatchesRequestUri)
+ } else if xml
+ .maybe_open(DAV_URN, "lock-token-submitted")
+ .await?
+ .is_some()
+ {
+ let links = xml.collect::<Href>().await?;
+ xml.close().await?;
+ Ok(Violation::LockTokenSubmitted(links))
+ } else if xml
+ .maybe_open(DAV_URN, "no-conflicting-lock")
+ .await?
+ .is_some()
+ {
+ let links = xml.collect::<Href>().await?;
+ xml.close().await?;
+ Ok(Violation::NoConflictingLock(links))
+ } else if xml
+ .maybe_open(DAV_URN, "no-external-entities")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Violation::NoExternalEntities)
+ } else if xml
+ .maybe_open(DAV_URN, "preserved-live-properties")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Violation::PreservedLiveProperties)
+ } else if xml
+ .maybe_open(DAV_URN, "propfind-finite-depth")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Violation::PropfindFiniteDepth)
+ } else if xml
+ .maybe_open(DAV_URN, "cannot-modify-protected-property")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ Ok(Violation::CannotModifyProtectedProperty)
+ } else {
+ E::Error::qread(xml).await.map(Violation::Extension)
+ }
+ }
+}
+
+impl<E: Extension> QRead<Include<E>> for Include<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "include").await?;
+ let acc = xml.collect::<PropertyRequest<E>>().await?;
+ xml.close().await?;
+ Ok(Include(acc))
+ }
+}
+
+impl<E: Extension> QRead<PropName<E>> for PropName<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "prop").await?;
+ let acc = xml.collect::<PropertyRequest<E>>().await?;
+ xml.close().await?;
+ Ok(PropName(acc))
+ }
+}
+
+impl<E: Extension> QRead<AnyProp<E>> for AnyProp<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "prop").await?;
+ let acc = xml.collect::<AnyProperty<E>>().await?;
+ xml.close().await?;
+ Ok(AnyProp(acc))
+ }
+}
+
+impl<E: Extension> QRead<AnyProperty<E>> for AnyProperty<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ match Property::qread(xml).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Self::Value),
+ }
+ PropertyRequest::qread(xml).await.map(Self::Request)
+ }
+}
+
+impl<E: Extension> QRead<PropertyRequest<E>> for PropertyRequest<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let maybe = if xml.maybe_open(DAV_URN, "creationdate").await?.is_some() {
+ Some(PropertyRequest::CreationDate)
+ } else if xml.maybe_open(DAV_URN, "displayname").await?.is_some() {
+ Some(PropertyRequest::DisplayName)
+ } else if xml
+ .maybe_open(DAV_URN, "getcontentlanguage")
+ .await?
+ .is_some()
+ {
+ Some(PropertyRequest::GetContentLanguage)
+ } else if xml.maybe_open(DAV_URN, "getcontentlength").await?.is_some() {
+ Some(PropertyRequest::GetContentLength)
+ } else if xml.maybe_open(DAV_URN, "getcontenttype").await?.is_some() {
+ Some(PropertyRequest::GetContentType)
+ } else if xml.maybe_open(DAV_URN, "getetag").await?.is_some() {
+ Some(PropertyRequest::GetEtag)
+ } else if xml.maybe_open(DAV_URN, "getlastmodified").await?.is_some() {
+ Some(PropertyRequest::GetLastModified)
+ } else if xml.maybe_open(DAV_URN, "lockdiscovery").await?.is_some() {
+ Some(PropertyRequest::LockDiscovery)
+ } else if xml.maybe_open(DAV_URN, "resourcetype").await?.is_some() {
+ Some(PropertyRequest::ResourceType)
+ } else if xml.maybe_open(DAV_URN, "supportedlock").await?.is_some() {
+ Some(PropertyRequest::SupportedLock)
+ } else {
+ None
+ };
+
+ match maybe {
+ Some(pr) => {
+ xml.close().await?;
+ Ok(pr)
+ }
+ None => E::PropertyRequest::qread(xml)
+ .await
+ .map(PropertyRequest::Extension),
+ }
+ }
+}
+
+impl<E: Extension> QRead<Property<E>> for Property<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ // Core WebDAV properties
+ if xml
+ .maybe_open_start(DAV_URN, "creationdate")
+ .await?
+ .is_some()
+ {
+ let datestr = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::CreationDate(DateTime::parse_from_rfc3339(
+ datestr.as_str(),
+ )?));
+ } else if xml
+ .maybe_open_start(DAV_URN, "displayname")
+ .await?
+ .is_some()
+ {
+ let name = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::DisplayName(name));
+ } else if xml
+ .maybe_open_start(DAV_URN, "getcontentlanguage")
+ .await?
+ .is_some()
+ {
+ let lang = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::GetContentLanguage(lang));
+ } else if xml
+ .maybe_open_start(DAV_URN, "getcontentlength")
+ .await?
+ .is_some()
+ {
+ let cl = xml.tag_string().await?.parse::<u64>()?;
+ xml.close().await?;
+ return Ok(Property::GetContentLength(cl));
+ } else if xml
+ .maybe_open_start(DAV_URN, "getcontenttype")
+ .await?
+ .is_some()
+ {
+ let ct = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::GetContentType(ct));
+ } else if xml.maybe_open_start(DAV_URN, "getetag").await?.is_some() {
+ let etag = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::GetEtag(etag));
+ } else if xml
+ .maybe_open_start(DAV_URN, "getlastmodified")
+ .await?
+ .is_some()
+ {
+ let datestr = xml.tag_string().await?;
+ xml.close().await?;
+ return Ok(Property::GetLastModified(DateTime::parse_from_rfc2822(
+ datestr.as_str(),
+ )?));
+ } else if xml
+ .maybe_open_start(DAV_URN, "lockdiscovery")
+ .await?
+ .is_some()
+ {
+ let acc = xml.collect::<ActiveLock>().await?;
+ xml.close().await?;
+ return Ok(Property::LockDiscovery(acc));
+ } else if xml
+ .maybe_open_start(DAV_URN, "resourcetype")
+ .await?
+ .is_some()
+ {
+ let acc = xml.collect::<ResourceType<E>>().await?;
+ xml.close().await?;
+ return Ok(Property::ResourceType(acc));
+ } else if xml
+ .maybe_open_start(DAV_URN, "supportedlock")
+ .await?
+ .is_some()
+ {
+ let acc = xml.collect::<LockEntry>().await?;
+ xml.close().await?;
+ return Ok(Property::SupportedLock(acc));
+ }
+
+ // Option 2: an extension property, delegating
+ E::Property::qread(xml).await.map(Property::Extension)
+ }
+}
+
+impl QRead<ActiveLock> for ActiveLock {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "activelock").await?;
+ let (
+ mut m_scope,
+ mut m_type,
+ mut m_depth,
+ mut owner,
+ mut timeout,
+ mut locktoken,
+ mut m_root,
+ ) = (None, None, None, None, None, None, None);
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read::<LockScope>(&mut m_scope, &mut dirty)
+ .await?;
+ xml.maybe_read::<LockType>(&mut m_type, &mut dirty).await?;
+ xml.maybe_read::<Depth>(&mut m_depth, &mut dirty).await?;
+ xml.maybe_read::<Owner>(&mut owner, &mut dirty).await?;
+ xml.maybe_read::<Timeout>(&mut timeout, &mut dirty).await?;
+ xml.maybe_read::<LockToken>(&mut locktoken, &mut dirty)
+ .await?;
+ xml.maybe_read::<LockRoot>(&mut m_root, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => {
+ xml.skip().await?;
+ }
+ }
+ }
+ }
+
+ xml.close().await?;
+ match (m_scope, m_type, m_depth, m_root) {
+ (Some(lockscope), Some(locktype), Some(depth), Some(lockroot)) => Ok(ActiveLock {
+ lockscope,
+ locktype,
+ depth,
+ owner,
+ timeout,
+ locktoken,
+ lockroot,
+ }),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+impl QRead<Depth> for Depth {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "depth").await?;
+ let depth_str = xml.tag_string().await?;
+ xml.close().await?;
+ match depth_str.as_str() {
+ "0" => Ok(Depth::Zero),
+ "1" => Ok(Depth::One),
+ "infinity" => Ok(Depth::Infinity),
+ _ => Err(ParsingError::WrongToken),
+ }
+ }
+}
+
+impl QRead<Owner> for Owner {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "owner").await?;
+
+ let mut owner = Owner::Unknown;
+ loop {
+ match xml.peek() {
+ Event::Text(_) | Event::CData(_) => {
+ let txt = xml.tag_string().await?;
+ if matches!(owner, Owner::Unknown) {
+ owner = Owner::Txt(txt);
+ }
+ }
+ Event::Start(_) | Event::Empty(_) => match Href::qread(xml).await {
+ Ok(href) => {
+ owner = Owner::Href(href);
+ }
+ Err(ParsingError::Recoverable) => {
+ xml.skip().await?;
+ }
+ Err(e) => return Err(e),
+ },
+ Event::End(_) => break,
+ _ => {
+ xml.skip().await?;
+ }
+ }
+ }
+ xml.close().await?;
+ Ok(owner)
+ }
+}
+
+impl QRead<Timeout> for Timeout {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ const SEC_PFX: &str = "Second-";
+ xml.open(DAV_URN, "timeout").await?;
+
+ let timeout = match xml.tag_string().await?.as_str() {
+ "Infinite" => Timeout::Infinite,
+ seconds => match seconds.strip_prefix(SEC_PFX) {
+ Some(secs) => Timeout::Seconds(secs.parse::<u32>()?),
+ None => return Err(ParsingError::InvalidValue),
+ },
+ };
+
+ xml.close().await?;
+ Ok(timeout)
+ }
+}
+
+impl QRead<LockToken> for LockToken {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "locktoken").await?;
+ let href = xml.find::<Href>().await?;
+ xml.close().await?;
+ Ok(LockToken(href))
+ }
+}
+
+impl QRead<LockRoot> for LockRoot {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "lockroot").await?;
+ let href = xml.find::<Href>().await?;
+ xml.close().await?;
+ Ok(LockRoot(href))
+ }
+}
+
+impl<E: Extension> QRead<ResourceType<E>> for ResourceType<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "collection").await?.is_some() {
+ xml.close().await?;
+ return Ok(ResourceType::Collection);
+ }
+
+ E::ResourceType::qread(xml)
+ .await
+ .map(ResourceType::Extension)
+ }
+}
+
+impl QRead<LockEntry> for LockEntry {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "lockentry").await?;
+ let (mut maybe_scope, mut maybe_type) = (None, None);
+
+ loop {
+ let mut dirty = false;
+ xml.maybe_read::<LockScope>(&mut maybe_scope, &mut dirty)
+ .await?;
+ xml.maybe_read::<LockType>(&mut maybe_type, &mut dirty)
+ .await?;
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ match (maybe_scope, maybe_type) {
+ (Some(lockscope), Some(locktype)) => Ok(LockEntry {
+ lockscope,
+ locktype,
+ }),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+impl QRead<LockScope> for LockScope {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "lockscope").await?;
+
+ let lockscope = loop {
+ if xml.maybe_open(DAV_URN, "exclusive").await?.is_some() {
+ xml.close().await?;
+ break LockScope::Exclusive;
+ }
+
+ if xml.maybe_open(DAV_URN, "shared").await?.is_some() {
+ xml.close().await?;
+ break LockScope::Shared;
+ }
+
+ xml.skip().await?;
+ };
+
+ xml.close().await?;
+ Ok(lockscope)
+ }
+}
+
+impl QRead<LockType> for LockType {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "locktype").await?;
+
+ let locktype = loop {
+ if xml.maybe_open(DAV_URN, "write").await?.is_some() {
+ xml.close().await?;
+ break LockType::Write;
+ }
+
+ xml.skip().await?;
+ };
+
+ xml.close().await?;
+ Ok(locktype)
+ }
+}
+
+impl QRead<Href> for Href {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "href").await?;
+ let url = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(Href(url))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::Core;
+ use chrono::{FixedOffset, TimeZone};
+ use quick_xml::reader::NsReader;
+
+ #[tokio::test]
+ async fn basic_propfind_propname() {
+ let src = r#"<?xml version="1.0" encoding="utf-8" ?>
+<rando/>
+<garbage><old/></garbage>
+<D:propfind xmlns:D="DAV:">
+ <D:propname/>
+</D:propfind>
+"#;
+
+ let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ let got = rdr.find::<PropFind<Core>>().await.unwrap();
+
+ assert_eq!(got, PropFind::<Core>::PropName);
+ }
+
+ #[tokio::test]
+ async fn basic_propfind_prop() {
+ let src = r#"<?xml version="1.0" encoding="utf-8" ?>
+<rando/>
+<garbage><old/></garbage>
+<D:propfind xmlns:D="DAV:">
+ <D:prop>
+ <D:displayname/>
+ <D:getcontentlength/>
+ <D:getcontenttype/>
+ <D:getetag/>
+ <D:getlastmodified/>
+ <D:resourcetype/>
+ <D:supportedlock/>
+ </D:prop>
+</D:propfind>
+"#;
+
+ let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ let got = rdr.find::<PropFind<Core>>().await.unwrap();
+
+ assert_eq!(
+ got,
+ PropFind::Prop(PropName(vec![
+ PropertyRequest::DisplayName,
+ PropertyRequest::GetContentLength,
+ PropertyRequest::GetContentType,
+ PropertyRequest::GetEtag,
+ PropertyRequest::GetLastModified,
+ PropertyRequest::ResourceType,
+ PropertyRequest::SupportedLock,
+ ]))
+ );
+ }
+
+ #[tokio::test]
+ async fn rfc_lock_error() {
+ let src = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <D:error xmlns:D="DAV:">
+ <D:lock-token-submitted>
+ <D:href>/locked/</D:href>
+ </D:lock-token-submitted>
+ </D:error>"#;
+
+ let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ let got = rdr.find::<Error<Core>>().await.unwrap();
+
+ assert_eq!(
+ got,
+ Error(vec![Violation::LockTokenSubmitted(vec![Href(
+ "/locked/".into()
+ )])])
+ );
+ }
+
+ #[tokio::test]
+ async fn rfc_propertyupdate() {
+ let src = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <D:propertyupdate xmlns:D="DAV:"
+ xmlns:Z="http://ns.example.com/standards/z39.50/">
+ <D:set>
+ <D:prop>
+ <Z:Authors>
+ <Z:Author>Jim Whitehead</Z:Author>
+ <Z:Author>Roy Fielding</Z:Author>
+ </Z:Authors>
+ </D:prop>
+ </D:set>
+ <D:remove>
+ <D:prop><Z:Copyright-Owner/></D:prop>
+ </D:remove>
+ </D:propertyupdate>"#;
+
+ let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ let got = rdr.find::<PropertyUpdate<Core>>().await.unwrap();
+
+ assert_eq!(
+ got,
+ PropertyUpdate(vec![
+ PropertyUpdateItem::Set(Set(PropValue(vec![]))),
+ PropertyUpdateItem::Remove(Remove(PropName(vec![]))),
+ ])
+ );
+ }
+
+ #[tokio::test]
+ async fn rfc_lockinfo() {
+ let src = r#"
+<?xml version="1.0" encoding="utf-8" ?>
+<D:lockinfo xmlns:D='DAV:'>
+ <D:lockscope><D:exclusive/></D:lockscope>
+ <D:locktype><D:write/></D:locktype>
+ <D:owner>
+ <D:href>http://example.org/~ejw/contact.html</D:href>
+ </D:owner>
+</D:lockinfo>
+"#;
+
+ let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ let got = rdr.find::<LockInfo>().await.unwrap();
+
+ assert_eq!(
+ got,
+ LockInfo {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ owner: Some(Owner::Href(Href(
+ "http://example.org/~ejw/contact.html".into()
+ ))),
+ }
+ );
+ }
+
+ #[tokio::test]
+ async fn rfc_multistatus_name() {
+ let src = r#"
+<?xml version="1.0" encoding="utf-8" ?>
+ <multistatus xmlns="DAV:">
+ <response>
+ <href>http://www.example.com/container/</href>
+ <propstat>
+ <prop xmlns:R="http://ns.example.com/boxschema/">
+ <R:bigbox/>
+ <R:author/>
+ <creationdate/>
+ <displayname/>
+ <resourcetype/>
+ <supportedlock/>
+ </prop>
+ <status>HTTP/1.1 200 OK</status>
+ </propstat>
+ </response>
+ <response>
+ <href>http://www.example.com/container/front.html</href>
+ <propstat>
+ <prop xmlns:R="http://ns.example.com/boxschema/">
+ <R:bigbox/>
+ <creationdate/>
+ <displayname/>
+ <getcontentlength/>
+ <getcontenttype/>
+ <getetag/>
+ <getlastmodified/>
+ <resourcetype/>
+ <supportedlock/>
+ </prop>
+ <status>HTTP/1.1 200 OK</status>
+ </propstat>
+ </response>
+ </multistatus>
+"#;
+
+ let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ let got = rdr.find::<Multistatus<Core>>().await.unwrap();
+
+ assert_eq!(
+ got,
+ Multistatus {
+ responses: vec![
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("http://www.example.com/container/".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Request(PropertyRequest::CreationDate),
+ AnyProperty::Request(PropertyRequest::DisplayName),
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Request(PropertyRequest::SupportedLock),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("http://www.example.com/container/front.html".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Request(PropertyRequest::CreationDate),
+ AnyProperty::Request(PropertyRequest::DisplayName),
+ AnyProperty::Request(PropertyRequest::GetContentLength),
+ AnyProperty::Request(PropertyRequest::GetContentType),
+ AnyProperty::Request(PropertyRequest::GetEtag),
+ AnyProperty::Request(PropertyRequest::GetLastModified),
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Request(PropertyRequest::SupportedLock),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ ],
+ responsedescription: None,
+ extension: None,
+ }
+ );
+ }
+
+ #[tokio::test]
+ async fn rfc_multistatus_value() {
+ let src = r#"
+ <?xml version="1.0" encoding="utf-8" ?>
+ <D:multistatus xmlns:D="DAV:">
+ <D:response>
+ <D:href>/container/</D:href>
+ <D:propstat>
+ <D:prop xmlns:R="http://ns.example.com/boxschema/">
+ <R:bigbox><R:BoxType>Box type A</R:BoxType></R:bigbox>
+ <R:author><R:Name>Hadrian</R:Name></R:author>
+ <D:creationdate>1997-12-01T17:42:21-08:00</D:creationdate>
+ <D:displayname>Example collection</D:displayname>
+ <D:resourcetype><D:collection/></D:resourcetype>
+ <D:supportedlock>
+ <D:lockentry>
+ <D:lockscope><D:exclusive/></D:lockscope>
+ <D:locktype><D:write/></D:locktype>
+ </D:lockentry>
+ <D:lockentry>
+ <D:lockscope><D:shared/></D:lockscope>
+ <D:locktype><D:write/></D:locktype>
+ </D:lockentry>
+ </D:supportedlock>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>/container/front.html</D:href>
+ <D:propstat>
+ <D:prop xmlns:R="http://ns.example.com/boxschema/">
+ <R:bigbox><R:BoxType>Box type B</R:BoxType>
+ </R:bigbox>
+ <D:creationdate>1997-12-01T18:27:21-08:00</D:creationdate>
+ <D:displayname>Example HTML resource</D:displayname>
+ <D:getcontentlength>4525</D:getcontentlength>
+ <D:getcontenttype>text/html</D:getcontenttype>
+ <D:getetag>"zzyzx"</D:getetag>
+ <D:getlastmodified
+ >Mon, 12 Jan 1998 09:25:56 GMT</D:getlastmodified>
+ <D:resourcetype/>
+ <D:supportedlock>
+ <D:lockentry>
+ <D:lockscope><D:exclusive/></D:lockscope>
+ <D:locktype><D:write/></D:locktype>
+ </D:lockentry>
+ <D:lockentry>
+ <D:lockscope><D:shared/></D:lockscope>
+ <D:locktype><D:write/></D:locktype>
+ </D:lockentry>
+ </D:supportedlock>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ </D:multistatus>"#;
+
+ let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ let got = rdr.find::<Multistatus<Core>>().await.unwrap();
+
+ assert_eq!(
+ got,
+ Multistatus {
+ extension: None,
+ responses: vec![
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("/container/".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Value(Property::CreationDate(
+ FixedOffset::west_opt(8 * 3600)
+ .unwrap()
+ .with_ymd_and_hms(1997, 12, 01, 17, 42, 21)
+ .unwrap()
+ )),
+ AnyProperty::Value(Property::DisplayName(
+ "Example collection".into()
+ )),
+ AnyProperty::Value(Property::ResourceType(vec![
+ ResourceType::Collection
+ ])),
+ AnyProperty::Value(Property::SupportedLock(vec![
+ LockEntry {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ },
+ LockEntry {
+ lockscope: LockScope::Shared,
+ locktype: LockType::Write,
+ },
+ ])),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("/container/front.html".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Value(Property::CreationDate(
+ FixedOffset::west_opt(8 * 3600)
+ .unwrap()
+ .with_ymd_and_hms(1997, 12, 01, 18, 27, 21)
+ .unwrap()
+ )),
+ AnyProperty::Value(Property::DisplayName(
+ "Example HTML resource".into()
+ )),
+ AnyProperty::Value(Property::GetContentLength(4525)),
+ AnyProperty::Value(Property::GetContentType(
+ "text/html".into()
+ )),
+ AnyProperty::Value(Property::GetEtag(r#""zzyzx""#.into())),
+ AnyProperty::Value(Property::GetLastModified(
+ FixedOffset::west_opt(0)
+ .unwrap()
+ .with_ymd_and_hms(1998, 01, 12, 09, 25, 56)
+ .unwrap()
+ )),
+ //@FIXME know bug, can't disambiguate between an empty resource
+ //type value and a request resource type
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Value(Property::SupportedLock(vec![
+ LockEntry {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ },
+ LockEntry {
+ lockscope: LockScope::Shared,
+ locktype: LockType::Write,
+ },
+ ])),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ ],
+ responsedescription: None,
+ }
+ );
+ }
+}
diff --git a/aero-dav/src/encoder.rs b/aero-dav/src/encoder.rs
new file mode 100644
index 0000000..6c77aa6
--- /dev/null
+++ b/aero-dav/src/encoder.rs
@@ -0,0 +1,1262 @@
+use super::types::*;
+use super::xml::{IWrite, Node, QWrite, Writer};
+use quick_xml::events::{BytesText, Event};
+use quick_xml::Error as QError;
+
+// --- XML ROOTS
+
+/// PROPFIND REQUEST
+impl<E: Extension> QWrite for PropFind<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("propfind");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::PropName => {
+ let empty_propname = xml.create_dav_element("propname");
+ xml.q
+ .write_event_async(Event::Empty(empty_propname))
+ .await?
+ }
+ Self::AllProp(maybe_include) => {
+ let empty_allprop = xml.create_dav_element("allprop");
+ xml.q.write_event_async(Event::Empty(empty_allprop)).await?;
+ if let Some(include) = maybe_include {
+ include.qwrite(xml).await?;
+ }
+ }
+ Self::Prop(propname) => propname.qwrite(xml).await?,
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+/// PROPPATCH REQUEST
+impl<E: Extension> QWrite for PropertyUpdate<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("propertyupdate");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for update in self.0.iter() {
+ update.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+/// PROPFIND RESPONSE, PROPPATCH RESPONSE, COPY RESPONSE, MOVE RESPONSE
+/// DELETE RESPONSE,
+impl<E: Extension> QWrite for Multistatus<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("multistatus");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for response in self.responses.iter() {
+ response.qwrite(xml).await?;
+ }
+ if let Some(description) = &self.responsedescription {
+ description.qwrite(xml).await?;
+ }
+ if let Some(extension) = &self.extension {
+ extension.qwrite(xml).await?;
+ }
+
+ xml.q.write_event_async(Event::End(end)).await?;
+ Ok(())
+ }
+}
+
+/// LOCK REQUEST
+impl QWrite for LockInfo {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("lockinfo");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.lockscope.qwrite(xml).await?;
+ self.locktype.qwrite(xml).await?;
+ if let Some(owner) = &self.owner {
+ owner.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+/// SOME LOCK RESPONSES
+impl<E: Extension> QWrite for PropValue<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("prop");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for propval in &self.0 {
+ propval.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+/// Error response
+impl<E: Extension> QWrite for Error<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("error");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for violation in &self.0 {
+ violation.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+// --- XML inner elements
+impl<E: Extension> QWrite for PropertyUpdateItem<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Set(set) => set.qwrite(xml).await,
+ Self::Remove(rm) => rm.qwrite(xml).await,
+ }
+ }
+}
+
+impl<E: Extension> QWrite for Set<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("set");
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for Remove<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("remove");
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for PropName<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("prop");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for propname in &self.0 {
+ propname.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for AnyProp<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("prop");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for propname in &self.0 {
+ propname.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for AnyProperty<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Request(v) => v.qwrite(xml).await,
+ Self::Value(v) => v.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for Href {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("href");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&self.0)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for Response<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("response");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.status_or_propstat.qwrite(xml).await?;
+ if let Some(error) = &self.error {
+ error.qwrite(xml).await?;
+ }
+ if let Some(responsedescription) = &self.responsedescription {
+ responsedescription.qwrite(xml).await?;
+ }
+ if let Some(location) = &self.location {
+ location.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for StatusOrPropstat<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Status(many_href, status) => {
+ for href in many_href.iter() {
+ href.qwrite(xml).await?;
+ }
+ status.qwrite(xml).await
+ }
+ Self::PropStat(href, propstat_list) => {
+ href.qwrite(xml).await?;
+ for propstat in propstat_list.iter() {
+ propstat.qwrite(xml).await?;
+ }
+ Ok(())
+ }
+ }
+ }
+}
+
+impl QWrite for Status {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("status");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+
+ let txt = format!(
+ "HTTP/1.1 {} {}",
+ self.0.as_str(),
+ self.0.canonical_reason().unwrap_or("No reason")
+ );
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&txt)))
+ .await?;
+
+ xml.q.write_event_async(Event::End(end)).await?;
+
+ Ok(())
+ }
+}
+
+impl QWrite for ResponseDescription {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("responsedescription");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&self.0)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for Location {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("location");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for PropStat<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("propstat");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.prop.qwrite(xml).await?;
+ self.status.qwrite(xml).await?;
+ if let Some(error) = &self.error {
+ error.qwrite(xml).await?;
+ }
+ if let Some(description) = &self.responsedescription {
+ description.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await?;
+
+ Ok(())
+ }
+}
+
+impl<E: Extension> QWrite for Property<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ use Property::*;
+ match self {
+ CreationDate(date) => {
+ // <D:creationdate>1997-12-01T17:42:21-08:00</D:creationdate>
+ let start = xml.create_dav_element("creationdate");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&date.to_rfc3339())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ DisplayName(name) => {
+ // <D:displayname>Example collection</D:displayname>
+ let start = xml.create_dav_element("displayname");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(name)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ GetContentLanguage(lang) => {
+ let start = xml.create_dav_element("getcontentlanguage");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(lang)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ GetContentLength(len) => {
+ // <D:getcontentlength>4525</D:getcontentlength>
+ let start = xml.create_dav_element("getcontentlength");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&len.to_string())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ GetContentType(ct) => {
+ // <D:getcontenttype>text/html</D:getcontenttype>
+ let start = xml.create_dav_element("getcontenttype");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&ct)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ GetEtag(et) => {
+ // <D:getetag>"zzyzx"</D:getetag>
+ let start = xml.create_dav_element("getetag");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(et)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ GetLastModified(date) => {
+ // <D:getlastmodified>Mon, 12 Jan 1998 09:25:56 GMT</D:getlastmodified>
+ let start = xml.create_dav_element("getlastmodified");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&date.to_rfc2822())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ LockDiscovery(many_locks) => {
+ // <D:lockdiscovery><D:activelock> ... </D:activelock></D:lockdiscovery>
+ let start = xml.create_dav_element("lockdiscovery");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for lock in many_locks.iter() {
+ lock.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ ResourceType(many_types) => {
+ // <D:resourcetype><D:collection/></D:resourcetype>
+
+ // <D:resourcetype/>
+
+ // <x:resourcetype xmlns:x="DAV:">
+ // <x:collection/>
+ // <f:search-results xmlns:f="http://www.example.com/ns"/>
+ // </x:resourcetype>
+
+ let start = xml.create_dav_element("resourcetype");
+ if many_types.is_empty() {
+ xml.q.write_event_async(Event::Empty(start)).await?;
+ } else {
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for restype in many_types.iter() {
+ restype.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ }
+ SupportedLock(many_entries) => {
+ // <D:supportedlock/>
+
+ // <D:supportedlock> <D:lockentry> ... </D:lockentry> </D:supportedlock>
+
+ let start = xml.create_dav_element("supportedlock");
+ if many_entries.is_empty() {
+ xml.q.write_event_async(Event::Empty(start)).await?;
+ } else {
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for entry in many_entries.iter() {
+ entry.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await?;
+ }
+ }
+ Extension(inner) => inner.qwrite(xml).await?,
+ };
+ Ok(())
+ }
+}
+
+impl<E: Extension> QWrite for ResourceType<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::Collection => {
+ let empty_collection = xml.create_dav_element("collection");
+ xml.q
+ .write_event_async(Event::Empty(empty_collection))
+ .await
+ }
+ Self::Extension(inner) => inner.qwrite(xml).await,
+ }
+ }
+}
+
+impl<E: Extension> QWrite for Include<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("include");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for prop in self.0.iter() {
+ prop.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for PropertyRequest<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ use PropertyRequest::*;
+ let mut atom = async |c| {
+ let empty_tag = xml.create_dav_element(c);
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ };
+
+ match self {
+ CreationDate => atom("creationdate").await,
+ DisplayName => atom("displayname").await,
+ GetContentLanguage => atom("getcontentlanguage").await,
+ GetContentLength => atom("getcontentlength").await,
+ GetContentType => atom("getcontenttype").await,
+ GetEtag => atom("getetag").await,
+ GetLastModified => atom("getlastmodified").await,
+ LockDiscovery => atom("lockdiscovery").await,
+ ResourceType => atom("resourcetype").await,
+ SupportedLock => atom("supportedlock").await,
+ Extension(inner) => inner.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for ActiveLock {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ // <D:activelock>
+ // <D:locktype><D:write/></D:locktype>
+ // <D:lockscope><D:exclusive/></D:lockscope>
+ // <D:depth>infinity</D:depth>
+ // <D:owner>
+ // <D:href>http://example.org/~ejw/contact.html</D:href>
+ // </D:owner>
+ // <D:timeout>Second-604800</D:timeout>
+ // <D:locktoken>
+ // <D:href>urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4</D:href>
+ // </D:locktoken>
+ // <D:lockroot>
+ // <D:href>http://example.com/workspace/webdav/proposal.doc</D:href>
+ // </D:lockroot>
+ // </D:activelock>
+ let start = xml.create_dav_element("activelock");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.locktype.qwrite(xml).await?;
+ self.lockscope.qwrite(xml).await?;
+ self.depth.qwrite(xml).await?;
+ if let Some(owner) = &self.owner {
+ owner.qwrite(xml).await?;
+ }
+ if let Some(timeout) = &self.timeout {
+ timeout.qwrite(xml).await?;
+ }
+ if let Some(locktoken) = &self.locktoken {
+ locktoken.qwrite(xml).await?;
+ }
+ self.lockroot.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for LockType {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("locktype");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::Write => {
+ let empty_write = xml.create_dav_element("write");
+ xml.q.write_event_async(Event::Empty(empty_write)).await?
+ }
+ };
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for LockScope {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("lockscope");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::Exclusive => {
+ let empty_tag = xml.create_dav_element("exclusive");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await?
+ }
+ Self::Shared => {
+ let empty_tag = xml.create_dav_element("shared");
+ xml.q.write_event_async(Event::Empty(empty_tag)).await?
+ }
+ };
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for Owner {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("owner");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::Txt(txt) => {
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&txt)))
+ .await?
+ }
+ Self::Href(href) => href.qwrite(xml).await?,
+ Self::Unknown => (),
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for Depth {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("depth");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::Zero => {
+ xml.q
+ .write_event_async(Event::Text(BytesText::new("0")))
+ .await?
+ }
+ Self::One => {
+ xml.q
+ .write_event_async(Event::Text(BytesText::new("1")))
+ .await?
+ }
+ Self::Infinity => {
+ xml.q
+ .write_event_async(Event::Text(BytesText::new("infinity")))
+ .await?
+ }
+ };
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for Timeout {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("timeout");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::Seconds(count) => {
+ let txt = format!("Second-{}", count);
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&txt)))
+ .await?
+ }
+ Self::Infinite => {
+ xml.q
+ .write_event_async(Event::Text(BytesText::new("Infinite")))
+ .await?
+ }
+ };
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for LockToken {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("locktoken");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for LockRoot {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("lockroot");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for LockEntry {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("lockentry");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.lockscope.qwrite(xml).await?;
+ self.locktype.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for Violation<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let mut atom = async |c| {
+ let empty_tag = xml.create_dav_element(c);
+ xml.q.write_event_async(Event::Empty(empty_tag)).await
+ };
+
+ match self {
+ Violation::LockTokenMatchesRequestUri => atom("lock-token-matches-request-uri").await,
+ Violation::LockTokenSubmitted(hrefs) if hrefs.is_empty() => {
+ atom("lock-token-submitted").await
+ }
+ Violation::LockTokenSubmitted(hrefs) => {
+ let start = xml.create_dav_element("lock-token-submitted");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for href in hrefs {
+ href.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Violation::NoConflictingLock(hrefs) if hrefs.is_empty() => {
+ atom("no-conflicting-lock").await
+ }
+ Violation::NoConflictingLock(hrefs) => {
+ let start = xml.create_dav_element("no-conflicting-lock");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for href in hrefs {
+ href.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ Violation::NoExternalEntities => atom("no-external-entities").await,
+ Violation::PreservedLiveProperties => atom("preserved-live-properties").await,
+ Violation::PropfindFiniteDepth => atom("propfind-finite-depth").await,
+ Violation::CannotModifyProtectedProperty => {
+ atom("cannot-modify-protected-property").await
+ }
+ Violation::Extension(inner) => inner.qwrite(xml).await,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::super::xml;
+ use super::*;
+ use crate::realization::Core;
+ use tokio::io::AsyncWriteExt;
+
+ /// To run only the unit tests and avoid the behavior ones:
+ /// cargo test --bin aerogramme
+
+ async fn serialize(elem: &impl QWrite) -> String {
+ let mut buffer = Vec::new();
+ let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
+ let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
+ let ns_to_apply = vec![("xmlns:D".into(), "DAV:".into())];
+ let mut writer = Writer { q, ns_to_apply };
+
+ elem.qwrite(&mut writer).await.expect("xml serialization");
+ tokio_buffer.flush().await.expect("tokio buffer flush");
+ let got = std::str::from_utf8(buffer.as_slice()).unwrap();
+
+ return got.into();
+ }
+
+ async fn deserialize<T: xml::Node<T>>(src: &str) -> T {
+ let mut rdr = xml::Reader::new(quick_xml::reader::NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn basic_href() {
+ let orig = Href("/SOGo/dav/so/".into());
+
+ let got = serialize(&orig).await;
+ let expected = r#"<D:href xmlns:D="DAV:">/SOGo/dav/so/</D:href>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<Href>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn basic_multistatus() {
+ let orig = Multistatus::<Core> {
+ extension: None,
+ responses: vec![],
+ responsedescription: Some(ResponseDescription("Hello world".into())),
+ };
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:multistatus xmlns:D="DAV:">
+ <D:responsedescription>Hello world</D:responsedescription>
+</D:multistatus>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<Multistatus::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_error_delete_locked() {
+ let orig = Error::<Core>(vec![Violation::LockTokenSubmitted(vec![Href(
+ "/locked/".into(),
+ )])]);
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:error xmlns:D="DAV:">
+ <D:lock-token-submitted>
+ <D:href>/locked/</D:href>
+ </D:lock-token-submitted>
+</D:error>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<Error<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_propname_req() {
+ let orig = PropFind::<Core>::PropName;
+
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:propfind xmlns:D="DAV:">
+ <D:propname/>
+</D:propfind>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<PropFind::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_propname_res() {
+ let orig = Multistatus::<Core> {
+ extension: None,
+ responses: vec![
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("http://www.example.com/container/".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Request(PropertyRequest::CreationDate),
+ AnyProperty::Request(PropertyRequest::DisplayName),
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Request(PropertyRequest::SupportedLock),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("http://www.example.com/container/front.html".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Request(PropertyRequest::CreationDate),
+ AnyProperty::Request(PropertyRequest::DisplayName),
+ AnyProperty::Request(PropertyRequest::GetContentLength),
+ AnyProperty::Request(PropertyRequest::GetContentType),
+ AnyProperty::Request(PropertyRequest::GetEtag),
+ AnyProperty::Request(PropertyRequest::GetLastModified),
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Request(PropertyRequest::SupportedLock),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ ],
+ responsedescription: None,
+ };
+
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:multistatus xmlns:D="DAV:">
+ <D:response>
+ <D:href>http://www.example.com/container/</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:creationdate/>
+ <D:displayname/>
+ <D:resourcetype/>
+ <D:supportedlock/>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>http://www.example.com/container/front.html</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:creationdate/>
+ <D:displayname/>
+ <D:getcontentlength/>
+ <D:getcontenttype/>
+ <D:getetag/>
+ <D:getlastmodified/>
+ <D:resourcetype/>
+ <D:supportedlock/>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+</D:multistatus>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<Multistatus::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_allprop_req() {
+ let orig = PropFind::<Core>::AllProp(None);
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:propfind xmlns:D="DAV:">
+ <D:allprop/>
+</D:propfind>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<PropFind::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_allprop_res() {
+ use chrono::{FixedOffset, TimeZone};
+
+ let orig = Multistatus::<Core> {
+ extension: None,
+ responses: vec![
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("/container/".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Value(Property::CreationDate(
+ FixedOffset::west_opt(8 * 3600)
+ .unwrap()
+ .with_ymd_and_hms(1997, 12, 1, 17, 42, 21)
+ .unwrap(),
+ )),
+ AnyProperty::Value(Property::DisplayName(
+ "Example collection".into(),
+ )),
+ AnyProperty::Value(Property::ResourceType(vec![
+ ResourceType::Collection,
+ ])),
+ AnyProperty::Value(Property::SupportedLock(vec![
+ LockEntry {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ },
+ LockEntry {
+ lockscope: LockScope::Shared,
+ locktype: LockType::Write,
+ },
+ ])),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ Response {
+ status_or_propstat: StatusOrPropstat::PropStat(
+ Href("/container/front.html".into()),
+ vec![PropStat {
+ prop: AnyProp(vec![
+ AnyProperty::Value(Property::CreationDate(
+ FixedOffset::west_opt(8 * 3600)
+ .unwrap()
+ .with_ymd_and_hms(1997, 12, 1, 18, 27, 21)
+ .unwrap(),
+ )),
+ AnyProperty::Value(Property::DisplayName(
+ "Example HTML resource".into(),
+ )),
+ AnyProperty::Value(Property::GetContentLength(4525)),
+ AnyProperty::Value(Property::GetContentType("text/html".into())),
+ AnyProperty::Value(Property::GetEtag(r#""zzyzx""#.into())),
+ AnyProperty::Value(Property::GetLastModified(
+ FixedOffset::east_opt(0)
+ .unwrap()
+ .with_ymd_and_hms(1998, 1, 12, 9, 25, 56)
+ .unwrap(),
+ )),
+ //@FIXME know bug, can't disambiguate between an empty resource
+ //type value and a request resource type
+ AnyProperty::Request(PropertyRequest::ResourceType),
+ AnyProperty::Value(Property::SupportedLock(vec![
+ LockEntry {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ },
+ LockEntry {
+ lockscope: LockScope::Shared,
+ locktype: LockType::Write,
+ },
+ ])),
+ ]),
+ status: Status(http::status::StatusCode::OK),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ responsedescription: None,
+ location: None,
+ },
+ ],
+ responsedescription: None,
+ };
+
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:multistatus xmlns:D="DAV:">
+ <D:response>
+ <D:href>/container/</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:creationdate>1997-12-01T17:42:21-08:00</D:creationdate>
+ <D:displayname>Example collection</D:displayname>
+ <D:resourcetype>
+ <D:collection/>
+ </D:resourcetype>
+ <D:supportedlock>
+ <D:lockentry>
+ <D:lockscope>
+ <D:exclusive/>
+ </D:lockscope>
+ <D:locktype>
+ <D:write/>
+ </D:locktype>
+ </D:lockentry>
+ <D:lockentry>
+ <D:lockscope>
+ <D:shared/>
+ </D:lockscope>
+ <D:locktype>
+ <D:write/>
+ </D:locktype>
+ </D:lockentry>
+ </D:supportedlock>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>/container/front.html</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:creationdate>1997-12-01T18:27:21-08:00</D:creationdate>
+ <D:displayname>Example HTML resource</D:displayname>
+ <D:getcontentlength>4525</D:getcontentlength>
+ <D:getcontenttype>text/html</D:getcontenttype>
+ <D:getetag>&quot;zzyzx&quot;</D:getetag>
+ <D:getlastmodified>Mon, 12 Jan 1998 09:25:56 +0000</D:getlastmodified>
+ <D:resourcetype/>
+ <D:supportedlock>
+ <D:lockentry>
+ <D:lockscope>
+ <D:exclusive/>
+ </D:lockscope>
+ <D:locktype>
+ <D:write/>
+ </D:locktype>
+ </D:lockentry>
+ <D:lockentry>
+ <D:lockscope>
+ <D:shared/>
+ </D:lockscope>
+ <D:locktype>
+ <D:write/>
+ </D:locktype>
+ </D:lockentry>
+ </D:supportedlock>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+</D:multistatus>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<Multistatus::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_allprop_include() {
+ let orig = PropFind::<Core>::AllProp(Some(Include(vec![
+ PropertyRequest::DisplayName,
+ PropertyRequest::ResourceType,
+ ])));
+
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:propfind xmlns:D="DAV:">
+ <D:allprop/>
+ <D:include>
+ <D:displayname/>
+ <D:resourcetype/>
+ </D:include>
+</D:propfind>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<PropFind::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_propertyupdate() {
+ let orig = PropertyUpdate::<Core>(vec![
+ PropertyUpdateItem::Set(Set(PropValue(vec![Property::GetContentLanguage(
+ "fr-FR".into(),
+ )]))),
+ PropertyUpdateItem::Remove(Remove(PropName(vec![PropertyRequest::DisplayName]))),
+ ]);
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:propertyupdate xmlns:D="DAV:">
+ <D:set>
+ <D:prop>
+ <D:getcontentlanguage>fr-FR</D:getcontentlanguage>
+ </D:prop>
+ </D:set>
+ <D:remove>
+ <D:prop>
+ <D:displayname/>
+ </D:prop>
+ </D:remove>
+</D:propertyupdate>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(
+ deserialize::<PropertyUpdate::<Core>>(got.as_str()).await,
+ orig
+ )
+ }
+
+ #[tokio::test]
+ async fn rfc_delete_locked2() {
+ let orig = Multistatus::<Core> {
+ extension: None,
+ responses: vec![Response {
+ status_or_propstat: StatusOrPropstat::Status(
+ vec![Href("http://www.example.com/container/resource3".into())],
+ Status(http::status::StatusCode::from_u16(423).unwrap()),
+ ),
+ error: Some(Error(vec![Violation::LockTokenSubmitted(vec![])])),
+ responsedescription: None,
+ location: None,
+ }],
+ responsedescription: None,
+ };
+
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:multistatus xmlns:D="DAV:">
+ <D:response>
+ <D:href>http://www.example.com/container/resource3</D:href>
+ <D:status>HTTP/1.1 423 Locked</D:status>
+ <D:error>
+ <D:lock-token-submitted/>
+ </D:error>
+ </D:response>
+</D:multistatus>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<Multistatus::<Core>>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_simple_lock_request() {
+ let orig = LockInfo {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ owner: Some(Owner::Href(Href(
+ "http://example.org/~ejw/contact.html".into(),
+ ))),
+ };
+
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:lockinfo xmlns:D="DAV:">
+ <D:lockscope>
+ <D:exclusive/>
+ </D:lockscope>
+ <D:locktype>
+ <D:write/>
+ </D:locktype>
+ <D:owner>
+ <D:href>http://example.org/~ejw/contact.html</D:href>
+ </D:owner>
+</D:lockinfo>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<LockInfo>(got.as_str()).await, orig)
+ }
+
+ #[tokio::test]
+ async fn rfc_simple_lock_response() {
+ let orig = PropValue::<Core>(vec![Property::LockDiscovery(vec![ActiveLock {
+ lockscope: LockScope::Exclusive,
+ locktype: LockType::Write,
+ depth: Depth::Infinity,
+ owner: Some(Owner::Href(Href(
+ "http://example.org/~ejw/contact.html".into(),
+ ))),
+ timeout: Some(Timeout::Seconds(604800)),
+ locktoken: Some(LockToken(Href(
+ "urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4".into(),
+ ))),
+ lockroot: LockRoot(Href(
+ "http://example.com/workspace/webdav/proposal.doc".into(),
+ )),
+ }])]);
+
+ let got = serialize(&orig).await;
+
+ let expected = r#"<D:prop xmlns:D="DAV:">
+ <D:lockdiscovery>
+ <D:activelock>
+ <D:locktype>
+ <D:write/>
+ </D:locktype>
+ <D:lockscope>
+ <D:exclusive/>
+ </D:lockscope>
+ <D:depth>infinity</D:depth>
+ <D:owner>
+ <D:href>http://example.org/~ejw/contact.html</D:href>
+ </D:owner>
+ <D:timeout>Second-604800</D:timeout>
+ <D:locktoken>
+ <D:href>urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4</D:href>
+ </D:locktoken>
+ <D:lockroot>
+ <D:href>http://example.com/workspace/webdav/proposal.doc</D:href>
+ </D:lockroot>
+ </D:activelock>
+ </D:lockdiscovery>
+</D:prop>"#;
+
+ assert_eq!(
+ &got, expected,
+ "\n---GOT---\n{got}\n---EXP---\n{expected}\n"
+ );
+ assert_eq!(deserialize::<PropValue::<Core>>(got.as_str()).await, orig)
+ }
+}
diff --git a/aero-dav/src/error.rs b/aero-dav/src/error.rs
new file mode 100644
index 0000000..c8f1de1
--- /dev/null
+++ b/aero-dav/src/error.rs
@@ -0,0 +1,62 @@
+use quick_xml::events::attributes::AttrError;
+
+#[derive(Debug)]
+pub enum ParsingError {
+ Recoverable,
+ MissingChild,
+ MissingAttribute,
+ NamespacePrefixAlreadyUsed,
+ WrongToken,
+ TagNotFound,
+ InvalidValue,
+ Utf8Error(std::str::Utf8Error),
+ QuickXml(quick_xml::Error),
+ Chrono(chrono::format::ParseError),
+ Int(std::num::ParseIntError),
+ Eof,
+}
+impl std::fmt::Display for ParsingError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Recoverable => write!(f, "Recoverable"),
+ Self::MissingChild => write!(f, "Missing child"),
+ Self::MissingAttribute => write!(f, "Missing attribute"),
+ Self::NamespacePrefixAlreadyUsed => write!(f, "Namespace prefix already used"),
+ Self::WrongToken => write!(f, "Wrong token"),
+ Self::TagNotFound => write!(f, "Tag not found"),
+ Self::InvalidValue => write!(f, "Invalid value"),
+ Self::Utf8Error(_) => write!(f, "Utf8 Error"),
+ Self::QuickXml(_) => write!(f, "Quick XML error"),
+ Self::Chrono(_) => write!(f, "Chrono error"),
+ Self::Int(_) => write!(f, "Number parsing error"),
+ Self::Eof => write!(f, "Found EOF while expecting data"),
+ }
+ }
+}
+impl std::error::Error for ParsingError {}
+impl From<AttrError> for ParsingError {
+ fn from(value: AttrError) -> Self {
+ Self::QuickXml(value.into())
+ }
+}
+impl From<quick_xml::Error> for ParsingError {
+ fn from(value: quick_xml::Error) -> Self {
+ Self::QuickXml(value)
+ }
+}
+impl From<std::str::Utf8Error> for ParsingError {
+ fn from(value: std::str::Utf8Error) -> Self {
+ Self::Utf8Error(value)
+ }
+}
+impl From<chrono::format::ParseError> for ParsingError {
+ fn from(value: chrono::format::ParseError) -> Self {
+ Self::Chrono(value)
+ }
+}
+
+impl From<std::num::ParseIntError> for ParsingError {
+ fn from(value: std::num::ParseIntError) -> Self {
+ Self::Int(value)
+ }
+}
diff --git a/aero-dav/src/lib.rs b/aero-dav/src/lib.rs
new file mode 100644
index 0000000..64be929
--- /dev/null
+++ b/aero-dav/src/lib.rs
@@ -0,0 +1,35 @@
+#![feature(type_alias_impl_trait)]
+#![feature(async_closure)]
+#![feature(trait_alias)]
+
+// utils
+pub mod error;
+pub mod xml;
+
+// webdav
+pub mod decoder;
+pub mod encoder;
+pub mod types;
+
+// calendar
+pub mod caldecoder;
+pub mod calencoder;
+pub mod caltypes;
+
+// acl (partial)
+pub mod acldecoder;
+pub mod aclencoder;
+pub mod acltypes;
+
+// versioning (partial)
+pub mod versioningdecoder;
+pub mod versioningencoder;
+pub mod versioningtypes;
+
+// sync
+pub mod syncdecoder;
+pub mod syncencoder;
+pub mod synctypes;
+
+// final type
+pub mod realization;
diff --git a/aero-dav/src/realization.rs b/aero-dav/src/realization.rs
new file mode 100644
index 0000000..76170f8
--- /dev/null
+++ b/aero-dav/src/realization.rs
@@ -0,0 +1,260 @@
+use super::acltypes as acl;
+use super::caltypes as cal;
+use super::error;
+use super::synctypes as sync;
+use super::types as dav;
+use super::versioningtypes as vers;
+use super::xml;
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct Disabled(());
+impl xml::QRead<Disabled> for Disabled {
+ async fn qread(_xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ Err(error::ParsingError::Recoverable)
+ }
+}
+impl xml::QWrite for Disabled {
+ async fn qwrite(
+ &self,
+ _xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ unreachable!()
+ }
+}
+
+/// The base WebDAV
+///
+/// Any extension is disabled through an object we can't build
+/// due to a private inner element.
+#[derive(Debug, PartialEq, Clone)]
+pub struct Core {}
+impl dav::Extension for Core {
+ type Error = Disabled;
+ type Property = Disabled;
+ type PropertyRequest = Disabled;
+ type ResourceType = Disabled;
+ type ReportType = Disabled;
+ type ReportTypeName = Disabled;
+ type Multistatus = Disabled;
+}
+
+// WebDAV with the base Calendar implementation (RFC4791)
+#[derive(Debug, PartialEq, Clone)]
+pub struct Calendar {}
+impl dav::Extension for Calendar {
+ type Error = cal::Violation;
+ type Property = cal::Property;
+ type PropertyRequest = cal::PropertyRequest;
+ type ResourceType = cal::ResourceType;
+ type ReportType = cal::ReportType<Calendar>;
+ type ReportTypeName = cal::ReportTypeName;
+ type Multistatus = Disabled;
+}
+
+// ACL
+#[derive(Debug, PartialEq, Clone)]
+pub struct Acl {}
+impl dav::Extension for Acl {
+ type Error = Disabled;
+ type Property = acl::Property;
+ type PropertyRequest = acl::PropertyRequest;
+ type ResourceType = acl::ResourceType;
+ type ReportType = Disabled;
+ type ReportTypeName = Disabled;
+ type Multistatus = Disabled;
+}
+
+// All merged
+#[derive(Debug, PartialEq, Clone)]
+pub struct All {}
+impl dav::Extension for All {
+ type Error = cal::Violation;
+ type Property = Property<All>;
+ type PropertyRequest = PropertyRequest;
+ type ResourceType = ResourceType;
+ type ReportType = ReportType<All>;
+ type ReportTypeName = ReportTypeName;
+ type Multistatus = Multistatus;
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property<E: dav::Extension> {
+ Cal(cal::Property),
+ Acl(acl::Property),
+ Sync(sync::Property),
+ Vers(vers::Property<E>),
+}
+impl<E: dav::Extension> xml::QRead<Property<E>> for Property<E> {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ match cal::Property::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Property::<E>::Cal),
+ }
+ match acl::Property::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Property::Acl),
+ }
+ match sync::Property::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Property::Sync),
+ }
+ vers::Property::qread(xml).await.map(Property::Vers)
+ }
+}
+impl<E: dav::Extension> xml::QWrite for Property<E> {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Acl(a) => a.qwrite(xml).await,
+ Self::Sync(s) => s.qwrite(xml).await,
+ Self::Vers(v) => v.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ Cal(cal::PropertyRequest),
+ Acl(acl::PropertyRequest),
+ Sync(sync::PropertyRequest),
+ Vers(vers::PropertyRequest),
+}
+impl xml::QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ match cal::PropertyRequest::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(PropertyRequest::Cal),
+ }
+ match acl::PropertyRequest::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(PropertyRequest::Acl),
+ }
+ match sync::PropertyRequest::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(PropertyRequest::Sync),
+ }
+ vers::PropertyRequest::qread(xml)
+ .await
+ .map(PropertyRequest::Vers)
+ }
+}
+impl xml::QWrite for PropertyRequest {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Acl(a) => a.qwrite(xml).await,
+ Self::Sync(s) => s.qwrite(xml).await,
+ Self::Vers(v) => v.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ResourceType {
+ Cal(cal::ResourceType),
+ Acl(acl::ResourceType),
+}
+impl xml::QRead<ResourceType> for ResourceType {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ match cal::ResourceType::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(ResourceType::Cal),
+ }
+ acl::ResourceType::qread(xml).await.map(ResourceType::Acl)
+ }
+}
+impl xml::QWrite for ResourceType {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Acl(a) => a.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportType<E: dav::Extension> {
+ Cal(cal::ReportType<E>),
+ Sync(sync::SyncCollection<E>),
+}
+impl<E: dav::Extension> xml::QRead<ReportType<E>> for ReportType<E> {
+ async fn qread(
+ xml: &mut xml::Reader<impl xml::IRead>,
+ ) -> Result<ReportType<E>, error::ParsingError> {
+ match cal::ReportType::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(ReportType::Cal),
+ }
+ sync::SyncCollection::qread(xml).await.map(ReportType::Sync)
+ }
+}
+impl<E: dav::Extension> xml::QWrite for ReportType<E> {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Sync(s) => s.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportTypeName {
+ Cal(cal::ReportTypeName),
+ Sync(sync::ReportTypeName),
+}
+impl xml::QRead<ReportTypeName> for ReportTypeName {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ match cal::ReportTypeName::qread(xml).await {
+ Err(error::ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(ReportTypeName::Cal),
+ }
+ sync::ReportTypeName::qread(xml)
+ .await
+ .map(ReportTypeName::Sync)
+ }
+}
+impl xml::QWrite for ReportTypeName {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Cal(c) => c.qwrite(xml).await,
+ Self::Sync(s) => s.qwrite(xml).await,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Multistatus {
+ Sync(sync::Multistatus),
+}
+
+impl xml::QWrite for Multistatus {
+ async fn qwrite(
+ &self,
+ xml: &mut xml::Writer<impl xml::IWrite>,
+ ) -> Result<(), quick_xml::Error> {
+ match self {
+ Self::Sync(s) => s.qwrite(xml).await,
+ }
+ }
+}
+
+impl xml::QRead<Multistatus> for Multistatus {
+ async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
+ sync::Multistatus::qread(xml).await.map(Self::Sync)
+ }
+}
diff --git a/aero-dav/src/syncdecoder.rs b/aero-dav/src/syncdecoder.rs
new file mode 100644
index 0000000..2a61dea
--- /dev/null
+++ b/aero-dav/src/syncdecoder.rs
@@ -0,0 +1,248 @@
+use quick_xml::events::Event;
+
+use super::error::ParsingError;
+use super::synctypes::*;
+use super::types as dav;
+use super::xml::{IRead, QRead, Reader, DAV_URN};
+
+impl QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "sync-token").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::SyncToken);
+ }
+ return Err(ParsingError::Recoverable);
+ }
+}
+
+impl QRead<Property> for Property {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ let mut dirty = false;
+ let mut m_cdr = None;
+ xml.maybe_read(&mut m_cdr, &mut dirty).await?;
+ m_cdr.ok_or(ParsingError::Recoverable).map(Self::SyncToken)
+ }
+}
+
+impl QRead<ReportTypeName> for ReportTypeName {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "sync-collection").await?.is_some() {
+ xml.close().await?;
+ return Ok(Self::SyncCollection);
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl QRead<Multistatus> for Multistatus {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ SyncToken::qread(xml)
+ .await
+ .map(|sync_token| Multistatus { sync_token })
+ }
+}
+
+impl<E: dav::Extension> QRead<SyncCollection<E>> for SyncCollection<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "sync-collection").await?;
+ let (mut sync_token, mut sync_level, mut limit, mut prop) = (None, None, None, None);
+ loop {
+ let mut dirty = false;
+ xml.maybe_read(&mut sync_token, &mut dirty).await?;
+ xml.maybe_read(&mut sync_level, &mut dirty).await?;
+ xml.maybe_read(&mut limit, &mut dirty).await?;
+ xml.maybe_read(&mut prop, &mut dirty).await?;
+
+ if !dirty {
+ match xml.peek() {
+ Event::End(_) => break,
+ _ => xml.skip().await?,
+ };
+ }
+ }
+
+ xml.close().await?;
+ match (sync_token, sync_level, prop) {
+ (Some(sync_token), Some(sync_level), Some(prop)) => Ok(SyncCollection {
+ sync_token,
+ sync_level,
+ limit,
+ prop,
+ }),
+ _ => Err(ParsingError::MissingChild),
+ }
+ }
+}
+
+impl QRead<SyncTokenRequest> for SyncTokenRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "sync-token").await?;
+ let token = match xml.tag_string().await {
+ Ok(v) => SyncTokenRequest::IncrementalSync(v),
+ Err(ParsingError::Recoverable) => SyncTokenRequest::InitialSync,
+ Err(e) => return Err(e),
+ };
+ xml.close().await?;
+ Ok(token)
+ }
+}
+
+impl QRead<SyncToken> for SyncToken {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "sync-token").await?;
+ let token = xml.tag_string().await?;
+ xml.close().await?;
+ Ok(SyncToken(token))
+ }
+}
+
+impl QRead<SyncLevel> for SyncLevel {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "sync-level").await?;
+ let lvl = match xml.tag_string().await?.to_lowercase().as_str() {
+ "1" => SyncLevel::One,
+ "infinite" => SyncLevel::Infinite,
+ _ => return Err(ParsingError::InvalidValue),
+ };
+ xml.close().await?;
+ Ok(lvl)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::{self, All};
+ use crate::types as dav;
+ use crate::versioningtypes as vers;
+ use crate::xml::Node;
+
+ async fn deserialize<T: Node<T>>(src: &str) -> T {
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn sync_level() {
+ {
+ let expected = SyncLevel::One;
+ let src = r#"<D:sync-level xmlns:D="DAV:">1</D:sync-level>"#;
+ let got = deserialize::<SyncLevel>(src).await;
+ assert_eq!(got, expected);
+ }
+ {
+ let expected = SyncLevel::Infinite;
+ let src = r#"<D:sync-level xmlns:D="DAV:">infinite</D:sync-level>"#;
+ let got = deserialize::<SyncLevel>(src).await;
+ assert_eq!(got, expected);
+ }
+ }
+
+ #[tokio::test]
+ async fn sync_token_request() {
+ {
+ let expected = SyncTokenRequest::InitialSync;
+ let src = r#"<D:sync-token xmlns:D="DAV:"/>"#;
+ let got = deserialize::<SyncTokenRequest>(src).await;
+ assert_eq!(got, expected);
+ }
+ {
+ let expected =
+ SyncTokenRequest::IncrementalSync("http://example.com/ns/sync/1232".into());
+ let src =
+ r#"<D:sync-token xmlns:D="DAV:">http://example.com/ns/sync/1232</D:sync-token>"#;
+ let got = deserialize::<SyncTokenRequest>(src).await;
+ assert_eq!(got, expected);
+ }
+ }
+
+ #[tokio::test]
+ async fn sync_token() {
+ let expected = SyncToken("http://example.com/ns/sync/1232".into());
+ let src = r#"<D:sync-token xmlns:D="DAV:">http://example.com/ns/sync/1232</D:sync-token>"#;
+ let got = deserialize::<SyncToken>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ #[tokio::test]
+ async fn sync_collection() {
+ {
+ let expected = SyncCollection::<All> {
+ sync_token: SyncTokenRequest::IncrementalSync(
+ "http://example.com/ns/sync/1232".into(),
+ ),
+ sync_level: SyncLevel::One,
+ limit: Some(vers::Limit(vers::NResults(100))),
+ prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
+ };
+ let src = r#"<D:sync-collection xmlns:D="DAV:">
+ <D:sync-token>http://example.com/ns/sync/1232</D:sync-token>
+ <D:sync-level>1</D:sync-level>
+ <D:limit>
+ <D:nresults>100</D:nresults>
+ </D:limit>
+ <D:prop>
+ <D:getetag/>
+ </D:prop>
+ </D:sync-collection>"#;
+ let got = deserialize::<SyncCollection<All>>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ {
+ let expected = SyncCollection::<All> {
+ sync_token: SyncTokenRequest::InitialSync,
+ sync_level: SyncLevel::Infinite,
+ limit: None,
+ prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
+ };
+ let src = r#"<D:sync-collection xmlns:D="DAV:">
+ <D:sync-token/>
+ <D:sync-level>infinite</D:sync-level>
+ <D:prop>
+ <D:getetag/>
+ </D:prop>
+ </D:sync-collection>"#;
+ let got = deserialize::<SyncCollection<All>>(src).await;
+ assert_eq!(got, expected);
+ }
+ }
+
+ #[tokio::test]
+ async fn prop_req() {
+ let expected = dav::PropName::<All>(vec![dav::PropertyRequest::Extension(
+ realization::PropertyRequest::Sync(PropertyRequest::SyncToken),
+ )]);
+ let src = r#"<prop xmlns="DAV:"><sync-token/></prop>"#;
+ let got = deserialize::<dav::PropName<All>>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ #[tokio::test]
+ async fn prop_val() {
+ let expected = dav::PropValue::<All>(vec![
+ dav::Property::Extension(realization::Property::Sync(Property::SyncToken(SyncToken(
+ "http://example.com/ns/sync/1232".into(),
+ )))),
+ dav::Property::Extension(realization::Property::Vers(
+ vers::Property::SupportedReportSet(vec![vers::SupportedReport(
+ vers::ReportName::Extension(realization::ReportTypeName::Sync(
+ ReportTypeName::SyncCollection,
+ )),
+ )]),
+ )),
+ ]);
+ let src = r#"<prop xmlns="DAV:">
+ <sync-token>http://example.com/ns/sync/1232</sync-token>
+ <supported-report-set>
+ <supported-report>
+ <report><sync-collection/></report>
+ </supported-report>
+ </supported-report-set>
+ </prop>"#;
+ let got = deserialize::<dav::PropValue<All>>(src).await;
+ assert_eq!(got, expected);
+ }
+}
diff --git a/aero-dav/src/syncencoder.rs b/aero-dav/src/syncencoder.rs
new file mode 100644
index 0000000..55f7ad6
--- /dev/null
+++ b/aero-dav/src/syncencoder.rs
@@ -0,0 +1,227 @@
+use quick_xml::events::{BytesText, Event};
+use quick_xml::Error as QError;
+
+use super::synctypes::*;
+use super::types::Extension;
+use super::xml::{IWrite, QWrite, Writer};
+
+impl QWrite for Property {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SyncToken(token) => token.qwrite(xml).await,
+ }
+ }
+}
+
+impl QWrite for PropertyRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SyncToken => {
+ let start = xml.create_dav_element("sync-token");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ }
+ }
+}
+
+impl QWrite for ReportTypeName {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SyncCollection => {
+ let start = xml.create_dav_element("sync-collection");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ }
+ }
+}
+
+impl QWrite for Multistatus {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ self.sync_token.qwrite(xml).await
+ }
+}
+
+impl<E: Extension> QWrite for SyncCollection<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("sync-collection");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.sync_token.qwrite(xml).await?;
+ self.sync_level.qwrite(xml).await?;
+ if let Some(limit) = &self.limit {
+ limit.qwrite(xml).await?;
+ }
+ self.prop.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for SyncTokenRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("sync-token");
+
+ match self {
+ Self::InitialSync => xml.q.write_event_async(Event::Empty(start)).await,
+ Self::IncrementalSync(uri) => {
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(uri.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ }
+ }
+}
+
+impl QWrite for SyncToken {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("sync-token");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(self.0.as_str())))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for SyncLevel {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("sync-level");
+ let end = start.to_end();
+ let text = match self {
+ Self::One => "1",
+ Self::Infinite => "infinite",
+ };
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(text)))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::realization::{self, All};
+ use crate::types as dav;
+ use crate::versioningtypes as vers;
+ use crate::xml::Node;
+ use crate::xml::Reader;
+ use tokio::io::AsyncWriteExt;
+
+ async fn serialize_deserialize<T: Node<T>>(src: &T) {
+ let mut buffer = Vec::new();
+ let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
+ let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
+ let ns_to_apply = vec![
+ ("xmlns:D".into(), "DAV:".into()),
+ ("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()),
+ ];
+ let mut writer = Writer { q, ns_to_apply };
+
+ src.qwrite(&mut writer).await.expect("xml serialization");
+ tokio_buffer.flush().await.expect("tokio buffer flush");
+ let got = std::str::from_utf8(buffer.as_slice()).unwrap();
+
+ // deserialize
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(got.as_bytes()))
+ .await
+ .unwrap();
+ let res = rdr.find().await.unwrap();
+
+ // check
+ assert_eq!(src, &res);
+ }
+
+ #[tokio::test]
+ async fn sync_level() {
+ serialize_deserialize(&SyncLevel::One).await;
+ serialize_deserialize(&SyncLevel::Infinite).await;
+ }
+
+ #[tokio::test]
+ async fn sync_token_request() {
+ serialize_deserialize(&SyncTokenRequest::InitialSync).await;
+ serialize_deserialize(&SyncTokenRequest::IncrementalSync(
+ "http://example.com/ns/sync/1232".into(),
+ ))
+ .await;
+ }
+
+ #[tokio::test]
+ async fn sync_token() {
+ serialize_deserialize(&SyncToken("http://example.com/ns/sync/1232".into())).await;
+ }
+
+ #[tokio::test]
+ async fn sync_collection() {
+ serialize_deserialize(&SyncCollection::<All> {
+ sync_token: SyncTokenRequest::IncrementalSync("http://example.com/ns/sync/1232".into()),
+ sync_level: SyncLevel::One,
+ limit: Some(vers::Limit(vers::NResults(100))),
+ prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
+ })
+ .await;
+
+ serialize_deserialize(&SyncCollection::<All> {
+ sync_token: SyncTokenRequest::InitialSync,
+ sync_level: SyncLevel::Infinite,
+ limit: None,
+ prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
+ })
+ .await;
+ }
+
+ #[tokio::test]
+ async fn prop_req() {
+ serialize_deserialize(&dav::PropName::<All>(vec![
+ dav::PropertyRequest::Extension(realization::PropertyRequest::Sync(
+ PropertyRequest::SyncToken,
+ )),
+ ]))
+ .await;
+ }
+
+ #[tokio::test]
+ async fn prop_val() {
+ serialize_deserialize(&dav::PropValue::<All>(vec![
+ dav::Property::Extension(realization::Property::Sync(Property::SyncToken(SyncToken(
+ "http://example.com/ns/sync/1232".into(),
+ )))),
+ dav::Property::Extension(realization::Property::Vers(
+ vers::Property::SupportedReportSet(vec![vers::SupportedReport(
+ vers::ReportName::Extension(realization::ReportTypeName::Sync(
+ ReportTypeName::SyncCollection,
+ )),
+ )]),
+ )),
+ ]))
+ .await;
+ }
+
+ #[tokio::test]
+ async fn multistatus_ext() {
+ serialize_deserialize(&dav::Multistatus::<All> {
+ responses: vec![dav::Response {
+ status_or_propstat: dav::StatusOrPropstat::Status(
+ vec![dav::Href("/".into())],
+ dav::Status(http::status::StatusCode::OK),
+ ),
+ error: None,
+ location: None,
+ responsedescription: None,
+ }],
+ responsedescription: None,
+ extension: Some(realization::Multistatus::Sync(Multistatus {
+ sync_token: SyncToken("http://example.com/ns/sync/1232".into()),
+ })),
+ })
+ .await;
+ }
+}
diff --git a/aero-dav/src/synctypes.rs b/aero-dav/src/synctypes.rs
new file mode 100644
index 0000000..2a14221
--- /dev/null
+++ b/aero-dav/src/synctypes.rs
@@ -0,0 +1,86 @@
+use super::types as dav;
+use super::versioningtypes as vers;
+
+// RFC 6578
+// https://datatracker.ietf.org/doc/html/rfc6578
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ SyncToken,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property {
+ SyncToken(SyncToken),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportTypeName {
+ SyncCollection,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct Multistatus {
+ pub sync_token: SyncToken,
+}
+
+//@FIXME add SyncToken to Multistatus
+
+/// Name: sync-collection
+///
+/// Namespace: DAV:
+///
+/// Purpose: WebDAV report used to synchronize data between client and
+/// server.
+///
+/// Description: See Section 3.
+///
+/// <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
+///
+/// <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
+/// <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct SyncCollection<E: dav::Extension> {
+ pub sync_token: SyncTokenRequest,
+ pub sync_level: SyncLevel,
+ pub limit: Option<vers::Limit>,
+ pub prop: dav::PropName<E>,
+}
+
+/// Name: sync-token
+///
+/// Namespace: DAV:
+///
+/// Purpose: The synchronization token provided by the server and
+/// returned by the client.
+///
+/// Description: See Section 3.
+///
+/// <!ELEMENT sync-token CDATA>
+///
+/// <!-- Text MUST be a URI -->
+/// Used by multistatus
+#[derive(Debug, PartialEq, Clone)]
+pub struct SyncToken(pub String);
+
+/// Used by propfind and report sync-collection
+#[derive(Debug, PartialEq, Clone)]
+pub enum SyncTokenRequest {
+ InitialSync,
+ IncrementalSync(String),
+}
+
+/// Name: sync-level
+///
+/// Namespace: DAV:
+///
+/// Purpose: Indicates the "scope" of the synchronization report
+/// request.
+///
+/// Description: See Section 3.3.
+#[derive(Debug, PartialEq, Clone)]
+pub enum SyncLevel {
+ One,
+ Infinite,
+}
diff --git a/aero-dav/src/types.rs b/aero-dav/src/types.rs
new file mode 100644
index 0000000..61a6fe9
--- /dev/null
+++ b/aero-dav/src/types.rs
@@ -0,0 +1,964 @@
+#![allow(dead_code)]
+use std::fmt::Debug;
+
+use super::xml;
+use chrono::{DateTime, FixedOffset};
+
+/// It's how we implement a DAV extension
+/// (That's the dark magic part...)
+pub trait Extension: std::fmt::Debug + PartialEq + Clone {
+ type Error: xml::Node<Self::Error>;
+ type Property: xml::Node<Self::Property>;
+ type PropertyRequest: xml::Node<Self::PropertyRequest>;
+ type ResourceType: xml::Node<Self::ResourceType>;
+ type ReportType: xml::Node<Self::ReportType>;
+ type ReportTypeName: xml::Node<Self::ReportTypeName>;
+ type Multistatus: xml::Node<Self::Multistatus>;
+}
+
+/// 14.1. activelock XML Element
+///
+/// Name: activelock
+///
+/// Purpose: Describes a lock on a resource.
+/// <!ELEMENT activelock (lockscope, locktype, depth, owner?, timeout?,
+/// locktoken?, lockroot)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct ActiveLock {
+ pub lockscope: LockScope,
+ pub locktype: LockType,
+ pub depth: Depth,
+ pub owner: Option<Owner>,
+ pub timeout: Option<Timeout>,
+ pub locktoken: Option<LockToken>,
+ pub lockroot: LockRoot,
+}
+
+/// 14.3 collection XML Element
+///
+/// Name: collection
+///
+/// Purpose: Identifies the associated resource as a collection. The
+/// DAV:resourcetype property of a collection resource MUST contain
+/// this element. It is normally empty but extensions may add sub-
+/// elements.
+///
+/// <!ELEMENT collection EMPTY >
+#[derive(Debug, PartialEq)]
+pub struct Collection {}
+
+/// 14.4 depth XML Element
+///
+/// Name: depth
+///
+/// Purpose: Used for representing depth values in XML content (e.g.,
+/// in lock information).
+///
+/// Value: "0" | "1" | "infinity"
+///
+/// <!ELEMENT depth (#PCDATA) >
+#[derive(Debug, PartialEq, Clone)]
+pub enum Depth {
+ Zero,
+ One,
+ Infinity,
+}
+
+/// 14.5 error XML Element
+///
+/// Name: error
+///
+/// Purpose: Error responses, particularly 403 Forbidden and 409
+/// Conflict, sometimes need more information to indicate what went
+/// wrong. In these cases, servers MAY return an XML response body
+/// with a document element of 'error', containing child elements
+/// identifying particular condition codes.
+///
+/// Description: Contains at least one XML element, and MUST NOT
+/// contain text or mixed content. Any element that is a child of the
+/// 'error' element is considered to be a precondition or
+/// postcondition code. Unrecognized elements MUST be ignored.
+///
+/// <!ELEMENT error ANY >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Error<E: Extension>(pub Vec<Violation<E>>);
+#[derive(Debug, PartialEq, Clone)]
+pub enum Violation<E: Extension> {
+ /// Name: lock-token-matches-request-uri
+ ///
+ /// Use with: 409 Conflict
+ ///
+ /// Purpose: (precondition) -- A request may include a Lock-Token header
+ /// to identify a lock for the UNLOCK method. However, if the
+ /// Request-URI does not fall within the scope of the lock identified
+ /// by the token, the server SHOULD use this error. The lock may have
+ /// a scope that does not include the Request-URI, or the lock could
+ /// have disappeared, or the token may be invalid.
+ LockTokenMatchesRequestUri,
+
+ /// Name: lock-token-submitted (precondition)
+ ///
+ /// Use with: 423 Locked
+ ///
+ /// Purpose: The request could not succeed because a lock token should
+ /// have been submitted. This element, if present, MUST contain at
+ /// least one URL of a locked resource that prevented the request. In
+ /// cases of MOVE, COPY, and DELETE where collection locks are
+ /// involved, it can be difficult for the client to find out which
+ /// locked resource made the request fail -- but the server is only
+ /// responsible for returning one such locked resource. The server
+ /// MAY return every locked resource that prevented the request from
+ /// succeeding if it knows them all.
+ ///
+ /// <!ELEMENT lock-token-submitted (href+) >
+ LockTokenSubmitted(Vec<Href>),
+
+ /// Name: no-conflicting-lock (precondition)
+ ///
+ /// Use with: Typically 423 Locked
+ ///
+ /// Purpose: A LOCK request failed due the presence of an already
+ /// existing conflicting lock. Note that a lock can be in conflict
+ /// although the resource to which the request was directed is only
+ /// indirectly locked. In this case, the precondition code can be
+ /// used to inform the client about the resource that is the root of
+ /// the conflicting lock, avoiding a separate lookup of the
+ /// "lockdiscovery" property.
+ ///
+ /// <!ELEMENT no-conflicting-lock (href)* >
+ NoConflictingLock(Vec<Href>),
+
+ /// Name: no-external-entities
+ ///
+ /// Use with: 403 Forbidden
+ ///
+ /// Purpose: (precondition) -- If the server rejects a client request
+ /// because the request body contains an external entity, the server
+ /// SHOULD use this error.
+ NoExternalEntities,
+
+ /// Name: preserved-live-properties
+ ///
+ /// Use with: 409 Conflict
+ ///
+ /// Purpose: (postcondition) -- The server received an otherwise-valid
+ /// MOVE or COPY request, but cannot maintain the live properties with
+ /// the same behavior at the destination. It may be that the server
+ /// only supports some live properties in some parts of the
+ /// repository, or simply has an internal error.
+ PreservedLiveProperties,
+
+ /// Name: propfind-finite-depth
+ ///
+ /// Use with: 403 Forbidden
+ ///
+ /// Purpose: (precondition) -- This server does not allow infinite-depth
+ /// PROPFIND requests on collections.
+ PropfindFiniteDepth,
+
+ /// Name: cannot-modify-protected-property
+ ///
+ /// Use with: 403 Forbidden
+ ///
+ /// Purpose: (precondition) -- The client attempted to set a protected
+ /// property in a PROPPATCH (such as DAV:getetag). See also
+ /// [RFC3253], Section 3.12.
+ CannotModifyProtectedProperty,
+
+ /// Specific errors
+ Extension(E::Error),
+}
+
+/// 14.6. exclusive XML Element
+///
+/// Name: exclusive
+///
+/// Purpose: Specifies an exclusive lock.
+///
+/// <!ELEMENT exclusive EMPTY >
+#[derive(Debug, PartialEq)]
+pub struct Exclusive {}
+
+/// 14.7. href XML Element
+///
+/// Name: href
+///
+/// Purpose: MUST contain a URI or a relative reference.
+///
+/// Description: There may be limits on the value of 'href' depending
+/// on the context of its use. Refer to the specification text where
+/// 'href' is used to see what limitations apply in each case.
+///
+/// Value: Simple-ref
+///
+/// <!ELEMENT href (#PCDATA)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct Href(pub String);
+
+/// 14.8. include XML Element
+///
+/// Name: include
+///
+/// Purpose: Any child element represents the name of a property to be
+/// included in the PROPFIND response. All elements inside an
+/// 'include' XML element MUST define properties related to the
+/// resource, although possible property names are in no way limited
+/// to those property names defined in this document or other
+/// standards. This element MUST NOT contain text or mixed content.
+///
+/// <!ELEMENT include ANY >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Include<E: Extension>(pub Vec<PropertyRequest<E>>);
+
+/// 14.9. location XML Element
+///
+/// Name: location
+///
+/// Purpose: HTTP defines the "Location" header (see [RFC2616], Section
+/// 14.30) for use with some status codes (such as 201 and the 300
+/// series codes). When these codes are used inside a 'multistatus'
+/// element, the 'location' element can be used to provide the
+/// accompanying Location header value.
+///
+/// Description: Contains a single href element with the same value
+/// that would be used in a Location header.
+///
+/// <!ELEMENT location (href)>
+#[derive(Debug, PartialEq, Clone)]
+pub struct Location(pub Href);
+
+/// 14.10. lockentry XML Element
+///
+/// Name: lockentry
+///
+/// Purpose: Defines the types of locks that can be used with the
+/// resource.
+///
+/// <!ELEMENT lockentry (lockscope, locktype) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct LockEntry {
+ pub lockscope: LockScope,
+ pub locktype: LockType,
+}
+
+/// 14.11. lockinfo XML Element
+///
+/// Name: lockinfo
+///
+/// Purpose: The 'lockinfo' XML element is used with a LOCK method to
+/// specify the type of lock the client wishes to have created.
+///
+/// <!ELEMENT lockinfo (lockscope, locktype, owner?) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct LockInfo {
+ pub lockscope: LockScope,
+ pub locktype: LockType,
+ pub owner: Option<Owner>,
+}
+
+/// 14.12. lockroot XML Element
+///
+/// Name: lockroot
+///
+/// Purpose: Contains the root URL of the lock, which is the URL
+/// through which the resource was addressed in the LOCK request.
+///
+/// Description: The href element contains the root of the lock. The
+/// server SHOULD include this in all DAV:lockdiscovery property
+/// values and the response to LOCK requests.
+///
+/// <!ELEMENT lockroot (href) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct LockRoot(pub Href);
+
+/// 14.13. lockscope XML Element
+///
+/// Name: lockscope
+///
+/// Purpose: Specifies whether a lock is an exclusive lock, or a shared
+/// lock.
+/// <!ELEMENT lockscope (exclusive | shared) >
+#[derive(Debug, PartialEq, Clone)]
+pub enum LockScope {
+ Exclusive,
+ Shared,
+}
+
+/// 14.14. locktoken XML Element
+///
+/// Name: locktoken
+///
+/// Purpose: The lock token associated with a lock.
+///
+/// Description: The href contains a single lock token URI, which
+/// refers to the lock.
+///
+/// <!ELEMENT locktoken (href) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct LockToken(pub Href);
+
+/// 14.15. locktype XML Element
+///
+/// Name: locktype
+///
+/// Purpose: Specifies the access type of a lock. At present, this
+/// specification only defines one lock type, the write lock.
+///
+/// <!ELEMENT locktype (write) >
+#[derive(Debug, PartialEq, Clone)]
+pub enum LockType {
+ /// 14.30. write XML Element
+ ///
+ /// Name: write
+ ///
+ /// Purpose: Specifies a write lock.
+ ///
+ ///
+ /// <!ELEMENT write EMPTY >
+ Write,
+}
+
+/// 14.16. multistatus XML Element
+///
+/// Name: multistatus
+///
+/// Purpose: Contains multiple response messages.
+///
+/// Description: The 'responsedescription' element at the top level is
+/// used to provide a general message describing the overarching
+/// nature of the response. If this value is available, an
+/// application may use it instead of presenting the individual
+/// response descriptions contained within the responses.
+///
+/// <!ELEMENT multistatus (response*, responsedescription?) >
+///
+/// In WebDAV sync (rfc6578), multistatus is extended:
+///
+/// <!ELEMENT multistatus (response*, responsedescription?, sync-token?) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Multistatus<E: Extension> {
+ pub responses: Vec<Response<E>>,
+ pub responsedescription: Option<ResponseDescription>,
+ pub extension: Option<E::Multistatus>,
+}
+
+/// 14.17. owner XML Element
+///
+/// Name: owner
+///
+/// Purpose: Holds client-supplied information about the creator of a
+/// lock.
+///
+/// Description: Allows a client to provide information sufficient for
+/// either directly contacting a principal (such as a telephone number
+/// or Email URI), or for discovering the principal (such as the URL
+/// of a homepage) who created a lock. The value provided MUST be
+/// treated as a dead property in terms of XML Information Item
+/// preservation. The server MUST NOT alter the value unless the
+/// owner value provided by the client is empty. For a certain amount
+/// of interoperability between different client implementations, if
+/// clients have URI-formatted contact information for the lock
+/// creator suitable for user display, then clients SHOULD put those
+/// URIs in 'href' child elements of the 'owner' element.
+///
+/// Extensibility: MAY be extended with child elements, mixed content,
+/// text content or attributes.
+///
+/// <!ELEMENT owner ANY >
+//@FIXME might need support for an extension
+#[derive(Debug, PartialEq, Clone)]
+pub enum Owner {
+ Txt(String),
+ Href(Href),
+ Unknown,
+}
+
+/// 14.18. prop XML Element
+///
+/// Name: prop
+///
+/// Purpose: Contains properties related to a resource.
+///
+/// Description: A generic container for properties defined on
+/// resources. All elements inside a 'prop' XML element MUST define
+/// properties related to the resource, although possible property
+/// names are in no way limited to those property names defined in
+/// this document or other standards. This element MUST NOT contain
+/// text or mixed content.
+///
+/// <!ELEMENT prop ANY >
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropName<E: Extension>(pub Vec<PropertyRequest<E>>);
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropValue<E: Extension>(pub Vec<Property<E>>);
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct AnyProp<E: Extension>(pub Vec<AnyProperty<E>>);
+
+/// 14.19. propertyupdate XML Element
+///
+/// Name: propertyupdate
+///
+/// Purpose: Contains a request to alter the properties on a resource.
+///
+/// Description: This XML element is a container for the information
+/// required to modify the properties on the resource.
+///
+/// <!ELEMENT propertyupdate (remove | set)+ >
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropertyUpdate<E: Extension>(pub Vec<PropertyUpdateItem<E>>);
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyUpdateItem<E: Extension> {
+ Remove(Remove<E>),
+ Set(Set<E>),
+}
+
+/// 14.2 allprop XML Element
+///
+/// Name: allprop
+///
+/// Purpose: Specifies that all names and values of dead properties and
+/// the live properties defined by this document existing on the
+/// resource are to be returned.
+///
+/// <!ELEMENT allprop EMPTY >
+///
+/// ---
+///
+/// 14.21. propname XML Element
+///
+/// Name: propname
+///
+/// Purpose: Specifies that only a list of property names on the
+/// resource is to be returned.
+///
+/// <!ELEMENT propname EMPTY >
+///
+/// ---
+///
+/// 14.20. propfind XML Element
+///
+/// Name: propfind
+///
+/// Purpose: Specifies the properties to be returned from a PROPFIND
+/// method. Four special elements are specified for use with
+/// 'propfind': 'prop', 'allprop', 'include', and 'propname'. If
+/// 'prop' is used inside 'propfind', it MUST NOT contain property
+/// values.
+///
+/// <!ELEMENT propfind ( propname | (allprop, include?) | prop ) >
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropFind<E: Extension> {
+ PropName,
+ AllProp(Option<Include<E>>),
+ Prop(PropName<E>),
+}
+
+/// 14.22 propstat XML Element
+///
+/// Name: propstat
+///
+/// Purpose: Groups together a prop and status element that is
+/// associated with a particular 'href' element.
+///
+/// Description: The propstat XML element MUST contain one prop XML
+/// element and one status XML element. The contents of the prop XML
+/// element MUST only list the names of properties to which the result
+/// in the status element applies. The optional precondition/
+/// postcondition element and 'responsedescription' text also apply to
+/// the properties named in 'prop'.
+///
+/// <!ELEMENT propstat (prop, status, error?, responsedescription?) >
+///
+/// ---
+///
+///
+#[derive(Debug, PartialEq, Clone)]
+pub struct PropStat<E: Extension> {
+ pub prop: AnyProp<E>,
+ pub status: Status,
+ pub error: Option<Error<E>>,
+ pub responsedescription: Option<ResponseDescription>,
+}
+
+/// 14.23. remove XML Element
+///
+/// Name: remove
+///
+/// Purpose: Lists the properties to be removed from a resource.
+///
+/// Description: Remove instructs that the properties specified in prop
+/// should be removed. Specifying the removal of a property that does
+/// not exist is not an error. All the XML elements in a 'prop' XML
+/// element inside of a 'remove' XML element MUST be empty, as only
+/// the names of properties to be removed are required.
+///
+/// <!ELEMENT remove (prop) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Remove<E: Extension>(pub PropName<E>);
+
+/// 14.24. response XML Element
+///
+/// Name: response
+///
+/// Purpose: Holds a single response describing the effect of a method
+/// on resource and/or its properties.
+///
+/// Description: The 'href' element contains an HTTP URL pointing to a
+/// WebDAV resource when used in the 'response' container. A
+/// particular 'href' value MUST NOT appear more than once as the
+/// child of a 'response' XML element under a 'multistatus' XML
+/// element. This requirement is necessary in order to keep
+/// processing costs for a response to linear time. Essentially, this
+/// prevents having to search in order to group together all the
+/// responses by 'href'. There are, however, no requirements
+/// regarding ordering based on 'href' values. The optional
+/// precondition/postcondition element and 'responsedescription' text
+/// can provide additional information about this resource relative to
+/// the request or result.
+///
+/// <!ELEMENT response (href, ((href*, status)|(propstat+)),
+/// error?, responsedescription? , location?) >
+///
+/// --- rewritten as ---
+/// <!ELEMENT response ((href+, status)|(href, propstat+), error?, responsedescription?, location?>
+#[derive(Debug, PartialEq, Clone)]
+pub enum StatusOrPropstat<E: Extension> {
+ // One status, multiple hrefs...
+ Status(Vec<Href>, Status),
+ // A single href, multiple properties...
+ PropStat(Href, Vec<PropStat<E>>),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct Response<E: Extension> {
+ pub status_or_propstat: StatusOrPropstat<E>,
+ pub error: Option<Error<E>>,
+ pub responsedescription: Option<ResponseDescription>,
+ pub location: Option<Location>,
+}
+
+/// 14.25. responsedescription XML Element
+///
+/// Name: responsedescription
+///
+/// Purpose: Contains information about a status response within a
+/// Multi-Status.
+///
+/// Description: Provides information suitable to be presented to a
+/// user.
+///
+/// <!ELEMENT responsedescription (#PCDATA) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct ResponseDescription(pub String);
+
+/// 14.26. set XML Element
+///
+/// Name: set
+///
+/// Purpose: Lists the property values to be set for a resource.
+///
+/// Description: The 'set' element MUST contain only a 'prop' element.
+/// The elements contained by the 'prop' element inside the 'set'
+/// element MUST specify the name and value of properties that are set
+/// on the resource identified by Request-URI. If a property already
+/// exists, then its value is replaced. Language tagging information
+/// appearing in the scope of the 'prop' element (in the "xml:lang"
+/// attribute, if present) MUST be persistently stored along with the
+/// property, and MUST be subsequently retrievable using PROPFIND.
+///
+/// <!ELEMENT set (prop) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Set<E: Extension>(pub PropValue<E>);
+
+/// 14.27. shared XML Element
+///
+/// Name: shared
+///
+/// Purpose: Specifies a shared lock.
+///
+///
+/// <!ELEMENT shared EMPTY >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Shared {}
+
+/// 14.28. status XML Element
+///
+/// Name: status
+///
+/// Purpose: Holds a single HTTP status-line.
+///
+/// Value: status-line (defined in Section 6.1 of [RFC2616])
+///
+/// <!ELEMENT status (#PCDATA) >
+//@FIXME: Better typing is possible with an enum for example
+#[derive(Debug, PartialEq, Clone)]
+pub struct Status(pub http::status::StatusCode);
+
+/// 14.29. timeout XML Element
+///
+/// Name: timeout
+///
+/// Purpose: The number of seconds remaining before a lock expires.
+///
+/// Value: TimeType (defined in Section 10.7)
+///
+///
+/// <!ELEMENT timeout (#PCDATA) >
+///
+/// TimeOut = "Timeout" ":" 1#TimeType
+/// TimeType = ("Second-" DAVTimeOutVal | "Infinite")
+/// ; No LWS allowed within TimeType
+/// DAVTimeOutVal = 1*DIGIT
+///
+/// Clients MAY include Timeout request headers in their LOCK requests.
+/// However, the server is not required to honor or even consider these
+/// requests. Clients MUST NOT submit a Timeout request header with any
+/// method other than a LOCK method.
+///
+/// The "Second" TimeType specifies the number of seconds that will
+/// elapse between granting of the lock at the server, and the automatic
+/// removal of the lock. The timeout value for TimeType "Second" MUST
+/// NOT be greater than 2^32-1.
+#[derive(Debug, PartialEq, Clone)]
+pub enum Timeout {
+ Seconds(u32),
+ Infinite,
+}
+
+/// 15. DAV Properties
+///
+/// For DAV properties, the name of the property is also the same as the
+/// name of the XML element that contains its value. In the section
+/// below, the final line of each section gives the element type
+/// declaration using the format defined in [REC-XML]. The "Value"
+/// field, where present, specifies further restrictions on the allowable
+/// contents of the XML element using BNF (i.e., to further restrict the
+/// values of a PCDATA element).
+///
+/// A protected property is one that cannot be changed with a PROPPATCH
+/// request. There may be other requests that would result in a change
+/// to a protected property (as when a LOCK request affects the value of
+/// DAV:lockdiscovery). Note that a given property could be protected on
+/// one type of resource, but not protected on another type of resource.
+///
+/// A computed property is one with a value defined in terms of a
+/// computation (based on the content and other properties of that
+/// resource, or even of some other resource). A computed property is
+/// always a protected property.
+///
+/// COPY and MOVE behavior refers to local COPY and MOVE operations.
+///
+/// For properties defined based on HTTP GET response headers (DAV:get*),
+/// the header value could include LWS as defined in [RFC2616], Section
+/// 4.2. Server implementors SHOULD strip LWS from these values before
+/// using as WebDAV property values.
+#[derive(Debug, PartialEq, Clone)]
+pub enum AnyProperty<E: Extension> {
+ Request(PropertyRequest<E>),
+ Value(Property<E>),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest<E: Extension> {
+ CreationDate,
+ DisplayName,
+ GetContentLanguage,
+ GetContentLength,
+ GetContentType,
+ GetEtag,
+ GetLastModified,
+ LockDiscovery,
+ ResourceType,
+ SupportedLock,
+ Extension(E::PropertyRequest),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property<E: Extension> {
+ /// 15.1. creationdate Property
+ ///
+ /// Name: creationdate
+ ///
+ /// Purpose: Records the time and date the resource was created.
+ ///
+ /// Value: date-time (defined in [RFC3339], see the ABNF in Section
+ /// 5.6.)
+ ///
+ /// Protected: MAY be protected. Some servers allow DAV:creationdate
+ /// to be changed to reflect the time the document was created if that
+ /// is more meaningful to the user (rather than the time it was
+ /// uploaded). Thus, clients SHOULD NOT use this property in
+ /// synchronization logic (use DAV:getetag instead).
+ ///
+ /// COPY/MOVE behavior: This property value SHOULD be kept during a
+ /// MOVE operation, but is normally re-initialized when a resource is
+ /// created with a COPY. It should not be set in a COPY.
+ ///
+ /// Description: The DAV:creationdate property SHOULD be defined on all
+ /// DAV compliant resources. If present, it contains a timestamp of
+ /// the moment when the resource was created. Servers that are
+ /// incapable of persistently recording the creation date SHOULD
+ /// instead leave it undefined (i.e. report "Not Found").
+ ///
+ /// <!ELEMENT creationdate (#PCDATA) >
+ CreationDate(DateTime<FixedOffset>),
+
+ /// 15.2. displayname Property
+ ///
+ /// Name: displayname
+ ///
+ /// Purpose: Provides a name for the resource that is suitable for
+ /// presentation to a user.
+ ///
+ /// Value: Any text.
+ ///
+ /// Protected: SHOULD NOT be protected. Note that servers implementing
+ /// [RFC2518] might have made this a protected property as this is a
+ /// new requirement.
+ ///
+ /// COPY/MOVE behavior: This property value SHOULD be preserved in COPY
+ /// and MOVE operations.
+ ///
+ /// Description: Contains a description of the resource that is
+ /// suitable for presentation to a user. This property is defined on
+ /// the resource, and hence SHOULD have the same value independent of
+ /// the Request-URI used to retrieve it (thus, computing this property
+ /// based on the Request-URI is deprecated). While generic clients
+ /// might display the property value to end users, client UI designers
+ /// must understand that the method for identifying resources is still
+ /// the URL. Changes to DAV:displayname do not issue moves or copies
+ /// to the server, but simply change a piece of meta-data on the
+ /// individual resource. Two resources can have the same DAV:
+ /// displayname value even within the same collection.
+ ///
+ /// <!ELEMENT displayname (#PCDATA) >
+ DisplayName(String),
+
+ /// 15.3. getcontentlanguage Property
+ ///
+ /// Name: getcontentlanguage
+ ///
+ /// Purpose: Contains the Content-Language header value (from Section
+ /// 14.12 of [RFC2616]) as it would be returned by a GET without
+ /// accept headers.
+ ///
+ /// Value: language-tag (language-tag is defined in Section 3.10 of
+ /// [RFC2616])
+ ///
+ /// Protected: SHOULD NOT be protected, so that clients can reset the
+ /// language. Note that servers implementing [RFC2518] might have
+ /// made this a protected property as this is a new requirement.
+ ///
+ /// COPY/MOVE behavior: This property value SHOULD be preserved in COPY
+ /// and MOVE operations.
+ ///
+ /// Description: The DAV:getcontentlanguage property MUST be defined on
+ /// any DAV-compliant resource that returns the Content-Language
+ /// header on a GET.
+ ///
+ /// <!ELEMENT getcontentlanguage (#PCDATA) >
+ GetContentLanguage(String),
+
+ /// 15.4. getcontentlength Property
+ ///
+ /// Name: getcontentlength
+ ///
+ /// Purpose: Contains the Content-Length header returned by a GET
+ /// without accept headers.
+ ///
+ /// Value: See Section 14.13 of [RFC2616].
+ ///
+ /// Protected: This property is computed, therefore protected.
+ ///
+ /// Description: The DAV:getcontentlength property MUST be defined on
+ /// any DAV-compliant resource that returns the Content-Length header
+ /// in response to a GET.
+ ///
+ /// COPY/MOVE behavior: This property value is dependent on the size of
+ /// the destination resource, not the value of the property on the
+ /// source resource.
+ ///
+ /// <!ELEMENT getcontentlength (#PCDATA) >
+ GetContentLength(u64),
+
+ /// 15.5. getcontenttype Property
+ ///
+ /// Name: getcontenttype
+ ///
+ /// Purpose: Contains the Content-Type header value (from Section 14.17
+ /// of [RFC2616]) as it would be returned by a GET without accept
+ /// headers.
+ ///
+ /// Value: media-type (defined in Section 3.7 of [RFC2616])
+ ///
+ /// Protected: Potentially protected if the server prefers to assign
+ /// content types on its own (see also discussion in Section 9.7.1).
+ ///
+ /// COPY/MOVE behavior: This property value SHOULD be preserved in COPY
+ /// and MOVE operations.
+ ///
+ /// Description: This property MUST be defined on any DAV-compliant
+ /// resource that returns the Content-Type header in response to a
+ /// GET.
+ ///
+ /// <!ELEMENT getcontenttype (#PCDATA) >
+ GetContentType(String),
+
+ /// 15.6. getetag Property
+ ///
+ /// Name: getetag
+ ///
+ /// Purpose: Contains the ETag header value (from Section 14.19 of
+ /// [RFC2616]) as it would be returned by a GET without accept
+ /// headers.
+ ///
+ /// Value: entity-tag (defined in Section 3.11 of [RFC2616])
+ ///
+ /// Protected: MUST be protected because this value is created and
+ /// controlled by the server.
+ ///
+ /// COPY/MOVE behavior: This property value is dependent on the final
+ /// state of the destination resource, not the value of the property
+ /// on the source resource. Also note the considerations in
+ /// Section 8.8.
+ ///
+ /// Description: The getetag property MUST be defined on any DAV-
+ /// compliant resource that returns the Etag header. Refer to Section
+ /// 3.11 of RFC 2616 for a complete definition of the semantics of an
+ /// ETag, and to Section 8.6 for a discussion of ETags in WebDAV.
+ ///
+ /// <!ELEMENT getetag (#PCDATA) >
+ GetEtag(String),
+
+ /// 15.7. getlastmodified Property
+ ///
+ /// Name: getlastmodified
+ ///
+ /// Purpose: Contains the Last-Modified header value (from Section
+ /// 14.29 of [RFC2616]) as it would be returned by a GET method
+ /// without accept headers.
+ ///
+ /// Value: rfc1123-date (defined in Section 3.3.1 of [RFC2616])
+ ///
+ /// Protected: SHOULD be protected because some clients may rely on the
+ /// value for appropriate caching behavior, or on the value of the
+ /// Last-Modified header to which this property is linked.
+ ///
+ /// COPY/MOVE behavior: This property value is dependent on the last
+ /// modified date of the destination resource, not the value of the
+ /// property on the source resource. Note that some server
+ /// implementations use the file system date modified value for the
+ /// DAV:getlastmodified value, and this can be preserved in a MOVE
+ /// even when the HTTP Last-Modified value SHOULD change. Note that
+ /// since [RFC2616] requires clients to use ETags where provided, a
+ /// server implementing ETags can count on clients using a much better
+ /// mechanism than modification dates for offline synchronization or
+ /// cache control. Also note the considerations in Section 8.8.
+ ///
+ /// Description: The last-modified date on a resource SHOULD only
+ /// reflect changes in the body (the GET responses) of the resource.
+ /// A change in a property only SHOULD NOT cause the last-modified
+ /// date to change, because clients MAY rely on the last-modified date
+ /// to know when to overwrite the existing body. The DAV:
+ /// getlastmodified property MUST be defined on any DAV-compliant
+ /// resource that returns the Last-Modified header in response to a
+ /// GET.
+ ///
+ /// <!ELEMENT getlastmodified (#PCDATA) >
+ GetLastModified(DateTime<FixedOffset>),
+
+ /// 15.8. lockdiscovery Property
+ ///
+ /// Name: lockdiscovery
+ ///
+ /// Purpose: Describes the active locks on a resource
+ ///
+ /// Protected: MUST be protected. Clients change the list of locks
+ /// through LOCK and UNLOCK, not through PROPPATCH.
+ ///
+ /// COPY/MOVE behavior: The value of this property depends on the lock
+ /// state of the destination, not on the locks of the source resource.
+ /// Recall that locks are not moved in a MOVE operation.
+ ///
+ /// Description: Returns a listing of who has a lock, what type of lock
+ /// he has, the timeout type and the time remaining on the timeout,
+ /// and the associated lock token. Owner information MAY be omitted
+ /// if it is considered sensitive. If there are no locks, but the
+ /// server supports locks, the property will be present but contain
+ /// zero 'activelock' elements. If there are one or more locks, an
+ /// 'activelock' element appears for each lock on the resource. This
+ /// property is NOT lockable with respect to write locks (Section 7).
+ ///
+ /// <!ELEMENT lockdiscovery (activelock)* >
+ LockDiscovery(Vec<ActiveLock>),
+
+ /// 15.9. resourcetype Property
+ ///
+ /// Name: resourcetype
+ ///
+ /// Purpose: Specifies the nature of the resource.
+ ///
+ /// Protected: SHOULD be protected. Resource type is generally decided
+ /// through the operation creating the resource (MKCOL vs PUT), not by
+ /// PROPPATCH.
+ ///
+ /// COPY/MOVE behavior: Generally a COPY/MOVE of a resource results in
+ /// the same type of resource at the destination.
+ ///
+ /// Description: MUST be defined on all DAV-compliant resources. Each
+ /// child element identifies a specific type the resource belongs to,
+ /// such as 'collection', which is the only resource type defined by
+ /// this specification (see Section 14.3). If the element contains
+ /// the 'collection' child element plus additional unrecognized
+ /// elements, it should generally be treated as a collection. If the
+ /// element contains no recognized child elements, it should be
+ /// treated as a non-collection resource. The default value is empty.
+ /// This element MUST NOT contain text or mixed content. Any custom
+ /// child element is considered to be an identifier for a resource
+ /// type.
+ ///
+ /// Example: (fictional example to show extensibility)
+ ///
+ /// <x:resourcetype xmlns:x="DAV:">
+ /// <x:collection/>
+ /// <f:search-results xmlns:f="http://www.example.com/ns"/>
+ /// </x:resourcetype>
+ ResourceType(Vec<ResourceType<E>>),
+
+ /// 15.10. supportedlock Property
+ ///
+ /// Name: supportedlock
+ ///
+ /// Purpose: To provide a listing of the lock capabilities supported by
+ /// the resource.
+ ///
+ /// Protected: MUST be protected. Servers, not clients, determine what
+ /// lock mechanisms are supported.
+ /// COPY/MOVE behavior: This property value is dependent on the kind of
+ /// locks supported at the destination, not on the value of the
+ /// property at the source resource. Servers attempting to COPY to a
+ /// destination should not attempt to set this property at the
+ /// destination.
+ ///
+ /// Description: Returns a listing of the combinations of scope and
+ /// access types that may be specified in a lock request on the
+ /// resource. Note that the actual contents are themselves controlled
+ /// by access controls, so a server is not required to provide
+ /// information the client is not authorized to see. This property is
+ /// NOT lockable with respect to write locks (Section 7).
+ ///
+ /// <!ELEMENT supportedlock (lockentry)* >
+ SupportedLock(Vec<LockEntry>),
+
+ /// Any extension
+ Extension(E::Property),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ResourceType<E: Extension> {
+ Collection,
+ Extension(E::ResourceType),
+}
diff --git a/aero-dav/src/versioningdecoder.rs b/aero-dav/src/versioningdecoder.rs
new file mode 100644
index 0000000..a0a3ddf
--- /dev/null
+++ b/aero-dav/src/versioningdecoder.rs
@@ -0,0 +1,132 @@
+use super::error::ParsingError;
+use super::types as dav;
+use super::versioningtypes::*;
+use super::xml::{IRead, QRead, Reader, DAV_URN};
+
+// -- extensions ---
+impl QRead<PropertyRequest> for PropertyRequest {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open(DAV_URN, "supported-report-set")
+ .await?
+ .is_some()
+ {
+ xml.close().await?;
+ return Ok(Self::SupportedReportSet);
+ }
+ return Err(ParsingError::Recoverable);
+ }
+}
+
+impl<E: dav::Extension> QRead<Property<E>> for Property<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml
+ .maybe_open_start(DAV_URN, "supported-report-set")
+ .await?
+ .is_some()
+ {
+ let supported_reports = xml.collect().await?;
+ xml.close().await?;
+ return Ok(Property::SupportedReportSet(supported_reports));
+ }
+ Err(ParsingError::Recoverable)
+ }
+}
+
+impl<E: dav::Extension> QRead<SupportedReport<E>> for SupportedReport<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "supported-report").await?;
+ let r = xml.find().await?;
+ xml.close().await?;
+ Ok(SupportedReport(r))
+ }
+}
+
+impl<E: dav::Extension> QRead<ReportName<E>> for ReportName<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "report").await?;
+
+ let final_result = if xml.maybe_open(DAV_URN, "version-tree").await?.is_some() {
+ xml.close().await?;
+ Ok(ReportName::VersionTree)
+ } else if xml.maybe_open(DAV_URN, "expand-property").await?.is_some() {
+ xml.close().await?;
+ Ok(ReportName::ExpandProperty)
+ } else {
+ let x = match xml.maybe_find().await? {
+ Some(v) => v,
+ None => return Err(ParsingError::MissingChild),
+ };
+ Ok(ReportName::Extension(x))
+ //E::ReportTypeName::qread(xml).await.map(ReportName::Extension)
+ };
+
+ xml.close().await?;
+ final_result
+ }
+}
+
+impl<E: dav::Extension> QRead<Report<E>> for Report<E> {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ if xml.maybe_open(DAV_URN, "version-tree").await?.is_some() {
+ xml.close().await?;
+ tracing::warn!("version-tree is not implemented, skipping");
+ Ok(Report::VersionTree)
+ } else if xml.maybe_open(DAV_URN, "expand-property").await?.is_some() {
+ xml.close().await?;
+ tracing::warn!("expand-property is not implemented, skipping");
+ Ok(Report::ExpandProperty)
+ } else {
+ E::ReportType::qread(xml).await.map(Report::Extension)
+ }
+ }
+}
+
+impl QRead<Limit> for Limit {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "limit").await?;
+ let nres = xml.find().await?;
+ xml.close().await?;
+ Ok(Limit(nres))
+ }
+}
+
+impl QRead<NResults> for NResults {
+ async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
+ xml.open(DAV_URN, "nresults").await?;
+ let sz = xml.tag_string().await?.parse::<u64>()?;
+ xml.close().await?;
+ Ok(NResults(sz))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::xml::Node;
+
+ async fn deserialize<T: Node<T>>(src: &str) -> T {
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn nresults() {
+ let expected = NResults(100);
+ let src = r#"<D:nresults xmlns:D="DAV:">100</D:nresults>"#;
+ let got = deserialize::<NResults>(src).await;
+ assert_eq!(got, expected);
+ }
+
+ #[tokio::test]
+ async fn limit() {
+ let expected = Limit(NResults(1024));
+ let src = r#"<D:limit xmlns:D="DAV:">
+ <D:nresults>1024</D:nresults>
+ </D:limit>"#;
+ let got = deserialize::<Limit>(src).await;
+ assert_eq!(got, expected);
+ }
+}
diff --git a/aero-dav/src/versioningencoder.rs b/aero-dav/src/versioningencoder.rs
new file mode 100644
index 0000000..c061f07
--- /dev/null
+++ b/aero-dav/src/versioningencoder.rs
@@ -0,0 +1,143 @@
+use quick_xml::events::{BytesText, Event};
+use quick_xml::Error as QError;
+
+use super::types::Extension;
+use super::versioningtypes::*;
+use super::xml::{IWrite, QWrite, Writer};
+
+// --- extensions to PROP
+impl QWrite for PropertyRequest {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SupportedReportSet => {
+ let start = xml.create_dav_element("supported-report-set");
+ xml.q.write_event_async(Event::Empty(start)).await
+ }
+ }
+ }
+}
+
+impl<E: Extension> QWrite for Property<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Self::SupportedReportSet(set) => {
+ let start = xml.create_dav_element("supported-report-set");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ for v in set.iter() {
+ v.qwrite(xml).await?;
+ }
+ xml.q.write_event_async(Event::End(end)).await
+ }
+ }
+ }
+}
+
+impl<E: Extension> QWrite for SupportedReport<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("supported-report");
+ let end = start.to_end();
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl<E: Extension> QWrite for ReportName<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("report");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ match self {
+ Self::VersionTree => {
+ let start = xml.create_dav_element("version-tree");
+ xml.q.write_event_async(Event::Empty(start)).await?;
+ }
+ Self::ExpandProperty => {
+ let start = xml.create_dav_element("expand-property");
+ xml.q.write_event_async(Event::Empty(start)).await?;
+ }
+ Self::Extension(ext) => ext.qwrite(xml).await?,
+ };
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+// --- root REPORT object ---
+impl<E: Extension> QWrite for Report<E> {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ match self {
+ Report::VersionTree => unimplemented!(),
+ Report::ExpandProperty => unimplemented!(),
+ Report::Extension(inner) => inner.qwrite(xml).await,
+ }
+ }
+}
+
+// --- limit REPORT parameter ---
+impl QWrite for Limit {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("limit");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ self.0.qwrite(xml).await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+impl QWrite for NResults {
+ async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
+ let start = xml.create_dav_element("nresults");
+ let end = start.to_end();
+
+ xml.q.write_event_async(Event::Start(start.clone())).await?;
+ xml.q
+ .write_event_async(Event::Text(BytesText::new(&format!("{}", self.0))))
+ .await?;
+ xml.q.write_event_async(Event::End(end)).await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::xml::Node;
+ use crate::xml::Reader;
+ use tokio::io::AsyncWriteExt;
+
+ async fn serialize_deserialize<T: Node<T>>(src: &T) -> T {
+ let mut buffer = Vec::new();
+ let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
+ let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
+ let ns_to_apply = vec![
+ ("xmlns:D".into(), "DAV:".into()),
+ ("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()),
+ ];
+ let mut writer = Writer { q, ns_to_apply };
+
+ src.qwrite(&mut writer).await.expect("xml serialization");
+ tokio_buffer.flush().await.expect("tokio buffer flush");
+ let got = std::str::from_utf8(buffer.as_slice()).unwrap();
+
+ // deserialize
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(got.as_bytes()))
+ .await
+ .unwrap();
+ rdr.find().await.unwrap()
+ }
+
+ #[tokio::test]
+ async fn nresults() {
+ let orig = NResults(100);
+ assert_eq!(orig, serialize_deserialize(&orig).await);
+ }
+
+ #[tokio::test]
+ async fn limit() {
+ let orig = Limit(NResults(1024));
+ assert_eq!(orig, serialize_deserialize(&orig).await);
+ }
+}
diff --git a/aero-dav/src/versioningtypes.rs b/aero-dav/src/versioningtypes.rs
new file mode 100644
index 0000000..1f8d1cf
--- /dev/null
+++ b/aero-dav/src/versioningtypes.rs
@@ -0,0 +1,59 @@
+use super::types as dav;
+
+//@FIXME required for a full DAV implementation
+// See section 7.1 of the CalDAV RFC
+// It seems it's mainly due to the fact that the REPORT method is re-used.
+// https://datatracker.ietf.org/doc/html/rfc4791#section-7.1
+//
+// Defines (required by CalDAV):
+// - REPORT method
+// - expand-property root report method
+//
+// Defines (required by Sync):
+// - limit, nresults
+// - supported-report-set
+
+// This property identifies the reports that are supported by the
+// resource.
+//
+// <!ELEMENT supported-report-set (supported-report*)>
+// <!ELEMENT supported-report report>
+// <!ELEMENT report ANY>
+// ANY value: a report element type
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum PropertyRequest {
+ SupportedReportSet,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Property<E: dav::Extension> {
+ SupportedReportSet(Vec<SupportedReport<E>>),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct SupportedReport<E: dav::Extension>(pub ReportName<E>);
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum ReportName<E: dav::Extension> {
+ VersionTree,
+ ExpandProperty,
+ Extension(E::ReportTypeName),
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Report<E: dav::Extension> {
+ VersionTree, // Not yet implemented
+ ExpandProperty, // Not yet implemented
+ Extension(E::ReportType),
+}
+
+/// Limit
+/// <!ELEMENT limit (nresults) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct Limit(pub NResults);
+
+/// NResults
+/// <!ELEMENT nresults (#PCDATA) >
+#[derive(Debug, PartialEq, Clone)]
+pub struct NResults(pub u64);
diff --git a/aero-dav/src/xml.rs b/aero-dav/src/xml.rs
new file mode 100644
index 0000000..e59f136
--- /dev/null
+++ b/aero-dav/src/xml.rs
@@ -0,0 +1,367 @@
+use futures::Future;
+use quick_xml::events::{BytesStart, Event};
+use quick_xml::name::ResolveResult;
+use quick_xml::reader::NsReader;
+use tokio::io::{AsyncBufRead, AsyncWrite};
+
+use super::error::ParsingError;
+
+// Constants
+pub const DAV_URN: &[u8] = b"DAV:";
+pub const CAL_URN: &[u8] = b"urn:ietf:params:xml:ns:caldav";
+pub const CARD_URN: &[u8] = b"urn:ietf:params:xml:ns:carddav";
+
+// Async traits
+pub trait IWrite = AsyncWrite + Unpin + Send;
+pub trait IRead = AsyncBufRead + Unpin;
+
+// Serialization/Deserialization traits
+pub trait QWrite {
+ fn qwrite(
+ &self,
+ xml: &mut Writer<impl IWrite>,
+ ) -> impl Future<Output = Result<(), quick_xml::Error>> + Send;
+}
+pub trait QRead<T> {
+ fn qread(xml: &mut Reader<impl IRead>) -> impl Future<Output = Result<T, ParsingError>>;
+}
+
+// The representation of an XML node in Rust
+pub trait Node<T> = QRead<T> + QWrite + std::fmt::Debug + PartialEq + Clone + Sync;
+
+// ---------------
+
+/// Transform a Rust object into an XML stream of characters
+pub struct Writer<T: IWrite> {
+ pub q: quick_xml::writer::Writer<T>,
+ pub ns_to_apply: Vec<(String, String)>,
+}
+impl<T: IWrite> Writer<T> {
+ pub fn create_dav_element(&mut self, name: &str) -> BytesStart<'static> {
+ self.create_ns_element("D", name)
+ }
+ pub fn create_cal_element(&mut self, name: &str) -> BytesStart<'static> {
+ self.create_ns_element("C", name)
+ }
+
+ fn create_ns_element(&mut self, ns: &str, name: &str) -> BytesStart<'static> {
+ let mut start = BytesStart::new(format!("{}:{}", ns, name));
+ if !self.ns_to_apply.is_empty() {
+ start.extend_attributes(
+ self.ns_to_apply
+ .iter()
+ .map(|(k, n)| (k.as_str(), n.as_str())),
+ );
+ self.ns_to_apply.clear()
+ }
+ start
+ }
+}
+
+/// Transform an XML stream of characters into a Rust object
+pub struct Reader<T: IRead> {
+ pub rdr: NsReader<T>,
+ cur: Event<'static>,
+ prev: Event<'static>,
+ parents: Vec<Event<'static>>,
+ buf: Vec<u8>,
+}
+impl<T: IRead> Reader<T> {
+ pub async fn new(mut rdr: NsReader<T>) -> Result<Self, ParsingError> {
+ let mut buf: Vec<u8> = vec![];
+ let cur = rdr.read_event_into_async(&mut buf).await?.into_owned();
+ let parents = vec![];
+ let prev = Event::Eof;
+ buf.clear();
+ Ok(Self {
+ cur,
+ prev,
+ parents,
+ rdr,
+ buf,
+ })
+ }
+
+ /// read one more tag
+ /// do not expose it publicly
+ async fn next(&mut self) -> Result<Event<'static>, ParsingError> {
+ let evt = self
+ .rdr
+ .read_event_into_async(&mut self.buf)
+ .await?
+ .into_owned();
+ self.buf.clear();
+ self.prev = std::mem::replace(&mut self.cur, evt);
+ Ok(self.prev.clone())
+ }
+
+ /// skip a node at current level
+ /// I would like to make this one private but not ready
+ pub async fn skip(&mut self) -> Result<Event<'static>, ParsingError> {
+ //println!("skipping inside node {:?} value {:?}", self.parents.last(), self.cur);
+ match &self.cur {
+ Event::Start(b) => {
+ let _span = self
+ .rdr
+ .read_to_end_into_async(b.to_end().name(), &mut self.buf)
+ .await?;
+ self.next().await
+ }
+ Event::End(_) => Err(ParsingError::WrongToken),
+ Event::Eof => Err(ParsingError::Eof),
+ _ => self.next().await,
+ }
+ }
+
+ /// check if this is the desired tag
+ fn is_tag(&self, ns: &[u8], key: &str) -> bool {
+ let qname = match self.peek() {
+ Event::Start(bs) | Event::Empty(bs) => bs.name(),
+ Event::End(be) => be.name(),
+ _ => return false,
+ };
+
+ let (extr_ns, local) = self.rdr.resolve_element(qname);
+
+ if local.into_inner() != key.as_bytes() {
+ return false;
+ }
+
+ match extr_ns {
+ ResolveResult::Bound(v) => v.into_inner() == ns,
+ _ => false,
+ }
+ }
+
+ pub fn parent_has_child(&self) -> bool {
+ matches!(self.parents.last(), Some(Event::Start(_)) | None)
+ }
+
+ fn ensure_parent_has_child(&self) -> Result<(), ParsingError> {
+ match self.parent_has_child() {
+ true => Ok(()),
+ false => Err(ParsingError::Recoverable),
+ }
+ }
+
+ pub fn peek(&self) -> &Event<'static> {
+ &self.cur
+ }
+
+ pub fn previous(&self) -> &Event<'static> {
+ &self.prev
+ }
+
+ // NEW API
+ pub async fn tag_string(&mut self) -> Result<String, ParsingError> {
+ self.ensure_parent_has_child()?;
+
+ let mut acc = String::new();
+ loop {
+ match self.peek() {
+ Event::CData(unescaped) => {
+ acc.push_str(std::str::from_utf8(unescaped.as_ref())?);
+ self.next().await?
+ }
+ Event::Text(escaped) => {
+ acc.push_str(escaped.unescape()?.as_ref());
+ self.next().await?
+ }
+ Event::End(_) | Event::Start(_) | Event::Empty(_) => return Ok(acc),
+ _ => self.next().await?,
+ };
+ }
+ }
+
+ pub async fn maybe_read<N: Node<N>>(
+ &mut self,
+ t: &mut Option<N>,
+ dirty: &mut bool,
+ ) -> Result<(), ParsingError> {
+ if !self.parent_has_child() {
+ return Ok(());
+ }
+
+ match N::qread(self).await {
+ Ok(v) => {
+ *t = Some(v);
+ *dirty = true;
+ Ok(())
+ }
+ Err(ParsingError::Recoverable) => Ok(()),
+ Err(e) => Err(e),
+ }
+ }
+
+ pub async fn maybe_push<N: Node<N>>(
+ &mut self,
+ t: &mut Vec<N>,
+ dirty: &mut bool,
+ ) -> Result<(), ParsingError> {
+ if !self.parent_has_child() {
+ return Ok(());
+ }
+
+ match N::qread(self).await {
+ Ok(v) => {
+ t.push(v);
+ *dirty = true;
+ Ok(())
+ }
+ Err(ParsingError::Recoverable) => Ok(()),
+ Err(e) => Err(e),
+ }
+ }
+
+ pub async fn find<N: Node<N>>(&mut self) -> Result<N, ParsingError> {
+ self.ensure_parent_has_child()?;
+
+ loop {
+ // Try parse
+ match N::qread(self).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise,
+ }
+
+ // If recovered, skip the element
+ self.skip().await?;
+ }
+ }
+
+ pub async fn maybe_find<N: Node<N>>(&mut self) -> Result<Option<N>, ParsingError> {
+ // We can't find anything inside a self-closed tag
+ if !self.parent_has_child() {
+ return Ok(None);
+ }
+
+ loop {
+ // Try parse
+ match N::qread(self).await {
+ Err(ParsingError::Recoverable) => (),
+ otherwise => return otherwise.map(Some),
+ }
+
+ // Skip or stop
+ match self.peek() {
+ Event::End(_) => return Ok(None),
+ _ => self.skip().await?,
+ };
+ }
+ }
+
+ pub async fn collect<N: Node<N>>(&mut self) -> Result<Vec<N>, ParsingError> {
+ let mut acc = Vec::new();
+ if !self.parent_has_child() {
+ return Ok(acc);
+ }
+
+ loop {
+ match N::qread(self).await {
+ Err(ParsingError::Recoverable) => match self.peek() {
+ Event::End(_) => return Ok(acc),
+ _ => {
+ self.skip().await?;
+ }
+ },
+ Ok(v) => acc.push(v),
+ Err(e) => return Err(e),
+ }
+ }
+ }
+
+ pub async fn open(&mut self, ns: &[u8], key: &str) -> Result<Event<'static>, ParsingError> {
+ //println!("try open tag {:?}, on {:?}", key, self.peek());
+ let evt = match self.peek() {
+ Event::Empty(_) if self.is_tag(ns, key) => {
+ // hack to make `prev_attr` works
+ // here we duplicate the current tag
+ // as in other words, we virtually moved one token
+ // which is useful for prev_attr and any logic based on
+ // self.prev + self.open() on empty nodes
+ self.prev = self.cur.clone();
+ self.cur.clone()
+ }
+ Event::Start(_) if self.is_tag(ns, key) => self.next().await?,
+ _ => return Err(ParsingError::Recoverable),
+ };
+
+ //println!("open tag {:?}", evt);
+ self.parents.push(evt.clone());
+ Ok(evt)
+ }
+
+ pub async fn open_start(
+ &mut self,
+ ns: &[u8],
+ key: &str,
+ ) -> Result<Event<'static>, ParsingError> {
+ //println!("try open start tag {:?}, on {:?}", key, self.peek());
+ let evt = match self.peek() {
+ Event::Start(_) if self.is_tag(ns, key) => self.next().await?,
+ _ => return Err(ParsingError::Recoverable),
+ };
+
+ //println!("open start tag {:?}", evt);
+ self.parents.push(evt.clone());
+ Ok(evt)
+ }
+
+ pub async fn maybe_open(
+ &mut self,
+ ns: &[u8],
+ key: &str,
+ ) -> Result<Option<Event<'static>>, ParsingError> {
+ match self.open(ns, key).await {
+ Ok(v) => Ok(Some(v)),
+ Err(ParsingError::Recoverable) => Ok(None),
+ Err(e) => Err(e),
+ }
+ }
+
+ pub async fn maybe_open_start(
+ &mut self,
+ ns: &[u8],
+ key: &str,
+ ) -> Result<Option<Event<'static>>, ParsingError> {
+ match self.open_start(ns, key).await {
+ Ok(v) => Ok(Some(v)),
+ Err(ParsingError::Recoverable) => Ok(None),
+ Err(e) => Err(e),
+ }
+ }
+
+ pub fn prev_attr(&self, attr: &str) -> Option<String> {
+ match &self.prev {
+ Event::Start(bs) | Event::Empty(bs) => match bs.try_get_attribute(attr) {
+ Ok(Some(attr)) => attr
+ .decode_and_unescape_value(&self.rdr)
+ .ok()
+ .map(|v| v.into_owned()),
+ _ => None,
+ },
+ _ => None,
+ }
+ }
+
+ // find stop tag
+ pub async fn close(&mut self) -> Result<Event<'static>, ParsingError> {
+ //println!("close tag {:?}", self.parents.last());
+
+ // Handle the empty case
+ if !self.parent_has_child() {
+ self.parents.pop();
+ return self.next().await;
+ }
+
+ // Handle the start/end case
+ loop {
+ match self.peek() {
+ Event::End(_) => {
+ self.parents.pop();
+ return self.next().await;
+ }
+ _ => self.skip().await?,
+ };
+ }
+ }
+}
diff --git a/aero-ical/Cargo.toml b/aero-ical/Cargo.toml
new file mode 100644
index 0000000..6cfe882
--- /dev/null
+++ b/aero-ical/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "aero-ical"
+version = "0.3.0"
+authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
+edition = "2021"
+license = "EUPL-1.2"
+description = "An iCalendar parser"
+
+[dependencies]
+aero-dav.workspace = true
+
+icalendar.workspace = true
+nom.workspace = true
+chrono.workspace = true
+tracing.workspace = true
diff --git a/aero-ical/src/lib.rs b/aero-ical/src/lib.rs
new file mode 100644
index 0000000..3f6f633
--- /dev/null
+++ b/aero-ical/src/lib.rs
@@ -0,0 +1,8 @@
+/// The iCalendar module is not yet properly rewritten
+/// Instead we heavily rely on the icalendar library
+/// However, for many reason, it's not satisfying:
+/// the goal will be to rewrite it in the end so it better
+/// integrates into Aerogramme
+pub mod parser;
+pub mod prune;
+pub mod query;
diff --git a/aero-ical/src/parser.rs b/aero-ical/src/parser.rs
new file mode 100644
index 0000000..ca271a5
--- /dev/null
+++ b/aero-ical/src/parser.rs
@@ -0,0 +1,146 @@
+use chrono::TimeDelta;
+
+use nom::branch::alt;
+use nom::bytes::complete::{tag, tag_no_case};
+use nom::character::complete as nomchar;
+use nom::combinator::{map, map_opt, opt, value};
+use nom::sequence::{pair, tuple};
+use nom::IResult;
+
+use aero_dav::caltypes as cal;
+
+//@FIXME too simple, we have 4 cases in practices:
+// - floating datetime
+// - floating datetime with a tzid as param so convertible to tz datetime
+// - utc datetime
+// - floating(?) date (without time)
+pub fn date_time(dt: &str) -> Option<chrono::DateTime<chrono::Utc>> {
+ tracing::trace!(raw_time = dt, "VEVENT raw time");
+ let tmpl = match dt.chars().last() {
+ Some('Z') => cal::UTC_DATETIME_FMT,
+ Some(_) => {
+ tracing::warn!(
+ raw_time = dt,
+ "floating datetime is not properly supported yet"
+ );
+ cal::FLOATING_DATETIME_FMT
+ }
+ None => return None,
+ };
+
+ chrono::NaiveDateTime::parse_from_str(dt, tmpl)
+ .ok()
+ .map(|v| v.and_utc())
+}
+
+/// RFC3389 Duration Value
+///
+/// ```abnf
+/// dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week)
+/// dur-date = dur-day [dur-time]
+/// dur-time = "T" (dur-hour / dur-minute / dur-second)
+/// dur-week = 1*DIGIT "W"
+/// dur-hour = 1*DIGIT "H" [dur-minute]
+/// dur-minute = 1*DIGIT "M" [dur-second]
+/// dur-second = 1*DIGIT "S"
+/// dur-day = 1*DIGIT "D"
+/// ```
+pub fn dur_value(text: &str) -> IResult<&str, TimeDelta> {
+ map_opt(
+ tuple((
+ dur_sign,
+ tag_no_case("P"),
+ alt((dur_date, dur_time, dur_week)),
+ )),
+ |(sign, _, delta)| delta.checked_mul(sign),
+ )(text)
+}
+
+fn dur_sign(text: &str) -> IResult<&str, i32> {
+ map(opt(alt((value(1, tag("+")), value(-1, tag("-"))))), |x| {
+ x.unwrap_or(1)
+ })(text)
+}
+fn dur_date(text: &str) -> IResult<&str, TimeDelta> {
+ map(pair(dur_day, opt(dur_time)), |(day, time)| {
+ day + time.unwrap_or(TimeDelta::zero())
+ })(text)
+}
+fn dur_time(text: &str) -> IResult<&str, TimeDelta> {
+ map(
+ pair(tag_no_case("T"), alt((dur_hour, dur_minute, dur_second))),
+ |(_, x)| x,
+ )(text)
+}
+fn dur_week(text: &str) -> IResult<&str, TimeDelta> {
+ map_opt(pair(nomchar::i64, tag_no_case("W")), |(i, _)| {
+ TimeDelta::try_weeks(i)
+ })(text)
+}
+fn dur_day(text: &str) -> IResult<&str, TimeDelta> {
+ map_opt(pair(nomchar::i64, tag_no_case("D")), |(i, _)| {
+ TimeDelta::try_days(i)
+ })(text)
+}
+fn dur_hour(text: &str) -> IResult<&str, TimeDelta> {
+ map_opt(
+ tuple((nomchar::i64, tag_no_case("H"), opt(dur_minute))),
+ |(i, _, mm)| TimeDelta::try_hours(i).map(|hours| hours + mm.unwrap_or(TimeDelta::zero())),
+ )(text)
+}
+fn dur_minute(text: &str) -> IResult<&str, TimeDelta> {
+ map_opt(
+ tuple((nomchar::i64, tag_no_case("M"), opt(dur_second))),
+ |(i, _, ms)| TimeDelta::try_minutes(i).map(|min| min + ms.unwrap_or(TimeDelta::zero())),
+ )(text)
+}
+fn dur_second(text: &str) -> IResult<&str, TimeDelta> {
+ map_opt(pair(nomchar::i64, tag_no_case("S")), |(i, _)| {
+ TimeDelta::try_seconds(i)
+ })(text)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn rfc5545_example1() {
+ // A duration of 15 days, 5 hours, and 20 seconds would be:
+ let to_parse = "P15DT5H0M20S";
+ let (_, time_delta) = dur_value(to_parse).unwrap();
+ assert_eq!(
+ time_delta,
+ TimeDelta::try_days(15).unwrap()
+ + TimeDelta::try_hours(5).unwrap()
+ + TimeDelta::try_seconds(20).unwrap()
+ );
+ }
+
+ #[test]
+ fn rfc5545_example2() {
+ // A duration of 7 weeks would be:
+ let to_parse = "P7W";
+ let (_, time_delta) = dur_value(to_parse).unwrap();
+ assert_eq!(time_delta, TimeDelta::try_weeks(7).unwrap());
+ }
+
+ #[test]
+ fn rfc4791_example1() {
+ // 10 minutes before
+ let to_parse = "-PT10M";
+
+ let (_, time_delta) = dur_value(to_parse).unwrap();
+ assert_eq!(time_delta, TimeDelta::try_minutes(-10).unwrap());
+ }
+
+ #[test]
+ fn ical_org_example1() {
+ // The following example is for a "VALARM" calendar component that specifies an email alarm
+ // that will trigger 2 days before the scheduled due DATE-TIME of a to-do with which it is associated.
+ let to_parse = "-P2D";
+
+ let (_, time_delta) = dur_value(to_parse).unwrap();
+ assert_eq!(time_delta, TimeDelta::try_days(-2).unwrap());
+ }
+}
diff --git a/aero-ical/src/prune.rs b/aero-ical/src/prune.rs
new file mode 100644
index 0000000..3eb50ca
--- /dev/null
+++ b/aero-ical/src/prune.rs
@@ -0,0 +1,55 @@
+use aero_dav::caltypes as cal;
+use icalendar::parser::{Component, Property};
+
+pub fn component<'a>(src: &'a Component<'a>, prune: &cal::Comp) -> Option<Component<'a>> {
+ if src.name.as_str() != prune.name.as_str() {
+ return None;
+ }
+
+ let name = src.name.clone();
+
+ let properties = match &prune.prop_kind {
+ Some(cal::PropKind::AllProp) | None => src.properties.clone(),
+ Some(cal::PropKind::Prop(l)) => src
+ .properties
+ .iter()
+ .filter_map(|prop| {
+ let sel_filt = match l
+ .iter()
+ .find(|filt| filt.name.0.as_str() == prop.name.as_str())
+ {
+ Some(v) => v,
+ None => return None,
+ };
+
+ match sel_filt.novalue {
+ None | Some(false) => Some(prop.clone()),
+ Some(true) => Some(Property {
+ name: prop.name.clone(),
+ params: prop.params.clone(),
+ val: "".into(),
+ }),
+ }
+ })
+ .collect::<Vec<_>>(),
+ };
+
+ let components = match &prune.comp_kind {
+ Some(cal::CompKind::AllComp) | None => src.components.clone(),
+ Some(cal::CompKind::Comp(many_inner_prune)) => src
+ .components
+ .iter()
+ .filter_map(|src_component| {
+ many_inner_prune
+ .iter()
+ .find_map(|inner_prune| component(src_component, inner_prune))
+ })
+ .collect::<Vec<_>>(),
+ };
+
+ Some(Component {
+ name,
+ properties,
+ components,
+ })
+}
diff --git a/aero-ical/src/query.rs b/aero-ical/src/query.rs
new file mode 100644
index 0000000..d69a919
--- /dev/null
+++ b/aero-ical/src/query.rs
@@ -0,0 +1,338 @@
+use crate::parser;
+use aero_dav::caltypes as cal;
+
+pub fn is_component_match(
+ parent: &icalendar::parser::Component,
+ components: &[icalendar::parser::Component],
+ filter: &cal::CompFilter,
+) -> bool {
+ // Find the component among the list
+ let maybe_comps = components
+ .iter()
+ .filter(|candidate| candidate.name.as_str() == filter.name.as_str())
+ .collect::<Vec<_>>();
+
+ // Filter according to rules
+ match (&maybe_comps[..], &filter.additional_rules) {
+ ([_, ..], None) => true,
+ ([], Some(cal::CompFilterRules::IsNotDefined)) => true,
+ ([], None) => false,
+ ([_, ..], Some(cal::CompFilterRules::IsNotDefined)) => false,
+ (comps, Some(cal::CompFilterRules::Matches(matcher))) => comps.iter().any(|component| {
+ // check time range
+ if let Some(time_range) = &matcher.time_range {
+ if !is_in_time_range(
+ &filter.name,
+ parent,
+ component.properties.as_ref(),
+ time_range,
+ ) {
+ return false;
+ }
+ }
+
+ // check properties
+ if !is_properties_match(component.properties.as_ref(), matcher.prop_filter.as_ref()) {
+ return false;
+ }
+
+ // check inner components
+ matcher.comp_filter.iter().all(|inner_filter| {
+ is_component_match(component, component.components.as_ref(), &inner_filter)
+ })
+ }),
+ }
+}
+
+fn prop_date(
+ properties: &[icalendar::parser::Property],
+ name: &str,
+) -> Option<chrono::DateTime<chrono::Utc>> {
+ properties
+ .iter()
+ .find(|candidate| candidate.name.as_str() == name)
+ .map(|p| p.val.as_str())
+ .map(parser::date_time)
+ .flatten()
+}
+
+fn prop_parse<T: std::str::FromStr>(
+ properties: &[icalendar::parser::Property],
+ name: &str,
+) -> Option<T> {
+ properties
+ .iter()
+ .find(|candidate| candidate.name.as_str() == name)
+ .map(|p| p.val.as_str().parse::<T>().ok())
+ .flatten()
+}
+
+fn is_properties_match(props: &[icalendar::parser::Property], filters: &[cal::PropFilter]) -> bool {
+ filters.iter().all(|single_filter| {
+ // Find the property
+ let candidate_props = props
+ .iter()
+ .filter(|candidate| candidate.name.as_str() == single_filter.name.0.as_str())
+ .collect::<Vec<_>>();
+
+ match (&single_filter.additional_rules, &candidate_props[..]) {
+ (None, [_, ..]) | (Some(cal::PropFilterRules::IsNotDefined), []) => true,
+ (None, []) | (Some(cal::PropFilterRules::IsNotDefined), [_, ..]) => false,
+ (Some(cal::PropFilterRules::Match(pattern)), multi_props) => {
+ multi_props.iter().any(|prop| {
+ // check value
+ match &pattern.time_or_text {
+ Some(cal::TimeOrText::Time(time_range)) => {
+ let maybe_parsed_date = parser::date_time(prop.val.as_str());
+
+ let parsed_date = match maybe_parsed_date {
+ None => return false,
+ Some(v) => v,
+ };
+
+ // see if entry is in range
+ let is_in_range = match time_range {
+ cal::TimeRange::OnlyStart(after) => &parsed_date >= after,
+ cal::TimeRange::OnlyEnd(before) => &parsed_date <= before,
+ cal::TimeRange::FullRange(after, before) => {
+ &parsed_date >= after && &parsed_date <= before
+ }
+ };
+ if !is_in_range {
+ return false;
+ }
+
+ // if you are here, this subcondition is valid
+ }
+ Some(cal::TimeOrText::Text(txt_match)) => {
+ //@FIXME ignoring collation
+ let is_match = match txt_match.negate_condition {
+ None | Some(false) => {
+ prop.val.as_str().contains(txt_match.text.as_str())
+ }
+ Some(true) => !prop.val.as_str().contains(txt_match.text.as_str()),
+ };
+ if !is_match {
+ return false;
+ }
+ }
+ None => (), // if not filter on value is set, continue
+ };
+
+ // check parameters
+ pattern.param_filter.iter().all(|single_param_filter| {
+ let multi_param = prop
+ .params
+ .iter()
+ .filter(|candidate| {
+ candidate.key.as_str() == single_param_filter.name.as_str()
+ })
+ .collect::<Vec<_>>();
+
+ match (&multi_param[..], &single_param_filter.additional_rules) {
+ ([.., _], None) => true,
+ ([], None) => false,
+ ([.., _], Some(cal::ParamFilterMatch::IsNotDefined)) => false,
+ ([], Some(cal::ParamFilterMatch::IsNotDefined)) => true,
+ (many_params, Some(cal::ParamFilterMatch::Match(txt_match))) => {
+ many_params.iter().any(|param| {
+ let param_val = match &param.val {
+ Some(v) => v,
+ None => return false,
+ };
+
+ match txt_match.negate_condition {
+ None | Some(false) => {
+ param_val.as_str().contains(txt_match.text.as_str())
+ }
+ Some(true) => {
+ !param_val.as_str().contains(txt_match.text.as_str())
+ }
+ }
+ })
+ }
+ }
+ })
+ })
+ }
+ }
+ })
+}
+
+fn resolve_trigger(
+ parent: &icalendar::parser::Component,
+ properties: &[icalendar::parser::Property],
+) -> Option<chrono::DateTime<chrono::Utc>> {
+ // A. Do we have a TRIGGER property? If not, returns early
+ let maybe_trigger_prop = properties
+ .iter()
+ .find(|candidate| candidate.name.as_str() == "TRIGGER");
+
+ let trigger_prop = match maybe_trigger_prop {
+ None => return None,
+ Some(v) => v,
+ };
+
+ // B.1 Is it an absolute datetime? If so, returns early
+ let maybe_absolute = trigger_prop
+ .params
+ .iter()
+ .find(|param| param.key.as_str() == "VALUE")
+ .map(|param| param.val.as_ref())
+ .flatten()
+ .map(|v| v.as_str() == "DATE-TIME");
+
+ if maybe_absolute.is_some() {
+ let final_date = prop_date(properties, "TRIGGER");
+ tracing::trace!(trigger=?final_date, "resolved absolute trigger");
+ return final_date;
+ }
+
+ // B.2 Otherwise it's a timedelta relative to a parent field.
+ // C.1 Parse the timedelta value, returns early if invalid
+ let (_, time_delta) = parser::dur_value(trigger_prop.val.as_str()).ok()?;
+
+ // C.2 Get the parent reference absolute datetime, returns early if invalid
+ let maybe_bound = trigger_prop
+ .params
+ .iter()
+ .find(|param| param.key.as_str() == "RELATED")
+ .map(|param| param.val.as_ref())
+ .flatten();
+
+ // If the trigger is set relative to START, then the "DTSTART" property MUST be present in the associated
+ // "VEVENT" or "VTODO" calendar component.
+ //
+ // If an alarm is specified for an event with the trigger set relative to the END,
+ // then the "DTEND" property or the "DTSTART" and "DURATION " properties MUST be present
+ // in the associated "VEVENT" calendar component.
+ //
+ // If the alarm is specified for a to-do with a trigger set relative to the END,
+ // then either the "DUE" property or the "DTSTART" and "DURATION " properties
+ // MUST be present in the associated "VTODO" calendar component.
+ let related_field = match maybe_bound.as_ref().map(|v| v.as_str()) {
+ Some("START") => "DTSTART",
+ Some("END") => "DTEND", //@FIXME must add support for DUE, DTSTART, and DURATION
+ _ => "DTSTART", // by default use DTSTART
+ };
+ let parent_date = match prop_date(parent.properties.as_ref(), related_field) {
+ Some(v) => v,
+ _ => return None,
+ };
+
+ // C.3 Compute the final date from the base date + timedelta
+ let final_date = parent_date + time_delta;
+ tracing::trace!(trigger=?final_date, "resolved relative trigger");
+ Some(final_date)
+}
+
+fn is_in_time_range(
+ component: &cal::Component,
+ parent: &icalendar::parser::Component,
+ properties: &[icalendar::parser::Property],
+ time_range: &cal::TimeRange,
+) -> bool {
+ //@FIXME timezones are not properly handled currently (everything is UTC)
+ //@FIXME does not support repeat
+ //ref: https://datatracker.ietf.org/doc/html/rfc4791#section-9.9
+ let (start, end) = match time_range {
+ cal::TimeRange::OnlyStart(start) => (start, &chrono::DateTime::<chrono::Utc>::MAX_UTC),
+ cal::TimeRange::OnlyEnd(end) => (&chrono::DateTime::<chrono::Utc>::MIN_UTC, end),
+ cal::TimeRange::FullRange(start, end) => (start, end),
+ };
+
+ match component {
+ cal::Component::VEvent => {
+ let dtstart = match prop_date(properties, "DTSTART") {
+ Some(v) => v,
+ _ => return false,
+ };
+ let maybe_dtend = prop_date(properties, "DTEND");
+ let maybe_duration = prop_parse::<i64>(properties, "DURATION")
+ .map(|d| chrono::TimeDelta::new(std::cmp::max(d, 0), 0))
+ .flatten();
+
+ //@FIXME missing "date" management (only support "datetime")
+ match (&maybe_dtend, &maybe_duration) {
+ // | Y | N | N | * | (start < DTEND AND end > DTSTART) |
+ (Some(dtend), _) => start < dtend && end > &dtstart,
+ // | N | Y | Y | * | (start < DTSTART+DURATION AND end > DTSTART) |
+ (_, Some(duration)) => *start <= dtstart + *duration && end > &dtstart,
+ // | N | N | N | Y | (start <= DTSTART AND end > DTSTART) |
+ _ => start <= &dtstart && end > &dtstart,
+ }
+ }
+ cal::Component::VTodo => {
+ let maybe_dtstart = prop_date(properties, "DTSTART");
+ let maybe_due = prop_date(properties, "DUE");
+ let maybe_completed = prop_date(properties, "COMPLETED");
+ let maybe_created = prop_date(properties, "CREATED");
+ let maybe_duration = prop_parse::<i64>(properties, "DURATION")
+ .map(|d| chrono::TimeDelta::new(d, 0))
+ .flatten();
+
+ match (
+ maybe_dtstart,
+ maybe_duration,
+ maybe_due,
+ maybe_completed,
+ maybe_created,
+ ) {
+ // | Y | Y | N | * | * | (start <= DTSTART+DURATION) AND |
+ // | | | | | | ((end > DTSTART) OR |
+ // | | | | | | (end >= DTSTART+DURATION)) |
+ (Some(dtstart), Some(duration), None, _, _) => {
+ *start <= dtstart + duration && (*end > dtstart || *end >= dtstart + duration)
+ }
+ // | Y | N | Y | * | * | ((start < DUE) OR (start <= DTSTART)) |
+ // | | | | | | AND |
+ // | | | | | | ((end > DTSTART) OR (end >= DUE)) |
+ (Some(dtstart), None, Some(due), _, _) => {
+ (*start < due || *start <= dtstart) && (*end > dtstart || *end >= due)
+ }
+ // | Y | N | N | * | * | (start <= DTSTART) AND (end > DTSTART) |
+ (Some(dtstart), None, None, _, _) => *start <= dtstart && *end > dtstart,
+ // | N | N | Y | * | * | (start < DUE) AND (end >= DUE) |
+ (None, None, Some(due), _, _) => *start < due && *end >= due,
+ // | N | N | N | Y | Y | ((start <= CREATED) OR (start <= COMPLETED))|
+ // | | | | | | AND |
+ // | | | | | | ((end >= CREATED) OR (end >= COMPLETED))|
+ (None, None, None, Some(completed), Some(created)) => {
+ (*start <= created || *start <= completed)
+ && (*end >= created || *end >= completed)
+ }
+ // | N | N | N | Y | N | (start <= COMPLETED) AND (end >= COMPLETED) |
+ (None, None, None, Some(completed), None) => {
+ *start <= completed && *end >= completed
+ }
+ // | N | N | N | N | Y | (end > CREATED) |
+ (None, None, None, None, Some(created)) => *end > created,
+ // | N | N | N | N | N | TRUE |
+ _ => true,
+ }
+ }
+ cal::Component::VJournal => {
+ let maybe_dtstart = prop_date(properties, "DTSTART");
+ match maybe_dtstart {
+ // | Y | Y | (start <= DTSTART) AND (end > DTSTART) |
+ Some(dtstart) => *start <= dtstart && *end > dtstart,
+ // | N | * | FALSE |
+ None => false,
+ }
+ }
+ cal::Component::VFreeBusy => {
+ //@FIXME freebusy is not supported yet
+ false
+ }
+ cal::Component::VAlarm => {
+ //@FIXME does not support REPEAT
+ let maybe_trigger = resolve_trigger(parent, properties);
+ match maybe_trigger {
+ // (start <= trigger-time) AND (end > trigger-time)
+ Some(trigger_time) => *start <= trigger_time && *end > trigger_time,
+ _ => false,
+ }
+ }
+ _ => false,
+ }
+}
diff --git a/aero-proto/Cargo.toml b/aero-proto/Cargo.toml
new file mode 100644
index 0000000..e8d6b8f
--- /dev/null
+++ b/aero-proto/Cargo.toml
@@ -0,0 +1,39 @@
+[package]
+name = "aero-proto"
+version = "0.3.0"
+authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
+edition = "2021"
+license = "EUPL-1.2"
+description = "Binding between Aerogramme's internal components and well-known protocols"
+
+[dependencies]
+aero-ical.workspace = true
+aero-sasl.workspace = true
+aero-dav.workspace = true
+aero-user.workspace = true
+aero-collections.workspace = true
+
+async-trait.workspace = true
+anyhow.workspace = true
+hyper.workspace = true
+base64.workspace = true
+hyper-util.workspace = true
+http-body-util.workspace = true
+futures.workspace = true
+tokio.workspace = true
+tokio-util.workspace = true
+tokio-rustls.workspace = true
+tokio-stream.workspace = true
+rustls.workspace = true
+rustls-pemfile.workspace = true
+imap-codec.workspace = true
+imap-flow.workspace = true
+chrono.workspace = true
+eml-codec.workspace = true
+thiserror.workspace = true
+duplexify.workspace = true
+smtp-message.workspace = true
+smtp-server.workspace = true
+tracing.workspace = true
+quick-xml.workspace = true
+icalendar.workspace = true
diff --git a/aero-proto/src/dav/codec.rs b/aero-proto/src/dav/codec.rs
new file mode 100644
index 0000000..a441e7e
--- /dev/null
+++ b/aero-proto/src/dav/codec.rs
@@ -0,0 +1,135 @@
+use anyhow::{bail, Result};
+use futures::sink::SinkExt;
+use futures::stream::StreamExt;
+use futures::stream::TryStreamExt;
+use http_body_util::combinators::UnsyncBoxBody;
+use http_body_util::BodyExt;
+use http_body_util::BodyStream;
+use http_body_util::Full;
+use http_body_util::StreamBody;
+use hyper::body::Frame;
+use hyper::body::Incoming;
+use hyper::{body::Bytes, Request, Response};
+use std::io::{Error, ErrorKind};
+use tokio_util::io::{CopyToBytes, SinkWriter};
+use tokio_util::sync::PollSender;
+
+use super::controller::HttpResponse;
+use super::node::PutPolicy;
+use aero_dav::types as dav;
+use aero_dav::xml as dxml;
+
+pub(crate) fn depth(req: &Request<impl hyper::body::Body>) -> dav::Depth {
+ match req
+ .headers()
+ .get("Depth")
+ .map(hyper::header::HeaderValue::to_str)
+ {
+ Some(Ok("0")) => dav::Depth::Zero,
+ Some(Ok("1")) => dav::Depth::One,
+ Some(Ok("Infinity")) => dav::Depth::Infinity,
+ _ => dav::Depth::Zero,
+ }
+}
+
+pub(crate) fn put_policy(req: &Request<impl hyper::body::Body>) -> Result<PutPolicy> {
+ if let Some(maybe_txt_etag) = req
+ .headers()
+ .get("If-Match")
+ .map(hyper::header::HeaderValue::to_str)
+ {
+ let etag = maybe_txt_etag?;
+ let dquote_count = etag.chars().filter(|c| *c == '"').count();
+ if dquote_count != 2 {
+ bail!("Either If-Match value is invalid or it's not supported (only single etag is supported)");
+ }
+
+ return Ok(PutPolicy::ReplaceEtag(etag.into()));
+ }
+
+ if let Some(maybe_txt_etag) = req
+ .headers()
+ .get("If-None-Match")
+ .map(hyper::header::HeaderValue::to_str)
+ {
+ let etag = maybe_txt_etag?;
+ if etag == "*" {
+ return Ok(PutPolicy::CreateOnly);
+ }
+ bail!("Either If-None-Match value is invalid or it's not supported (only asterisk is supported)")
+ }
+
+ Ok(PutPolicy::OverwriteAll)
+}
+
+pub(crate) fn text_body(txt: &'static str) -> UnsyncBoxBody<Bytes, std::io::Error> {
+ UnsyncBoxBody::new(Full::new(Bytes::from(txt)).map_err(|e| match e {}))
+}
+
+pub(crate) fn serialize<T: dxml::QWrite + Send + 'static>(
+ status_ok: hyper::StatusCode,
+ elem: T,
+) -> Result<HttpResponse> {
+ let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(1);
+
+ // Build the writer
+ tokio::task::spawn(async move {
+ let sink = PollSender::new(tx).sink_map_err(|_| Error::from(ErrorKind::BrokenPipe));
+ let mut writer = SinkWriter::new(CopyToBytes::new(sink));
+ let q = quick_xml::writer::Writer::new_with_indent(&mut writer, b' ', 4);
+ let ns_to_apply = vec![
+ ("xmlns:D".into(), "DAV:".into()),
+ ("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()),
+ ];
+ let mut qwriter = dxml::Writer { q, ns_to_apply };
+ let decl =
+ quick_xml::events::BytesDecl::from_start(quick_xml::events::BytesStart::from_content(
+ "xml version=\"1.0\" encoding=\"utf-8\"",
+ 0,
+ ));
+ match qwriter
+ .q
+ .write_event_async(quick_xml::events::Event::Decl(decl))
+ .await
+ {
+ Ok(_) => (),
+ Err(e) => tracing::error!(err=?e, "unable to write XML declaration <?xml ... >"),
+ }
+ match elem.qwrite(&mut qwriter).await {
+ Ok(_) => tracing::debug!("fully serialized object"),
+ Err(e) => tracing::error!(err=?e, "failed to serialize object"),
+ }
+ });
+
+ // Build the reader
+ let recv = tokio_stream::wrappers::ReceiverStream::new(rx);
+ let stream = StreamBody::new(recv.map(|v| Ok(Frame::data(v))));
+ let boxed_body = UnsyncBoxBody::new(stream);
+
+ let response = Response::builder()
+ .status(status_ok)
+ .header("content-type", "application/xml; charset=\"utf-8\"")
+ .body(boxed_body)?;
+
+ Ok(response)
+}
+
+/// Deserialize a request body to an XML request
+pub(crate) async fn deserialize<T: dxml::Node<T>>(req: Request<Incoming>) -> Result<T> {
+ let stream_of_frames = BodyStream::new(req.into_body());
+ let stream_of_bytes = stream_of_frames
+ .map_ok(|frame| frame.into_data())
+ .map(|obj| match obj {
+ Ok(Ok(v)) => Ok(v),
+ Ok(Err(_)) => Err(std::io::Error::new(
+ std::io::ErrorKind::Other,
+ "conversion error",
+ )),
+ Err(err) => Err(std::io::Error::new(std::io::ErrorKind::Other, err)),
+ });
+ let async_read = tokio_util::io::StreamReader::new(stream_of_bytes);
+ let async_read = std::pin::pin!(async_read);
+ let mut rdr = dxml::Reader::new(quick_xml::reader::NsReader::from_reader(async_read)).await?;
+ let parsed = rdr.find::<T>().await?;
+ Ok(parsed)
+}
diff --git a/aero-proto/src/dav/controller.rs b/aero-proto/src/dav/controller.rs
new file mode 100644
index 0000000..8c53c6b
--- /dev/null
+++ b/aero-proto/src/dav/controller.rs
@@ -0,0 +1,436 @@
+use anyhow::Result;
+use futures::stream::{StreamExt, TryStreamExt};
+use http_body_util::combinators::UnsyncBoxBody;
+use http_body_util::BodyStream;
+use http_body_util::StreamBody;
+use hyper::body::Frame;
+use hyper::body::Incoming;
+use hyper::{body::Bytes, Request, Response};
+
+use aero_collections::{davdag::Token, user::User};
+use aero_dav::caltypes as cal;
+use aero_dav::realization::{self, All};
+use aero_dav::synctypes as sync;
+use aero_dav::types as dav;
+use aero_dav::versioningtypes as vers;
+use aero_ical::query::is_component_match;
+
+use crate::dav::codec;
+use crate::dav::codec::{depth, deserialize, serialize, text_body};
+use crate::dav::node::DavNode;
+use crate::dav::resource::{RootNode, BASE_TOKEN_URI};
+
+pub(super) type ArcUser = std::sync::Arc<User>;
+pub(super) type HttpResponse = Response<UnsyncBoxBody<Bytes, std::io::Error>>;
+
+const ALLPROP: [dav::PropertyRequest<All>; 10] = [
+ dav::PropertyRequest::CreationDate,
+ dav::PropertyRequest::DisplayName,
+ dav::PropertyRequest::GetContentLanguage,
+ dav::PropertyRequest::GetContentLength,
+ dav::PropertyRequest::GetContentType,
+ dav::PropertyRequest::GetEtag,
+ dav::PropertyRequest::GetLastModified,
+ dav::PropertyRequest::LockDiscovery,
+ dav::PropertyRequest::ResourceType,
+ dav::PropertyRequest::SupportedLock,
+];
+
+pub(crate) struct Controller {
+ node: Box<dyn DavNode>,
+ user: std::sync::Arc<User>,
+ req: Request<Incoming>,
+}
+impl Controller {
+ pub(crate) async fn route(
+ user: std::sync::Arc<User>,
+ req: Request<Incoming>,
+ ) -> Result<HttpResponse> {
+ let path = req.uri().path().to_string();
+ let path_segments: Vec<_> = path.split("/").filter(|s| *s != "").collect();
+ let method = req.method().as_str().to_uppercase();
+
+ let can_create = matches!(method.as_str(), "PUT" | "MKCOL" | "MKCALENDAR");
+ let node = match (RootNode {}).fetch(&user, &path_segments, can_create).await {
+ Ok(v) => v,
+ Err(e) => {
+ tracing::warn!(err=?e, "dav node fetch failed");
+ return Ok(Response::builder()
+ .status(404)
+ .body(codec::text_body("Resource not found"))?);
+ }
+ };
+
+ let dav_hdrs = node.dav_header();
+ let ctrl = Self { node, user, req };
+
+ match method.as_str() {
+ "OPTIONS" => Ok(Response::builder()
+ .status(200)
+ .header("DAV", dav_hdrs)
+ .header("Allow", "HEAD,GET,PUT,OPTIONS,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK,MKCALENDAR,REPORT")
+ .body(codec::text_body(""))?),
+ "HEAD" => {
+ tracing::warn!("HEAD might not correctly implemented: should return ETags & co");
+ Ok(Response::builder()
+ .status(200)
+ .body(codec::text_body(""))?)
+ },
+ "GET" => ctrl.get().await,
+ "PUT" => ctrl.put().await,
+ "DELETE" => ctrl.delete().await,
+ "PROPFIND" => ctrl.propfind().await,
+ "REPORT" => ctrl.report().await,
+ _ => Ok(Response::builder()
+ .status(501)
+ .body(codec::text_body("HTTP Method not implemented"))?),
+ }
+ }
+
+ // --- Per-method functions ---
+
+ /// REPORT has been first described in the "Versioning Extension" of WebDAV
+ /// It allows more complex queries compared to PROPFIND
+ ///
+ /// Note: current implementation is not generic at all, it is heavily tied to CalDAV.
+ /// A rewrite would be required to make it more generic (with the extension system that has
+ /// been introduced in aero-dav)
+ async fn report(self) -> Result<HttpResponse> {
+ let status = hyper::StatusCode::from_u16(207)?;
+
+ let cal_report = match deserialize::<vers::Report<All>>(self.req).await {
+ Ok(v) => v,
+ Err(e) => {
+ tracing::error!(err=?e, "unable to decode REPORT body");
+ return Ok(Response::builder()
+ .status(400)
+ .body(text_body("Bad request"))?);
+ }
+ };
+
+ // Internal representation that will handle processed request
+ let (mut ok_node, mut not_found) = (Vec::new(), Vec::new());
+ let calprop: Option<cal::CalendarSelector<All>>;
+ let extension: Option<realization::Multistatus>;
+
+ // Extracting request information
+ match cal_report {
+ vers::Report::Extension(realization::ReportType::Cal(cal::ReportType::Multiget(m))) => {
+ // Multiget is really like a propfind where Depth: 0|1|Infinity is replaced by an arbitrary
+ // list of URLs
+ // Getting the list of nodes
+ for h in m.href.into_iter() {
+ let maybe_collected_node = match Path::new(h.0.as_str()) {
+ Ok(Path::Abs(p)) => RootNode {}
+ .fetch(&self.user, p.as_slice(), false)
+ .await
+ .or(Err(h)),
+ Ok(Path::Rel(p)) => self
+ .node
+ .fetch(&self.user, p.as_slice(), false)
+ .await
+ .or(Err(h)),
+ Err(_) => Err(h),
+ };
+
+ match maybe_collected_node {
+ Ok(v) => ok_node.push(v),
+ Err(h) => not_found.push(h),
+ };
+ }
+ calprop = m.selector;
+ extension = None;
+ }
+ vers::Report::Extension(realization::ReportType::Cal(cal::ReportType::Query(q))) => {
+ calprop = q.selector;
+ extension = None;
+ ok_node = apply_filter(self.node.children(&self.user).await, &q.filter)
+ .try_collect()
+ .await?;
+ }
+ vers::Report::Extension(realization::ReportType::Sync(sync_col)) => {
+ calprop = Some(cal::CalendarSelector::Prop(sync_col.prop));
+
+ if sync_col.limit.is_some() {
+ tracing::warn!("limit is not supported, ignoring");
+ }
+ if matches!(sync_col.sync_level, sync::SyncLevel::Infinite) {
+ tracing::debug!("aerogramme calendar collections are not nested");
+ }
+
+ let token = match sync_col.sync_token {
+ sync::SyncTokenRequest::InitialSync => None,
+ sync::SyncTokenRequest::IncrementalSync(token_raw) => {
+ // parse token
+ if token_raw.len() != BASE_TOKEN_URI.len() + 48 {
+ anyhow::bail!("invalid token length")
+ }
+ let token = token_raw[BASE_TOKEN_URI.len()..]
+ .parse()
+ .or(Err(anyhow::anyhow!("can't parse token")))?;
+ Some(token)
+ }
+ };
+ // do the diff
+ let new_token: Token;
+ (new_token, ok_node, not_found) = match self.node.diff(token).await {
+ Ok(t) => t,
+ Err(e) => match e.kind() {
+ std::io::ErrorKind::NotFound => return Ok(Response::builder()
+ .status(410)
+ .body(text_body("Diff failed, token might be expired"))?),
+ _ => return Ok(Response::builder()
+ .status(500)
+ .body(text_body("Server error, maybe this operation is not supported on this collection"))?),
+ },
+ };
+ extension = Some(realization::Multistatus::Sync(sync::Multistatus {
+ sync_token: sync::SyncToken(format!("{}{}", BASE_TOKEN_URI, new_token)),
+ }));
+ }
+ _ => {
+ return Ok(Response::builder()
+ .status(501)
+ .body(text_body("Not implemented"))?)
+ }
+ };
+
+ // Getting props
+ let props = match calprop {
+ None | Some(cal::CalendarSelector::AllProp) => Some(dav::PropName(ALLPROP.to_vec())),
+ Some(cal::CalendarSelector::PropName) => None,
+ Some(cal::CalendarSelector::Prop(inner)) => Some(inner),
+ };
+
+ serialize(
+ status,
+ Self::multistatus(&self.user, ok_node, not_found, props, extension).await,
+ )
+ }
+
+ /// PROPFIND is the standard way to fetch WebDAV properties
+ async fn propfind(self) -> Result<HttpResponse> {
+ let depth = depth(&self.req);
+ if matches!(depth, dav::Depth::Infinity) {
+ return Ok(Response::builder()
+ .status(501)
+ .body(text_body("Depth: Infinity not implemented"))?);
+ }
+
+ let status = hyper::StatusCode::from_u16(207)?;
+
+ // A client may choose not to submit a request body. An empty PROPFIND
+ // request body MUST be treated as if it were an 'allprop' request.
+ // @FIXME here we handle any invalid data as an allprop, an empty request is thus correctly
+ // handled, but corrupted requests are also silently handled as allprop.
+ let propfind = deserialize::<dav::PropFind<All>>(self.req)
+ .await
+ .unwrap_or_else(|_| dav::PropFind::<All>::AllProp(None));
+ tracing::debug!(recv=?propfind, "inferred propfind request");
+
+ // Collect nodes as PROPFIND is not limited to the targeted node
+ let mut nodes = vec![];
+ if matches!(depth, dav::Depth::One | dav::Depth::Infinity) {
+ nodes.extend(self.node.children(&self.user).await);
+ }
+ nodes.push(self.node);
+
+ // Expand properties request
+ let propname = match propfind {
+ dav::PropFind::PropName => None,
+ dav::PropFind::AllProp(None) => Some(dav::PropName(ALLPROP.to_vec())),
+ dav::PropFind::AllProp(Some(dav::Include(mut include))) => {
+ include.extend_from_slice(&ALLPROP);
+ Some(dav::PropName(include))
+ }
+ dav::PropFind::Prop(inner) => Some(inner),
+ };
+
+ // Not Found is currently impossible considering the way we designed this function
+ let not_found = vec![];
+ serialize(
+ status,
+ Self::multistatus(&self.user, nodes, not_found, propname, None).await,
+ )
+ }
+
+ async fn put(self) -> Result<HttpResponse> {
+ let put_policy = codec::put_policy(&self.req)?;
+
+ let stream_of_frames = BodyStream::new(self.req.into_body());
+ let stream_of_bytes = stream_of_frames
+ .map_ok(|frame| frame.into_data())
+ .map(|obj| match obj {
+ Ok(Ok(v)) => Ok(v),
+ Ok(Err(_)) => Err(std::io::Error::new(
+ std::io::ErrorKind::Other,
+ "conversion error",
+ )),
+ Err(err) => Err(std::io::Error::new(std::io::ErrorKind::Other, err)),
+ })
+ .boxed();
+
+ let etag = match self.node.put(put_policy, stream_of_bytes).await {
+ Ok(etag) => etag,
+ Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
+ tracing::warn!("put pre-condition failed");
+ let response = Response::builder().status(412).body(text_body(""))?;
+ return Ok(response);
+ }
+ Err(e) => Err(e)?,
+ };
+
+ let response = Response::builder()
+ .status(201)
+ .header("ETag", etag)
+ //.header("content-type", "application/xml; charset=\"utf-8\"")
+ .body(text_body(""))?;
+
+ Ok(response)
+ }
+
+ async fn get(self) -> Result<HttpResponse> {
+ let stream_body = StreamBody::new(self.node.content().map_ok(|v| Frame::data(v)));
+ let boxed_body = UnsyncBoxBody::new(stream_body);
+
+ let mut builder = Response::builder().status(200);
+ builder = builder.header("content-type", self.node.content_type());
+ if let Some(etag) = self.node.etag().await {
+ builder = builder.header("etag", etag);
+ }
+ let response = builder.body(boxed_body)?;
+
+ Ok(response)
+ }
+
+ async fn delete(self) -> Result<HttpResponse> {
+ self.node.delete().await?;
+ let response = Response::builder()
+ .status(204)
+ //.header("content-type", "application/xml; charset=\"utf-8\"")
+ .body(text_body(""))?;
+ Ok(response)
+ }
+
+ // --- Common utility functions ---
+ /// Build a multistatus response from a list of DavNodes
+ async fn multistatus(
+ user: &ArcUser,
+ nodes: Vec<Box<dyn DavNode>>,
+ not_found: Vec<dav::Href>,
+ props: Option<dav::PropName<All>>,
+ extension: Option<realization::Multistatus>,
+ ) -> dav::Multistatus<All> {
+ // Collect properties on existing objects
+ let mut responses: Vec<dav::Response<All>> = match props {
+ Some(props) => {
+ futures::stream::iter(nodes)
+ .then(|n| n.response_props(user, props.clone()))
+ .collect()
+ .await
+ }
+ None => nodes
+ .into_iter()
+ .map(|n| n.response_propname(user))
+ .collect(),
+ };
+
+ // Register not found objects only if relevant
+ if !not_found.is_empty() {
+ responses.push(dav::Response {
+ status_or_propstat: dav::StatusOrPropstat::Status(
+ not_found,
+ dav::Status(hyper::StatusCode::NOT_FOUND),
+ ),
+ error: None,
+ location: None,
+ responsedescription: None,
+ });
+ }
+
+ // Build response
+ let multistatus = dav::Multistatus::<All> {
+ responses,
+ responsedescription: None,
+ extension,
+ };
+
+ tracing::debug!(multistatus=?multistatus, "multistatus response");
+ multistatus
+ }
+}
+
+/// Path is a voluntarily feature limited
+/// compared to the expressiveness of a UNIX path
+/// For example getting parent with ../ is not supported, scheme is not supported, etc.
+/// More complex support could be added later if needed by clients
+enum Path<'a> {
+ Abs(Vec<&'a str>),
+ Rel(Vec<&'a str>),
+}
+impl<'a> Path<'a> {
+ fn new(path: &'a str) -> Result<Self> {
+ // This check is naive, it does not aim at detecting all fully qualified
+ // URL or protect from any attack, its only goal is to help debugging.
+ if path.starts_with("http://") || path.starts_with("https://") {
+ anyhow::bail!("Full URL are not supported")
+ }
+
+ let path_segments: Vec<_> = path.split("/").filter(|s| *s != "" && *s != ".").collect();
+ if path.starts_with("/") {
+ return Ok(Path::Abs(path_segments));
+ }
+ Ok(Path::Rel(path_segments))
+ }
+}
+
+//@FIXME naive implementation, must be refactored later
+use futures::stream::Stream;
+fn apply_filter<'a>(
+ nodes: Vec<Box<dyn DavNode>>,
+ filter: &'a cal::Filter,
+) -> impl Stream<Item = std::result::Result<Box<dyn DavNode>, std::io::Error>> + 'a {
+ futures::stream::iter(nodes).filter_map(move |single_node| async move {
+ // Get ICS
+ let chunks: Vec<_> = match single_node.content().try_collect().await {
+ Ok(v) => v,
+ Err(e) => return Some(Err(e)),
+ };
+ let raw_ics = chunks.iter().fold(String::new(), |mut acc, single_chunk| {
+ let str_fragment = std::str::from_utf8(single_chunk.as_ref());
+ acc.extend(str_fragment);
+ acc
+ });
+
+ // Parse ICS
+ let ics = match icalendar::parser::read_calendar(&raw_ics) {
+ Ok(v) => v,
+ Err(e) => {
+ tracing::warn!(err=?e, "Unable to parse ICS in calendar-query");
+ return Some(Err(std::io::Error::from(std::io::ErrorKind::InvalidData)));
+ }
+ };
+
+ // Do checks
+ // @FIXME: icalendar does not consider VCALENDAR as a component
+ // but WebDAV does...
+ // Build a fake VCALENDAR component for icalendar compatibility, it's a hack
+ let root_filter = &filter.0;
+ let fake_vcal_component = icalendar::parser::Component {
+ name: cal::Component::VCalendar.as_str().into(),
+ properties: ics.properties,
+ components: ics.components,
+ };
+ tracing::debug!(filter=?root_filter, "calendar-query filter");
+
+ // Adjust return value according to filter
+ match is_component_match(
+ &fake_vcal_component,
+ &[fake_vcal_component.clone()],
+ root_filter,
+ ) {
+ true => Some(Ok(single_node)),
+ _ => None,
+ }
+ })
+}
diff --git a/aero-proto/src/dav/middleware.rs b/aero-proto/src/dav/middleware.rs
new file mode 100644
index 0000000..8964699
--- /dev/null
+++ b/aero-proto/src/dav/middleware.rs
@@ -0,0 +1,70 @@
+use anyhow::{anyhow, Result};
+use base64::Engine;
+use hyper::body::Incoming;
+use hyper::{Request, Response};
+
+use aero_collections::user::User;
+use aero_user::login::ArcLoginProvider;
+
+use super::codec::text_body;
+use super::controller::HttpResponse;
+
+type ArcUser = std::sync::Arc<User>;
+
+pub(super) async fn auth<'a>(
+ login: ArcLoginProvider,
+ req: Request<Incoming>,
+ next: impl Fn(ArcUser, Request<Incoming>) -> futures::future::BoxFuture<'a, Result<HttpResponse>>,
+) -> Result<HttpResponse> {
+ let auth_val = match req.headers().get(hyper::header::AUTHORIZATION) {
+ Some(hv) => hv.to_str()?,
+ None => {
+ tracing::info!("Missing authorization field");
+ return Ok(Response::builder()
+ .status(401)
+ .header("WWW-Authenticate", "Basic realm=\"Aerogramme\"")
+ .body(text_body("Missing Authorization field"))?);
+ }
+ };
+
+ let b64_creds_maybe_padded = match auth_val.split_once(" ") {
+ Some(("Basic", b64)) => b64,
+ _ => {
+ tracing::info!("Unsupported authorization field");
+ return Ok(Response::builder()
+ .status(400)
+ .body(text_body("Unsupported Authorization field"))?);
+ }
+ };
+
+ // base64urlencoded may have trailing equals, base64urlsafe has not
+ // theoretically authorization is padded but "be liberal in what you accept"
+ let b64_creds_clean = b64_creds_maybe_padded.trim_end_matches('=');
+
+ // Decode base64
+ let creds = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64_creds_clean)?;
+ let str_creds = std::str::from_utf8(&creds)?;
+
+ // Split username and password
+ let (username, password) = str_creds.split_once(':').ok_or(anyhow!(
+ "Missing colon in Authorization, can't split decoded value into a username/password pair"
+ ))?;
+
+ // Call login provider
+ let creds = match login.login(username, password).await {
+ Ok(c) => c,
+ Err(_) => {
+ tracing::info!(user = username, "Wrong credentials");
+ return Ok(Response::builder()
+ .status(401)
+ .header("WWW-Authenticate", "Basic realm=\"Aerogramme\"")
+ .body(text_body("Wrong credentials"))?);
+ }
+ };
+
+ // Build a user
+ let user = User::new(username.into(), creds).await?;
+
+ // Call router with user
+ next(user, req).await
+}
diff --git a/aero-proto/src/dav/mod.rs b/aero-proto/src/dav/mod.rs
new file mode 100644
index 0000000..a3dd58d
--- /dev/null
+++ b/aero-proto/src/dav/mod.rs
@@ -0,0 +1,195 @@
+mod codec;
+mod controller;
+mod middleware;
+mod node;
+mod resource;
+
+use std::net::SocketAddr;
+use std::sync::Arc;
+
+use anyhow::Result;
+use futures::future::FutureExt;
+use futures::stream::{FuturesUnordered, StreamExt};
+use hyper::rt::{Read, Write};
+use hyper::server::conn::http1 as http;
+use hyper::service::service_fn;
+use hyper::{Request, Response};
+use hyper_util::rt::TokioIo;
+use rustls_pemfile::{certs, private_key};
+use tokio::io::{AsyncRead, AsyncWrite};
+use tokio::net::TcpListener;
+use tokio::net::TcpStream;
+use tokio::sync::watch;
+use tokio_rustls::TlsAcceptor;
+
+use aero_user::config::{DavConfig, DavUnsecureConfig};
+use aero_user::login::ArcLoginProvider;
+
+use crate::dav::controller::Controller;
+
+pub struct Server {
+ bind_addr: SocketAddr,
+ login_provider: ArcLoginProvider,
+ tls: Option<TlsAcceptor>,
+}
+
+pub fn new_unsecure(config: DavUnsecureConfig, login: ArcLoginProvider) -> Server {
+ Server {
+ bind_addr: config.bind_addr,
+ login_provider: login,
+ tls: None,
+ }
+}
+
+pub fn new(config: DavConfig, login: ArcLoginProvider) -> Result<Server> {
+ let loaded_certs = certs(&mut std::io::BufReader::new(std::fs::File::open(
+ config.certs,
+ )?))
+ .collect::<Result<Vec<_>, _>>()?;
+ let loaded_key = private_key(&mut std::io::BufReader::new(std::fs::File::open(
+ config.key,
+ )?))?
+ .unwrap();
+
+ let tls_config = rustls::ServerConfig::builder()
+ .with_no_client_auth()
+ .with_single_cert(loaded_certs, loaded_key)?;
+ let acceptor = TlsAcceptor::from(Arc::new(tls_config));
+
+ Ok(Server {
+ bind_addr: config.bind_addr,
+ login_provider: login,
+ tls: Some(acceptor),
+ })
+}
+
+trait Stream: Read + Write + Send + Unpin {}
+impl<T: Unpin + AsyncRead + AsyncWrite + Send> Stream for TokioIo<T> {}
+
+impl Server {
+ pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
+ let tcp = TcpListener::bind(self.bind_addr).await?;
+ tracing::info!("DAV server listening on {:#}", self.bind_addr);
+
+ let mut connections = FuturesUnordered::new();
+ while !*must_exit.borrow() {
+ let wait_conn_finished = async {
+ if connections.is_empty() {
+ futures::future::pending().await
+ } else {
+ connections.next().await
+ }
+ };
+ let (socket, remote_addr) = tokio::select! {
+ a = tcp.accept() => a?,
+ _ = wait_conn_finished => continue,
+ _ = must_exit.changed() => continue,
+ };
+ tracing::info!("Accepted connection from {}", remote_addr);
+ let stream = match self.build_stream(socket).await {
+ Ok(v) => v,
+ Err(e) => {
+ tracing::error!(err=?e, "TLS acceptor failed");
+ continue;
+ }
+ };
+
+ let login = self.login_provider.clone();
+ let conn = tokio::spawn(async move {
+ //@FIXME should create a generic "public web" server on which "routers" could be
+ //abitrarily bound
+ //@FIXME replace with a handler supporting http2
+
+ match http::Builder::new()
+ .serve_connection(
+ stream,
+ service_fn(|req: Request<hyper::body::Incoming>| {
+ let login = login.clone();
+ tracing::info!("{:?} {:?}", req.method(), req.uri());
+ tracing::debug!(req=?req, "full request");
+ async {
+ let response = match middleware::auth(login, req, |user, request| {
+ async { Controller::route(user, request).await }.boxed()
+ })
+ .await
+ {
+ Ok(v) => Ok(v),
+ Err(e) => {
+ tracing::error!(err=?e, "internal error");
+ Response::builder()
+ .status(500)
+ .body(codec::text_body("Internal error"))
+ }
+ };
+ tracing::debug!(resp=?response, "full response");
+ response
+ }
+ }),
+ )
+ .await
+ {
+ Err(e) => tracing::warn!(err=?e, "connection failed"),
+ Ok(()) => tracing::trace!("connection terminated with success"),
+ }
+ });
+ connections.push(conn);
+ }
+ drop(tcp);
+
+ tracing::info!("Server shutting down, draining remaining connections...");
+ while connections.next().await.is_some() {}
+
+ Ok(())
+ }
+
+ async fn build_stream(&self, socket: TcpStream) -> Result<Box<dyn Stream>> {
+ match self.tls.clone() {
+ Some(acceptor) => {
+ let stream = acceptor.accept(socket).await?;
+ Ok(Box::new(TokioIo::new(stream)))
+ }
+ None => Ok(Box::new(TokioIo::new(socket))),
+ }
+ }
+}
+
+// <D:propfind xmlns:D='DAV:' xmlns:A='http://apple.com/ns/ical/'>
+// <D:prop>
+// <D:getcontenttype/>
+// <D:resourcetype/>
+// <D:displayname/>
+// <A:calendar-color/>
+// </D:prop>
+// </D:propfind>
+
+// <D:propfind xmlns:D='DAV:' xmlns:A='http://apple.com/ns/ical/' xmlns:C='urn:ietf:params:xml:ns:caldav'>
+// <D:prop>
+// <D:resourcetype/>
+// <D:owner/>
+// <D:displayname/>
+// <D:current-user-principal/>
+// <D:current-user-privilege-set/>
+// <A:calendar-color/>
+// <C:calendar-home-set/>
+// </D:prop>
+// </D:propfind>
+
+// <D:propfind xmlns:D='DAV:' xmlns:C='urn:ietf:params:xml:ns:caldav' xmlns:CS='http://calendarserver.org/ns/'>
+// <D:prop>
+// <D:resourcetype/>
+// <D:owner/>
+// <D:current-user-principal/>
+// <D:current-user-privilege-set/>
+// <D:supported-report-set/>
+// <C:supported-calendar-component-set/>
+// <CS:getctag/>
+// </D:prop>
+// </D:propfind>
+
+// <C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+// <D:prop>
+// <D:getetag/>
+// <C:calendar-data/>
+// </D:prop>
+// <D:href>/alice/calendar/personal/something.ics</D:href>
+// </C:calendar-multiget>
diff --git a/aero-proto/src/dav/node.rs b/aero-proto/src/dav/node.rs
new file mode 100644
index 0000000..3af3b81
--- /dev/null
+++ b/aero-proto/src/dav/node.rs
@@ -0,0 +1,145 @@
+use anyhow::Result;
+use futures::future::{BoxFuture, FutureExt};
+use futures::stream::{BoxStream, StreamExt};
+use hyper::body::Bytes;
+
+use aero_collections::davdag::{Etag, Token};
+use aero_dav::realization::All;
+use aero_dav::types as dav;
+
+use super::controller::ArcUser;
+
+pub(crate) type Content<'a> = BoxStream<'a, std::result::Result<Bytes, std::io::Error>>;
+pub(crate) type PropertyStream<'a> =
+ BoxStream<'a, std::result::Result<dav::Property<All>, dav::PropertyRequest<All>>>;
+
+pub(crate) enum PutPolicy {
+ OverwriteAll,
+ CreateOnly,
+ ReplaceEtag(String),
+}
+
+/// A DAV node should implement the following methods
+/// @FIXME not satisfied by BoxFutures but I have no better idea currently
+pub(crate) trait DavNode: Send {
+ // recurence, filesystem hierarchy
+ /// This node direct children
+ fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>>;
+ /// Recursively fetch a child (progress inside the filesystem hierarchy)
+ fn fetch<'a>(
+ &self,
+ user: &'a ArcUser,
+ path: &'a [&str],
+ create: bool,
+ ) -> BoxFuture<'a, Result<Box<dyn DavNode>>>;
+
+ // node properties
+ /// Get the path
+ fn path(&self, user: &ArcUser) -> String;
+ /// Get the supported WebDAV properties
+ fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All>;
+ /// Get the values for the given properties
+ fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static>;
+ /// Get the value of the DAV header to return
+ fn dav_header(&self) -> String;
+
+ /// Put an element (create or update)
+ fn put<'a>(
+ &'a self,
+ policy: PutPolicy,
+ stream: Content<'a>,
+ ) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>>;
+ /// Content type of the element
+ fn content_type(&self) -> &str;
+ /// Get ETag
+ fn etag(&self) -> BoxFuture<Option<Etag>>;
+ /// Get content
+ fn content<'a>(&self) -> Content<'a>;
+ /// Delete
+ fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>>;
+ /// Sync
+ fn diff<'a>(
+ &self,
+ sync_token: Option<Token>,
+ ) -> BoxFuture<
+ 'a,
+ std::result::Result<(Token, Vec<Box<dyn DavNode>>, Vec<dav::Href>), std::io::Error>,
+ >;
+
+ /// Utility function to get a propname response from a node
+ fn response_propname(&self, user: &ArcUser) -> dav::Response<All> {
+ dav::Response {
+ status_or_propstat: dav::StatusOrPropstat::PropStat(
+ dav::Href(self.path(user)),
+ vec![dav::PropStat {
+ status: dav::Status(hyper::StatusCode::OK),
+ prop: dav::AnyProp(
+ self.supported_properties(user)
+ .0
+ .into_iter()
+ .map(dav::AnyProperty::Request)
+ .collect(),
+ ),
+ error: None,
+ responsedescription: None,
+ }],
+ ),
+ error: None,
+ location: None,
+ responsedescription: None,
+ }
+ }
+
+ /// Utility function to get a prop response from a node & a list of propname
+ fn response_props(
+ &self,
+ user: &ArcUser,
+ props: dav::PropName<All>,
+ ) -> BoxFuture<'static, dav::Response<All>> {
+ //@FIXME we should make the DAV parsed object a stream...
+ let mut result_stream = self.properties(user, props);
+ let path = self.path(user);
+
+ async move {
+ let mut prop_desc = vec![];
+ let (mut found, mut not_found) = (vec![], vec![]);
+ while let Some(maybe_prop) = result_stream.next().await {
+ match maybe_prop {
+ Ok(v) => found.push(dav::AnyProperty::Value(v)),
+ Err(v) => not_found.push(dav::AnyProperty::Request(v)),
+ }
+ }
+
+ // If at least one property has been found on this object, adding a HTTP 200 propstat to
+ // the response
+ if !found.is_empty() {
+ prop_desc.push(dav::PropStat {
+ status: dav::Status(hyper::StatusCode::OK),
+ prop: dav::AnyProp(found),
+ error: None,
+ responsedescription: None,
+ });
+ }
+
+ // If at least one property can't be found on this object, adding a HTTP 404 propstat to
+ // the response
+ if !not_found.is_empty() {
+ prop_desc.push(dav::PropStat {
+ status: dav::Status(hyper::StatusCode::NOT_FOUND),
+ prop: dav::AnyProp(not_found),
+ error: None,
+ responsedescription: None,
+ })
+ }
+
+ // Build the finale response
+ dav::Response {
+ status_or_propstat: dav::StatusOrPropstat::PropStat(dav::Href(path), prop_desc),
+ error: None,
+ location: None,
+ responsedescription: None,
+ }
+ }
+ .boxed()
+ }
+}
diff --git a/aero-proto/src/dav/resource.rs b/aero-proto/src/dav/resource.rs
new file mode 100644
index 0000000..b5ae029
--- /dev/null
+++ b/aero-proto/src/dav/resource.rs
@@ -0,0 +1,999 @@
+use std::sync::Arc;
+type ArcUser = std::sync::Arc<User>;
+
+use anyhow::{anyhow, Result};
+use futures::io::AsyncReadExt;
+use futures::stream::{StreamExt, TryStreamExt};
+use futures::{future::BoxFuture, future::FutureExt};
+
+use aero_collections::{
+ calendar::Calendar,
+ davdag::{BlobId, Etag, SyncChange, Token},
+ user::User,
+};
+use aero_dav::acltypes as acl;
+use aero_dav::caltypes as cal;
+use aero_dav::realization::{self as all, All};
+use aero_dav::synctypes as sync;
+use aero_dav::types as dav;
+use aero_dav::versioningtypes as vers;
+
+use super::node::PropertyStream;
+use crate::dav::node::{Content, DavNode, PutPolicy};
+
+/// Why "https://aerogramme.0"?
+/// Because tokens must be valid URI.
+/// And numeric TLD are ~mostly valid in URI (check the .42 TLD experience)
+/// and at the same time, they are not used sold by the ICANN and there is no plan to use them.
+/// So I am sure that the URL remains invalid, avoiding leaking requests to an hardcoded URL in the
+/// future.
+/// The best option would be to make it configurable ofc, so someone can put a domain name
+/// that they control, it would probably improve compatibility (maybe some WebDAV spec tells us
+/// how to handle/resolve this URI but I am not aware of that...). But that's not the plan for
+/// now. So here we are: https://aerogramme.0.
+pub const BASE_TOKEN_URI: &str = "https://aerogramme.0/sync/";
+
+#[derive(Clone)]
+pub(crate) struct RootNode {}
+impl DavNode for RootNode {
+ fn fetch<'a>(
+ &self,
+ user: &'a ArcUser,
+ path: &'a [&str],
+ create: bool,
+ ) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
+ if path.len() == 0 {
+ let this = self.clone();
+ return async { Ok(Box::new(this) as Box<dyn DavNode>) }.boxed();
+ }
+
+ if path[0] == user.username {
+ let child = Box::new(HomeNode {});
+ return child.fetch(user, &path[1..], create);
+ }
+
+ //@NOTE: We can't create a node at this level
+ async { Err(anyhow!("Not found")) }.boxed()
+ }
+
+ fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
+ async { vec![Box::new(HomeNode {}) as Box<dyn DavNode>] }.boxed()
+ }
+
+ fn path(&self, user: &ArcUser) -> String {
+ "/".into()
+ }
+
+ fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
+ dav::PropName(vec![
+ dav::PropertyRequest::DisplayName,
+ dav::PropertyRequest::ResourceType,
+ dav::PropertyRequest::GetContentType,
+ dav::PropertyRequest::Extension(all::PropertyRequest::Acl(
+ acl::PropertyRequest::CurrentUserPrincipal,
+ )),
+ ])
+ }
+
+ fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
+ let user = user.clone();
+ futures::stream::iter(prop.0)
+ .map(move |n| {
+ let prop = match n {
+ dav::PropertyRequest::DisplayName => {
+ dav::Property::DisplayName("DAV Root".to_string())
+ }
+ dav::PropertyRequest::ResourceType => {
+ dav::Property::ResourceType(vec![dav::ResourceType::Collection])
+ }
+ dav::PropertyRequest::GetContentType => {
+ dav::Property::GetContentType("httpd/unix-directory".into())
+ }
+ dav::PropertyRequest::Extension(all::PropertyRequest::Acl(
+ acl::PropertyRequest::CurrentUserPrincipal,
+ )) => dav::Property::Extension(all::Property::Acl(
+ acl::Property::CurrentUserPrincipal(acl::User::Authenticated(dav::Href(
+ HomeNode {}.path(&user),
+ ))),
+ )),
+ v => return Err(v),
+ };
+ Ok(prop)
+ })
+ .boxed()
+ }
+
+ fn put<'a>(
+ &'a self,
+ _policy: PutPolicy,
+ stream: Content<'a>,
+ ) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
+ futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported)).boxed()
+ }
+
+ fn content<'a>(&self) -> Content<'a> {
+ futures::stream::once(futures::future::err(std::io::Error::from(
+ std::io::ErrorKind::Unsupported,
+ )))
+ .boxed()
+ }
+
+ fn content_type(&self) -> &str {
+ "text/plain"
+ }
+
+ fn etag(&self) -> BoxFuture<Option<Etag>> {
+ async { None }.boxed()
+ }
+
+ fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
+ async { Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)) }.boxed()
+ }
+
+ fn diff<'a>(
+ &self,
+ _sync_token: Option<Token>,
+ ) -> BoxFuture<
+ 'a,
+ std::result::Result<(Token, Vec<Box<dyn DavNode>>, Vec<dav::Href>), std::io::Error>,
+ > {
+ async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
+ }
+
+ fn dav_header(&self) -> String {
+ "1".into()
+ }
+}
+
+#[derive(Clone)]
+pub(crate) struct HomeNode {}
+impl DavNode for HomeNode {
+ fn fetch<'a>(
+ &self,
+ user: &'a ArcUser,
+ path: &'a [&str],
+ create: bool,
+ ) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
+ if path.len() == 0 {
+ let node = Box::new(self.clone()) as Box<dyn DavNode>;
+ return async { Ok(node) }.boxed();
+ }
+
+ if path[0] == "calendar" {
+ return async move {
+ let child = Box::new(CalendarListNode::new(user).await?);
+ child.fetch(user, &path[1..], create).await
+ }
+ .boxed();
+ }
+
+ //@NOTE: we can't create a node at this level
+ async { Err(anyhow!("Not found")) }.boxed()
+ }
+
+ fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
+ async {
+ CalendarListNode::new(user)
+ .await
+ .map(|c| vec![Box::new(c) as Box<dyn DavNode>])
+ .unwrap_or(vec![])
+ }
+ .boxed()
+ }
+
+ fn path(&self, user: &ArcUser) -> String {
+ format!("/{}/", user.username)
+ }
+
+ fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
+ dav::PropName(vec![
+ dav::PropertyRequest::DisplayName,
+ dav::PropertyRequest::ResourceType,
+ dav::PropertyRequest::GetContentType,
+ dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
+ cal::PropertyRequest::CalendarHomeSet,
+ )),
+ ])
+ }
+ fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
+ let user = user.clone();
+
+ futures::stream::iter(prop.0)
+ .map(move |n| {
+ let prop = match n {
+ dav::PropertyRequest::DisplayName => {
+ dav::Property::DisplayName(format!("{} home", user.username))
+ }
+ dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![
+ dav::ResourceType::Collection,
+ dav::ResourceType::Extension(all::ResourceType::Acl(
+ acl::ResourceType::Principal,
+ )),
+ ]),
+ dav::PropertyRequest::GetContentType => {
+ dav::Property::GetContentType("httpd/unix-directory".into())
+ }
+ dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
+ cal::PropertyRequest::CalendarHomeSet,
+ )) => dav::Property::Extension(all::Property::Cal(
+ cal::Property::CalendarHomeSet(dav::Href(
+ //@FIXME we are hardcoding the calendar path, instead we would want to use
+ //objects
+ format!("/{}/calendar/", user.username),
+ )),
+ )),
+ v => return Err(v),
+ };
+ Ok(prop)
+ })
+ .boxed()
+ }
+
+ fn put<'a>(
+ &'a self,
+ _policy: PutPolicy,
+ stream: Content<'a>,
+ ) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
+ futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported)).boxed()
+ }
+
+ fn content<'a>(&self) -> Content<'a> {
+ futures::stream::once(futures::future::err(std::io::Error::from(
+ std::io::ErrorKind::Unsupported,
+ )))
+ .boxed()
+ }
+
+ fn content_type(&self) -> &str {
+ "text/plain"
+ }
+
+ fn etag(&self) -> BoxFuture<Option<Etag>> {
+ async { None }.boxed()
+ }
+
+ fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
+ async { Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)) }.boxed()
+ }
+ fn diff<'a>(
+ &self,
+ _sync_token: Option<Token>,
+ ) -> BoxFuture<
+ 'a,
+ std::result::Result<(Token, Vec<Box<dyn DavNode>>, Vec<dav::Href>), std::io::Error>,
+ > {
+ async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
+ }
+
+ fn dav_header(&self) -> String {
+ "1, access-control, calendar-access".into()
+ }
+}
+
+#[derive(Clone)]
+pub(crate) struct CalendarListNode {
+ list: Vec<String>,
+}
+impl CalendarListNode {
+ async fn new(user: &ArcUser) -> Result<Self> {
+ let list = user.calendars.list(user).await?;
+ Ok(Self { list })
+ }
+}
+impl DavNode for CalendarListNode {
+ fn fetch<'a>(
+ &self,
+ user: &'a ArcUser,
+ path: &'a [&str],
+ create: bool,
+ ) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
+ if path.len() == 0 {
+ let node = Box::new(self.clone()) as Box<dyn DavNode>;
+ return async { Ok(node) }.boxed();
+ }
+
+ async move {
+ //@FIXME: we should create a node if the open returns a "not found".
+ let cal = user
+ .calendars
+ .open(user, path[0])
+ .await?
+ .ok_or(anyhow!("Not found"))?;
+ let child = Box::new(CalendarNode {
+ col: cal,
+ calname: path[0].to_string(),
+ });
+ child.fetch(user, &path[1..], create).await
+ }
+ .boxed()
+ }
+
+ fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
+ let list = self.list.clone();
+ async move {
+ //@FIXME maybe we want to be lazy here?!
+ futures::stream::iter(list.iter())
+ .filter_map(|name| async move {
+ user.calendars
+ .open(user, name)
+ .await
+ .ok()
+ .flatten()
+ .map(|v| (name, v))
+ })
+ .map(|(name, cal)| {
+ Box::new(CalendarNode {
+ col: cal,
+ calname: name.to_string(),
+ }) as Box<dyn DavNode>
+ })
+ .collect::<Vec<Box<dyn DavNode>>>()
+ .await
+ }
+ .boxed()
+ }
+
+ fn path(&self, user: &ArcUser) -> String {
+ format!("/{}/calendar/", user.username)
+ }
+
+ fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
+ dav::PropName(vec![
+ dav::PropertyRequest::DisplayName,
+ dav::PropertyRequest::ResourceType,
+ dav::PropertyRequest::GetContentType,
+ ])
+ }
+ fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
+ let user = user.clone();
+
+ futures::stream::iter(prop.0)
+ .map(move |n| {
+ let prop = match n {
+ dav::PropertyRequest::DisplayName => {
+ dav::Property::DisplayName(format!("{} calendars", user.username))
+ }
+ dav::PropertyRequest::ResourceType => {
+ dav::Property::ResourceType(vec![dav::ResourceType::Collection])
+ }
+ dav::PropertyRequest::GetContentType => {
+ dav::Property::GetContentType("httpd/unix-directory".into())
+ }
+ v => return Err(v),
+ };
+ Ok(prop)
+ })
+ .boxed()
+ }
+
+ fn put<'a>(
+ &'a self,
+ _policy: PutPolicy,
+ stream: Content<'a>,
+ ) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
+ futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported)).boxed()
+ }
+
+ fn content<'a>(&self) -> Content<'a> {
+ futures::stream::once(futures::future::err(std::io::Error::from(
+ std::io::ErrorKind::Unsupported,
+ )))
+ .boxed()
+ }
+
+ fn content_type(&self) -> &str {
+ "text/plain"
+ }
+
+ fn etag(&self) -> BoxFuture<Option<Etag>> {
+ async { None }.boxed()
+ }
+
+ fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
+ async { Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)) }.boxed()
+ }
+ fn diff<'a>(
+ &self,
+ _sync_token: Option<Token>,
+ ) -> BoxFuture<
+ 'a,
+ std::result::Result<(Token, Vec<Box<dyn DavNode>>, Vec<dav::Href>), std::io::Error>,
+ > {
+ async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
+ }
+
+ fn dav_header(&self) -> String {
+ "1, access-control, calendar-access".into()
+ }
+}
+
+#[derive(Clone)]
+pub(crate) struct CalendarNode {
+ col: Arc<Calendar>,
+ calname: String,
+}
+impl DavNode for CalendarNode {
+ fn fetch<'a>(
+ &self,
+ user: &'a ArcUser,
+ path: &'a [&str],
+ create: bool,
+ ) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
+ if path.len() == 0 {
+ let node = Box::new(self.clone()) as Box<dyn DavNode>;
+ return async { Ok(node) }.boxed();
+ }
+
+ let col = self.col.clone();
+ let calname = self.calname.clone();
+ async move {
+ match (col.dag().await.idx_by_filename.get(path[0]), create) {
+ (Some(blob_id), _) => {
+ let child = Box::new(EventNode {
+ col: col.clone(),
+ calname,
+ filename: path[0].to_string(),
+ blob_id: *blob_id,
+ });
+ child.fetch(user, &path[1..], create).await
+ }
+ (None, true) => {
+ let child = Box::new(CreateEventNode {
+ col: col.clone(),
+ calname,
+ filename: path[0].to_string(),
+ });
+ child.fetch(user, &path[1..], create).await
+ }
+ _ => Err(anyhow!("Not found")),
+ }
+ }
+ .boxed()
+ }
+
+ fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
+ let col = self.col.clone();
+ let calname = self.calname.clone();
+
+ async move {
+ col.dag()
+ .await
+ .idx_by_filename
+ .iter()
+ .map(|(filename, blob_id)| {
+ Box::new(EventNode {
+ col: col.clone(),
+ calname: calname.clone(),
+ filename: filename.to_string(),
+ blob_id: *blob_id,
+ }) as Box<dyn DavNode>
+ })
+ .collect()
+ }
+ .boxed()
+ }
+
+ fn path(&self, user: &ArcUser) -> String {
+ format!("/{}/calendar/{}/", user.username, self.calname)
+ }
+
+ fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
+ dav::PropName(vec![
+ dav::PropertyRequest::DisplayName,
+ dav::PropertyRequest::ResourceType,
+ dav::PropertyRequest::GetContentType,
+ dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
+ cal::PropertyRequest::SupportedCalendarComponentSet,
+ )),
+ dav::PropertyRequest::Extension(all::PropertyRequest::Sync(
+ sync::PropertyRequest::SyncToken,
+ )),
+ dav::PropertyRequest::Extension(all::PropertyRequest::Vers(
+ vers::PropertyRequest::SupportedReportSet,
+ )),
+ ])
+ }
+ fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
+ let calname = self.calname.to_string();
+ let col = self.col.clone();
+
+ futures::stream::iter(prop.0)
+ .then(move |n| {
+ let calname = calname.clone();
+ let col = col.clone();
+
+ async move {
+ let prop = match n {
+ dav::PropertyRequest::DisplayName => {
+ dav::Property::DisplayName(format!("{} calendar", calname))
+ }
+ dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![
+ dav::ResourceType::Collection,
+ dav::ResourceType::Extension(all::ResourceType::Cal(
+ cal::ResourceType::Calendar,
+ )),
+ ]),
+ //dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("httpd/unix-directory".into())),
+ //@FIXME seems wrong but seems to be what Thunderbird expects...
+ dav::PropertyRequest::GetContentType => {
+ dav::Property::GetContentType("text/calendar".into())
+ }
+ dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
+ cal::PropertyRequest::SupportedCalendarComponentSet,
+ )) => dav::Property::Extension(all::Property::Cal(
+ cal::Property::SupportedCalendarComponentSet(vec![
+ cal::CompSupport(cal::Component::VEvent),
+ cal::CompSupport(cal::Component::VTodo),
+ cal::CompSupport(cal::Component::VJournal),
+ ]),
+ )),
+ dav::PropertyRequest::Extension(all::PropertyRequest::Sync(
+ sync::PropertyRequest::SyncToken,
+ )) => match col.token().await {
+ Ok(token) => dav::Property::Extension(all::Property::Sync(
+ sync::Property::SyncToken(sync::SyncToken(format!(
+ "{}{}",
+ BASE_TOKEN_URI, token
+ ))),
+ )),
+ _ => return Err(n.clone()),
+ },
+ dav::PropertyRequest::Extension(all::PropertyRequest::Vers(
+ vers::PropertyRequest::SupportedReportSet,
+ )) => dav::Property::Extension(all::Property::Vers(
+ vers::Property::SupportedReportSet(vec![
+ vers::SupportedReport(vers::ReportName::Extension(
+ all::ReportTypeName::Cal(cal::ReportTypeName::Multiget),
+ )),
+ vers::SupportedReport(vers::ReportName::Extension(
+ all::ReportTypeName::Cal(cal::ReportTypeName::Query),
+ )),
+ vers::SupportedReport(vers::ReportName::Extension(
+ all::ReportTypeName::Sync(sync::ReportTypeName::SyncCollection),
+ )),
+ ]),
+ )),
+ v => return Err(v),
+ };
+ Ok(prop)
+ }
+ })
+ .boxed()
+ }
+
+ fn put<'a>(
+ &'a self,
+ _policy: PutPolicy,
+ _stream: Content<'a>,
+ ) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
+ futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported)).boxed()
+ }
+
+ fn content<'a>(&self) -> Content<'a> {
+ futures::stream::once(futures::future::err(std::io::Error::from(
+ std::io::ErrorKind::Unsupported,
+ )))
+ .boxed()
+ }
+
+ fn content_type(&self) -> &str {
+ "text/plain"
+ }
+
+ fn etag(&self) -> BoxFuture<Option<Etag>> {
+ async { None }.boxed()
+ }
+
+ fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
+ async { Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)) }.boxed()
+ }
+ fn diff<'a>(
+ &self,
+ sync_token: Option<Token>,
+ ) -> BoxFuture<
+ 'a,
+ std::result::Result<(Token, Vec<Box<dyn DavNode>>, Vec<dav::Href>), std::io::Error>,
+ > {
+ let col = self.col.clone();
+ let calname = self.calname.clone();
+ async move {
+ let sync_token = match sync_token {
+ Some(v) => v,
+ None => {
+ let token = col
+ .token()
+ .await
+ .or(Err(std::io::Error::from(std::io::ErrorKind::Interrupted)))?;
+ let ok_nodes = col
+ .dag()
+ .await
+ .idx_by_filename
+ .iter()
+ .map(|(filename, blob_id)| {
+ Box::new(EventNode {
+ col: col.clone(),
+ calname: calname.clone(),
+ filename: filename.to_string(),
+ blob_id: *blob_id,
+ }) as Box<dyn DavNode>
+ })
+ .collect();
+
+ return Ok((token, ok_nodes, vec![]));
+ }
+ };
+ let (new_token, listed_changes) = match col.diff(sync_token).await {
+ Ok(v) => v,
+ Err(e) => {
+ tracing::info!(err=?e, "token resolution failed, maybe a forgotten token");
+ return Err(std::io::Error::from(std::io::ErrorKind::NotFound));
+ }
+ };
+
+ let mut ok_nodes: Vec<Box<dyn DavNode>> = vec![];
+ let mut rm_nodes: Vec<dav::Href> = vec![];
+ for change in listed_changes.into_iter() {
+ match change {
+ SyncChange::Ok((filename, blob_id)) => {
+ let child = Box::new(EventNode {
+ col: col.clone(),
+ calname: calname.clone(),
+ filename,
+ blob_id,
+ });
+ ok_nodes.push(child);
+ }
+ SyncChange::NotFound(filename) => {
+ rm_nodes.push(dav::Href(filename));
+ }
+ }
+ }
+
+ Ok((new_token, ok_nodes, rm_nodes))
+ }
+ .boxed()
+ }
+ fn dav_header(&self) -> String {
+ "1, access-control, calendar-access".into()
+ }
+}
+
+#[derive(Clone)]
+pub(crate) struct EventNode {
+ col: Arc<Calendar>,
+ calname: String,
+ filename: String,
+ blob_id: BlobId,
+}
+
+impl DavNode for EventNode {
+ fn fetch<'a>(
+ &self,
+ user: &'a ArcUser,
+ path: &'a [&str],
+ create: bool,
+ ) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
+ if path.len() == 0 {
+ let node = Box::new(self.clone()) as Box<dyn DavNode>;
+ return async { Ok(node) }.boxed();
+ }
+
+ async {
+ Err(anyhow!(
+ "Not supported: can't create a child on an event node"
+ ))
+ }
+ .boxed()
+ }
+
+ fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
+ async { vec![] }.boxed()
+ }
+
+ fn path(&self, user: &ArcUser) -> String {
+ format!(
+ "/{}/calendar/{}/{}",
+ user.username, self.calname, self.filename
+ )
+ }
+
+ fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
+ dav::PropName(vec![
+ dav::PropertyRequest::DisplayName,
+ dav::PropertyRequest::ResourceType,
+ dav::PropertyRequest::GetEtag,
+ dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
+ cal::PropertyRequest::CalendarData(cal::CalendarDataRequest::default()),
+ )),
+ ])
+ }
+ fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
+ let this = self.clone();
+
+ futures::stream::iter(prop.0)
+ .then(move |n| {
+ let this = this.clone();
+
+ async move {
+ let prop = match &n {
+ dav::PropertyRequest::DisplayName => {
+ dav::Property::DisplayName(format!("{} event", this.filename))
+ }
+ dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![]),
+ dav::PropertyRequest::GetContentType => {
+ dav::Property::GetContentType("text/calendar".into())
+ }
+ dav::PropertyRequest::GetEtag => {
+ let etag = this.etag().await.ok_or(n.clone())?;
+ dav::Property::GetEtag(etag)
+ }
+ dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
+ cal::PropertyRequest::CalendarData(req),
+ )) => {
+ let ics = String::from_utf8(
+ this.col.get(this.blob_id).await.or(Err(n.clone()))?,
+ )
+ .or(Err(n.clone()))?;
+
+ let new_ics = match &req.comp {
+ None => ics,
+ Some(prune_comp) => {
+ // parse content
+ let ics = match icalendar::parser::read_calendar(&ics) {
+ Ok(v) => v,
+ Err(e) => {
+ tracing::warn!(err=?e, "Unable to parse ICS in calendar-query");
+ return Err(n.clone())
+ }
+ };
+
+ // build a fake vcal component for caldav compat
+ let fake_vcal_component = icalendar::parser::Component {
+ name: cal::Component::VCalendar.as_str().into(),
+ properties: ics.properties,
+ components: ics.components,
+ };
+
+ // rebuild component
+ let new_comp = match aero_ical::prune::component(&fake_vcal_component, prune_comp) {
+ Some(v) => v,
+ None => return Err(n.clone()),
+ };
+
+ // reserialize
+ format!("{}", icalendar::parser::Calendar { properties: new_comp.properties, components: new_comp.components })
+ },
+ };
+
+
+
+ dav::Property::Extension(all::Property::Cal(
+ cal::Property::CalendarData(cal::CalendarDataPayload {
+ mime: None,
+ payload: new_ics,
+ }),
+ ))
+ }
+ _ => return Err(n),
+ };
+ Ok(prop)
+ }
+ })
+ .boxed()
+ }
+
+ fn put<'a>(
+ &'a self,
+ policy: PutPolicy,
+ stream: Content<'a>,
+ ) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
+ async {
+ let existing_etag = self
+ .etag()
+ .await
+ .ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Etag error"))?;
+ match policy {
+ PutPolicy::CreateOnly => {
+ return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists))
+ }
+ PutPolicy::ReplaceEtag(etag) if etag != existing_etag.as_str() => {
+ return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists))
+ }
+ _ => (),
+ };
+
+ //@FIXME for now, our storage interface does not allow streaming,
+ // so we load everything in memory
+ let mut evt = Vec::new();
+ let mut reader = stream.into_async_read();
+ reader
+ .read_to_end(&mut evt)
+ .await
+ .or(Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe)))?;
+ let (_token, entry) = self
+ .col
+ .put(self.filename.as_str(), evt.as_ref())
+ .await
+ .or(Err(std::io::ErrorKind::Interrupted))?;
+ self.col
+ .opportunistic_sync()
+ .await
+ .or(Err(std::io::ErrorKind::ConnectionReset))?;
+ Ok(entry.2)
+ }
+ .boxed()
+ }
+
+ fn content<'a>(&self) -> Content<'a> {
+ //@FIXME for now, our storage interface does not allow streaming,
+ // so we load everything in memory
+ let calendar = self.col.clone();
+ let blob_id = self.blob_id.clone();
+ let calblob = async move {
+ let raw_ics = calendar
+ .get(blob_id)
+ .await
+ .or(Err(std::io::Error::from(std::io::ErrorKind::Interrupted)))?;
+
+ Ok(hyper::body::Bytes::from(raw_ics))
+ };
+ futures::stream::once(Box::pin(calblob)).boxed()
+ }
+
+ fn content_type(&self) -> &str {
+ "text/calendar"
+ }
+
+ fn etag(&self) -> BoxFuture<Option<Etag>> {
+ let calendar = self.col.clone();
+
+ async move {
+ calendar
+ .dag()
+ .await
+ .table
+ .get(&self.blob_id)
+ .map(|(_, _, etag)| etag.to_string())
+ }
+ .boxed()
+ }
+
+ fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
+ let calendar = self.col.clone();
+ let blob_id = self.blob_id.clone();
+
+ async move {
+ let _token = match calendar.delete(blob_id).await {
+ Ok(v) => v,
+ Err(e) => {
+ tracing::error!(err=?e, "delete event node");
+ return Err(std::io::Error::from(std::io::ErrorKind::Interrupted));
+ }
+ };
+ calendar
+ .opportunistic_sync()
+ .await
+ .or(Err(std::io::ErrorKind::ConnectionReset))?;
+ Ok(())
+ }
+ .boxed()
+ }
+ fn diff<'a>(
+ &self,
+ _sync_token: Option<Token>,
+ ) -> BoxFuture<
+ 'a,
+ std::result::Result<(Token, Vec<Box<dyn DavNode>>, Vec<dav::Href>), std::io::Error>,
+ > {
+ async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
+ }
+
+ fn dav_header(&self) -> String {
+ "1, access-control".into()
+ }
+}
+
+#[derive(Clone)]
+pub(crate) struct CreateEventNode {
+ col: Arc<Calendar>,
+ calname: String,
+ filename: String,
+}
+impl DavNode for CreateEventNode {
+ fn fetch<'a>(
+ &self,
+ user: &'a ArcUser,
+ path: &'a [&str],
+ create: bool,
+ ) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
+ if path.len() == 0 {
+ let node = Box::new(self.clone()) as Box<dyn DavNode>;
+ return async { Ok(node) }.boxed();
+ }
+
+ async {
+ Err(anyhow!(
+ "Not supported: can't create a child on an event node"
+ ))
+ }
+ .boxed()
+ }
+
+ fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
+ async { vec![] }.boxed()
+ }
+
+ fn path(&self, user: &ArcUser) -> String {
+ format!(
+ "/{}/calendar/{}/{}",
+ user.username, self.calname, self.filename
+ )
+ }
+
+ fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
+ dav::PropName(vec![])
+ }
+
+ fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
+ futures::stream::iter(vec![]).boxed()
+ }
+
+ fn put<'a>(
+ &'a self,
+ _policy: PutPolicy,
+ stream: Content<'a>,
+ ) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
+ //@NOTE: policy might not be needed here: whatever we put, there is no known entries here
+
+ async {
+ //@FIXME for now, our storage interface does not allow for streaming
+ let mut evt = Vec::new();
+ let mut reader = stream.into_async_read();
+ reader.read_to_end(&mut evt).await.unwrap();
+ let (_token, entry) = self
+ .col
+ .put(self.filename.as_str(), evt.as_ref())
+ .await
+ .or(Err(std::io::ErrorKind::Interrupted))?;
+ self.col
+ .opportunistic_sync()
+ .await
+ .or(Err(std::io::ErrorKind::ConnectionReset))?;
+ Ok(entry.2)
+ }
+ .boxed()
+ }
+
+ fn content<'a>(&self) -> Content<'a> {
+ futures::stream::once(futures::future::err(std::io::Error::from(
+ std::io::ErrorKind::Unsupported,
+ )))
+ .boxed()
+ }
+
+ fn content_type(&self) -> &str {
+ "text/plain"
+ }
+
+ fn etag(&self) -> BoxFuture<Option<Etag>> {
+ async { None }.boxed()
+ }
+
+ fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
+ // Nothing to delete
+ async { Ok(()) }.boxed()
+ }
+ fn diff<'a>(
+ &self,
+ _sync_token: Option<Token>,
+ ) -> BoxFuture<
+ 'a,
+ std::result::Result<(Token, Vec<Box<dyn DavNode>>, Vec<dav::Href>), std::io::Error>,
+ > {
+ async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
+ }
+
+ fn dav_header(&self) -> String {
+ "1, access-control".into()
+ }
+}
diff --git a/src/imap/attributes.rs b/aero-proto/src/imap/attributes.rs
index 89446a8..89446a8 100644
--- a/src/imap/attributes.rs
+++ b/aero-proto/src/imap/attributes.rs
diff --git a/src/imap/capability.rs b/aero-proto/src/imap/capability.rs
index c76b51c..c76b51c 100644
--- a/src/imap/capability.rs
+++ b/aero-proto/src/imap/capability.rs
diff --git a/src/imap/command/anonymous.rs b/aero-proto/src/imap/command/anonymous.rs
index 0582b06..f23ec17 100644
--- a/src/imap/command/anonymous.rs
+++ b/aero-proto/src/imap/command/anonymous.rs
@@ -4,12 +4,13 @@ use imap_codec::imap_types::core::AString;
use imap_codec::imap_types::response::Code;
use imap_codec::imap_types::secret::Secret;
+use aero_collections::user::User;
+use aero_user::login::ArcLoginProvider;
+
use crate::imap::capability::ServerCapability;
use crate::imap::command::anystate;
use crate::imap::flow;
use crate::imap::response::Response;
-use crate::login::ArcLoginProvider;
-use crate::mail::user::User;
//--- dispatching
diff --git a/src/imap/command/anystate.rs b/aero-proto/src/imap/command/anystate.rs
index 718ba3f..718ba3f 100644
--- a/src/imap/command/anystate.rs
+++ b/aero-proto/src/imap/command/anystate.rs
diff --git a/src/imap/command/authenticated.rs b/aero-proto/src/imap/command/authenticated.rs
index eb8833d..5bd34cb 100644
--- a/src/imap/command/authenticated.rs
+++ b/aero-proto/src/imap/command/authenticated.rs
@@ -14,16 +14,16 @@ use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec};
use imap_codec::imap_types::response::{Code, CodeOther, Data};
use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName};
+use aero_collections::mail::namespace::MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW;
+use aero_collections::mail::uidindex::*;
+use aero_collections::mail::IMF;
+use aero_collections::user::User;
+
use crate::imap::capability::{ClientCapability, ServerCapability};
use crate::imap::command::{anystate, MailboxName};
use crate::imap::flow;
-use crate::imap::mailbox_view::{MailboxView, UpdateParameters};
+use crate::imap::mailbox_view::MailboxView;
use crate::imap::response::Response;
-use crate::imap::Body;
-
-use crate::mail::uidindex::*;
-use crate::mail::user::{User, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW};
-use crate::mail::IMF;
pub struct AuthenticatedContext<'a> {
pub req: &'a Command<'static>,
@@ -610,7 +610,7 @@ impl<'a> AuthenticatedContext<'a> {
Some(mb) => mb,
None => bail!("Mailbox does not exist"),
};
- let mut view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await;
+ let view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await;
if date.is_some() {
tracing::warn!("Cannot set date when appending message");
diff --git a/src/imap/command/mod.rs b/aero-proto/src/imap/command/mod.rs
index 073040e..5382d06 100644
--- a/src/imap/command/mod.rs
+++ b/aero-proto/src/imap/command/mod.rs
@@ -3,7 +3,7 @@ pub mod anystate;
pub mod authenticated;
pub mod selected;
-use crate::mail::user::INBOX;
+use aero_collections::mail::namespace::INBOX;
use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec;
/// Convert an IMAP mailbox name/identifier representation
diff --git a/src/imap/command/selected.rs b/aero-proto/src/imap/command/selected.rs
index d000905..190949b 100644
--- a/src/imap/command/selected.rs
+++ b/aero-proto/src/imap/command/selected.rs
@@ -3,7 +3,7 @@ use std::sync::Arc;
use anyhow::Result;
use imap_codec::imap_types::command::{Command, CommandBody, FetchModifier, StoreModifier};
-use imap_codec::imap_types::core::{Charset, Vec1};
+use imap_codec::imap_types::core::Charset;
use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames;
use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType};
use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec;
@@ -11,13 +11,14 @@ use imap_codec::imap_types::response::{Code, CodeOther};
use imap_codec::imap_types::search::SearchKey;
use imap_codec::imap_types::sequence::SequenceSet;
+use aero_collections::user::User;
+
use crate::imap::attributes::AttributesProxy;
use crate::imap::capability::{ClientCapability, ServerCapability};
use crate::imap::command::{anystate, authenticated, MailboxName};
use crate::imap::flow;
use crate::imap::mailbox_view::{MailboxView, UpdateParameters};
use crate::imap::response::Response;
-use crate::mail::user::User;
pub struct SelectedContext<'a> {
pub req: &'a Command<'static>,
diff --git a/src/imap/flags.rs b/aero-proto/src/imap/flags.rs
index 0f6ec64..0f6ec64 100644
--- a/src/imap/flags.rs
+++ b/aero-proto/src/imap/flags.rs
diff --git a/src/imap/flow.rs b/aero-proto/src/imap/flow.rs
index e372d69..1986447 100644
--- a/src/imap/flow.rs
+++ b/aero-proto/src/imap/flow.rs
@@ -5,8 +5,9 @@ use std::sync::Arc;
use imap_codec::imap_types::core::Tag;
use tokio::sync::Notify;
+use aero_collections::user::User;
+
use crate::imap::mailbox_view::MailboxView;
-use crate::mail::user::User;
#[derive(Debug)]
pub enum Error {
diff --git a/src/imap/imf_view.rs b/aero-proto/src/imap/imf_view.rs
index a4ca2e8..a4ca2e8 100644
--- a/src/imap/imf_view.rs
+++ b/aero-proto/src/imap/imf_view.rs
diff --git a/src/imap/index.rs b/aero-proto/src/imap/index.rs
index 9b794b8..afe6991 100644
--- a/src/imap/index.rs
+++ b/aero-proto/src/imap/index.rs
@@ -3,8 +3,8 @@ use std::num::{NonZeroU32, NonZeroU64};
use anyhow::{anyhow, Result};
use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet};
-use crate::mail::uidindex::{ImapUid, ModSeq, UidIndex};
-use crate::mail::unique_ident::UniqueIdent;
+use aero_collections::mail::uidindex::{ImapUid, ModSeq, UidIndex};
+use aero_collections::unique_ident::UniqueIdent;
pub struct Index<'a> {
pub imap_index: Vec<MailIndex<'a>>,
diff --git a/src/imap/mail_view.rs b/aero-proto/src/imap/mail_view.rs
index a8db733..054014a 100644
--- a/src/imap/mail_view.rs
+++ b/aero-proto/src/imap/mail_view.rs
@@ -16,7 +16,7 @@ use eml_codec::{
part::{composite::Message, AnyPart},
};
-use crate::mail::query::QueryResult;
+use aero_collections::mail::query::QueryResult;
use crate::imap::attributes::AttributesProxy;
use crate::imap::flags;
diff --git a/src/imap/mailbox_view.rs b/aero-proto/src/imap/mailbox_view.rs
index 1c53b93..0b808aa 100644
--- a/src/imap/mailbox_view.rs
+++ b/aero-proto/src/imap/mailbox_view.rs
@@ -6,18 +6,18 @@ use anyhow::{anyhow, Error, Result};
use futures::stream::{StreamExt, TryStreamExt};
-use imap_codec::imap_types::core::{Charset, Vec1};
+use imap_codec::imap_types::core::Charset;
use imap_codec::imap_types::fetch::MessageDataItem;
use imap_codec::imap_types::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType};
use imap_codec::imap_types::response::{Code, CodeOther, Data, Status};
use imap_codec::imap_types::search::SearchKey;
use imap_codec::imap_types::sequence::SequenceSet;
-use crate::mail::mailbox::Mailbox;
-use crate::mail::query::QueryScope;
-use crate::mail::snapshot::FrozenMailbox;
-use crate::mail::uidindex::{ImapUid, ImapUidvalidity, ModSeq};
-use crate::mail::unique_ident::UniqueIdent;
+use aero_collections::mail::mailbox::Mailbox;
+use aero_collections::mail::query::QueryScope;
+use aero_collections::mail::snapshot::FrozenMailbox;
+use aero_collections::mail::uidindex::{ImapUid, ImapUidvalidity, ModSeq};
+use aero_collections::unique_ident::UniqueIdent;
use crate::imap::attributes::AttributesProxy;
use crate::imap::flags;
@@ -638,13 +638,13 @@ mod tests {
use imap_codec::ResponseCodec;
use std::fs;
- use crate::cryptoblob;
+ use aero_collections::mail::mailbox::MailMeta;
+ use aero_collections::mail::query::QueryResult;
+ use aero_collections::unique_ident;
+ use aero_user::cryptoblob;
+
use crate::imap::index::MailIndex;
- use crate::imap::mail_view::MailView;
use crate::imap::mime_view;
- use crate::mail::mailbox::MailMeta;
- use crate::mail::query::QueryResult;
- use crate::mail::unique_ident;
#[test]
fn mailview_body_ext() -> Result<()> {
@@ -745,8 +745,8 @@ mod tests {
for pref in prefixes.iter() {
println!("{}", pref);
- let txt = fs::read(format!("{}.eml", pref))?;
- let oracle = fs::read(format!("{}.dovecot.body", pref))?;
+ let txt = fs::read(format!("../{}.eml", pref))?;
+ let oracle = fs::read(format!("../{}.dovecot.body", pref))?;
let message = eml_codec::parse_message(&txt).unwrap().1;
let test_repr = Response::Data(Data::Fetch {
diff --git a/src/imap/mime_view.rs b/aero-proto/src/imap/mime_view.rs
index 8bbbd2d..fd0f4b0 100644
--- a/src/imap/mime_view.rs
+++ b/aero-proto/src/imap/mime_view.rs
@@ -33,7 +33,7 @@ pub enum BodySection<'a> {
///
/// Example of message sections:
///
-/// ```
+/// ```text
/// HEADER ([RFC-2822] header of the message)
/// TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED
/// 1 TEXT/PLAIN
@@ -384,6 +384,8 @@ impl<'a> NodeMsg<'a> {
})
}
}
+
+#[allow(dead_code)]
struct NodeMult<'a>(&'a NodeMime<'a>, &'a composite::Multipart<'a>);
impl<'a> NodeMult<'a> {
fn structure(&self, is_ext: bool) -> Result<BodyStructure<'static>> {
diff --git a/src/imap/mod.rs b/aero-proto/src/imap/mod.rs
index 02ab9ce..6a768b0 100644
--- a/src/imap/mod.rs
+++ b/aero-proto/src/imap/mod.rs
@@ -15,26 +15,25 @@ mod session;
use std::net::SocketAddr;
-use anyhow::{anyhow, bail, Context, Result};
+use anyhow::{anyhow, bail, Result};
use futures::stream::{FuturesUnordered, StreamExt};
-
-use tokio::net::TcpListener;
-use tokio::sync::mpsc;
-use tokio::sync::watch;
-
use imap_codec::imap_types::response::{Code, CommandContinuationRequest, Response, Status};
use imap_codec::imap_types::{core::Text, response::Greeting};
use imap_flow::server::{ServerFlow, ServerFlowEvent, ServerFlowOptions};
use imap_flow::stream::AnyStream;
use rustls_pemfile::{certs, private_key};
+use tokio::net::TcpListener;
+use tokio::sync::mpsc;
+use tokio::sync::watch;
use tokio_rustls::TlsAcceptor;
-use crate::config::{ImapConfig, ImapUnsecureConfig};
+use aero_user::config::{ImapConfig, ImapUnsecureConfig};
+use aero_user::login::ArcLoginProvider;
+
use crate::imap::capability::ServerCapability;
use crate::imap::request::Request;
use crate::imap::response::{Body, ResponseOrIdle};
use crate::imap::session::Instance;
-use crate::login::ArcLoginProvider;
/// Server is a thin wrapper to register our Services in BàL
pub struct Server {
@@ -140,7 +139,6 @@ impl Server {
use std::sync::Arc;
use tokio::sync::mpsc::*;
use tokio::sync::Notify;
-use tokio_util::bytes::BytesMut;
const PIPELINABLE_COMMANDS: usize = 64;
@@ -325,8 +323,6 @@ impl NetLoop {
self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
tracing::error!("session task exited for {:?}, quitting", self.ctx.addr);
},
- Some(_) => unreachable!(),
-
},
// When receiving a CTRL+C
@@ -337,85 +333,4 @@ impl NetLoop {
};
}
}
-
- /*
- async fn idle_mode(&mut self, mut buff: BytesMut, stop: Arc<Notify>) -> Result<LoopMode> {
- // Flush send
- loop {
- tracing::trace!("flush server send");
- match self.server.progress_send().await? {
- Some(..) => continue,
- None => break,
- }
- }
-
- tokio::select! {
- // Receiving IDLE event from background
- maybe_msg = self.resp_rx.recv() => match maybe_msg {
- // Session decided idle is terminated
- Some(ResponseOrIdle::Response(response)) => {
- tracing::trace!("server imap session said idle is done, sending response done, switching to interactive");
- for body_elem in response.body.into_iter() {
- let _handle = match body_elem {
- Body::Data(d) => self.server.enqueue_data(d),
- Body::Status(s) => self.server.enqueue_status(s),
- };
- }
- self.server.enqueue_status(response.completion);
- return Ok(LoopMode::Interactive)
- },
- // Session has some information for user
- Some(ResponseOrIdle::IdleEvent(elems)) => {
- tracing::trace!("server imap session has some change to communicate to the client");
- for body_elem in elems.into_iter() {
- let _handle = match body_elem {
- Body::Data(d) => self.server.enqueue_data(d),
- Body::Status(s) => self.server.enqueue_status(s),
- };
- }
- self.cmd_tx.try_send(Request::Idle)?;
- return Ok(LoopMode::Idle(buff, stop))
- },
-
- // Session crashed
- None => {
- self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
- tracing::error!("session task exited for {:?}, quitting", self.ctx.addr);
- return Ok(LoopMode::Interactive)
- },
-
- // Session can't start idling while already idling, it's a logic error!
- Some(ResponseOrIdle::StartIdle(..)) => bail!("can't start idling while already idling!"),
- },
-
- // User is trying to interact with us
- read_client_result = self.server.stream.read(&mut buff) => {
- let _bytes_read = read_client_result?;
- use imap_codec::decode::Decoder;
- let codec = imap_codec::IdleDoneCodec::new();
- tracing::trace!("client sent some data for the server IMAP session");
- match codec.decode(&buff) {
- Ok(([], imap_codec::imap_types::extensions::idle::IdleDone)) => {
- // Session will be informed that it must stop idle
- // It will generate the "done" message and change the loop mode
- tracing::trace!("client sent DONE and want to stop IDLE");
- stop.notify_one()
- },
- Err(_) => {
- tracing::trace!("Unable to decode DONE, maybe not enough data were sent?");
- },
- _ => bail!("Client sent data after terminating the continuation without waiting for the server. This is an unsupported behavior and bug in Aerogramme, quitting."),
- };
-
- return Ok(LoopMode::Idle(buff, stop))
- },
-
- // When receiving a CTRL+C
- _ = self.ctx.must_exit.changed() => {
- tracing::trace!("CTRL+C sent, aborting IDLE for this session");
- self.server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap());
- return Ok(LoopMode::Interactive)
- },
- };
- }*/
}
diff --git a/src/imap/request.rs b/aero-proto/src/imap/request.rs
index cff18a3..cff18a3 100644
--- a/src/imap/request.rs
+++ b/aero-proto/src/imap/request.rs
diff --git a/src/imap/response.rs b/aero-proto/src/imap/response.rs
index b6a0e98..b6a0e98 100644
--- a/src/imap/response.rs
+++ b/aero-proto/src/imap/response.rs
diff --git a/src/imap/search.rs b/aero-proto/src/imap/search.rs
index 37a7e9e..3634a3a 100644
--- a/src/imap/search.rs
+++ b/aero-proto/src/imap/search.rs
@@ -4,9 +4,10 @@ use imap_codec::imap_types::core::Vec1;
use imap_codec::imap_types::search::{MetadataItemSearch, SearchKey};
use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet};
+use aero_collections::mail::query::QueryScope;
+
use crate::imap::index::MailIndex;
use crate::imap::mail_view::MailView;
-use crate::mail::query::QueryScope;
pub enum SeqType {
Undefined,
diff --git a/src/imap/session.rs b/aero-proto/src/imap/session.rs
index fa3232a..92b5eb6 100644
--- a/src/imap/session.rs
+++ b/aero-proto/src/imap/session.rs
@@ -1,11 +1,13 @@
+use anyhow::{anyhow, bail, Context, Result};
+use imap_codec::imap_types::{command::Command, core::Tag};
+
+use aero_user::login::ArcLoginProvider;
+
use crate::imap::capability::{ClientCapability, ServerCapability};
use crate::imap::command::{anonymous, authenticated, selected};
use crate::imap::flow;
use crate::imap::request::Request;
use crate::imap::response::{Response, ResponseOrIdle};
-use crate::login::ArcLoginProvider;
-use anyhow::{anyhow, bail, Context, Result};
-use imap_codec::imap_types::{command::Command, core::Tag};
//-----
pub struct Instance {
diff --git a/aero-proto/src/lib.rs b/aero-proto/src/lib.rs
new file mode 100644
index 0000000..d5154cd
--- /dev/null
+++ b/aero-proto/src/lib.rs
@@ -0,0 +1,6 @@
+#![feature(async_closure)]
+
+pub mod dav;
+pub mod imap;
+pub mod lmtp;
+pub mod sasl;
diff --git a/src/lmtp.rs b/aero-proto/src/lmtp.rs
index dcd4bcc..a82a783 100644
--- a/src/lmtp.rs
+++ b/aero-proto/src/lmtp.rs
@@ -10,18 +10,16 @@ use futures::{
stream::{FuturesOrdered, FuturesUnordered},
StreamExt,
};
-use log::*;
+use smtp_message::{DataUnescaper, Email, EscapedDataReader, Reply, ReplyCode};
+use smtp_server::{reply, Config, ConnectionMetadata, Decision, MailMetadata};
use tokio::net::TcpListener;
use tokio::select;
use tokio::sync::watch;
use tokio_util::compat::*;
-use smtp_message::{DataUnescaper, Email, EscapedDataReader, Reply, ReplyCode};
-use smtp_server::{reply, Config, ConnectionMetadata, Decision, MailMetadata};
-
-use crate::config::*;
-use crate::login::*;
-use crate::mail::incoming::EncryptedMessage;
+use aero_collections::mail::incoming::EncryptedMessage;
+use aero_user::config::*;
+use aero_user::login::*;
pub struct LmtpServer {
bind_addr: SocketAddr,
@@ -43,7 +41,7 @@ impl LmtpServer {
pub async fn run(self: &Arc<Self>, mut must_exit: watch::Receiver<bool>) -> Result<()> {
let tcp = TcpListener::bind(self.bind_addr).await?;
- info!("LMTP server listening on {:#}", self.bind_addr);
+ tracing::info!("LMTP server listening on {:#}", self.bind_addr);
let mut connections = FuturesUnordered::new();
@@ -60,7 +58,7 @@ impl LmtpServer {
_ = wait_conn_finished => continue,
_ = must_exit.changed() => continue,
};
- info!("LMTP: accepted connection from {}", remote_addr);
+ tracing::info!("LMTP: accepted connection from {}", remote_addr);
let conn = tokio::spawn(smtp_server::interact(
socket.compat(),
@@ -73,7 +71,7 @@ impl LmtpServer {
}
drop(tcp);
- info!("LMTP server shutting down, draining remaining connections...");
+ tracing::info!("LMTP server shutting down, draining remaining connections...");
while connections.next().await.is_some() {}
Ok(())
diff --git a/aero-proto/src/sasl.rs b/aero-proto/src/sasl.rs
new file mode 100644
index 0000000..48c0815
--- /dev/null
+++ b/aero-proto/src/sasl.rs
@@ -0,0 +1,142 @@
+use std::net::SocketAddr;
+
+use anyhow::{anyhow, bail, Result};
+use futures::stream::{FuturesUnordered, StreamExt};
+use tokio::io::BufStream;
+use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
+use tokio::net::{TcpListener, TcpStream};
+use tokio::sync::watch;
+use tokio_util::bytes::BytesMut;
+
+use aero_sasl::{decode::client_command, encode::Encode, flow::State};
+use aero_user::config::AuthConfig;
+use aero_user::login::ArcLoginProvider;
+
+pub struct AuthServer {
+ login_provider: ArcLoginProvider,
+ bind_addr: SocketAddr,
+}
+
+impl AuthServer {
+ pub fn new(config: AuthConfig, login_provider: ArcLoginProvider) -> Self {
+ Self {
+ bind_addr: config.bind_addr,
+ login_provider,
+ }
+ }
+
+ pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
+ let tcp = TcpListener::bind(self.bind_addr).await?;
+ tracing::info!(
+ "SASL Authentication Protocol listening on {:#}",
+ self.bind_addr
+ );
+
+ let mut connections = FuturesUnordered::new();
+
+ while !*must_exit.borrow() {
+ let wait_conn_finished = async {
+ if connections.is_empty() {
+ futures::future::pending().await
+ } else {
+ connections.next().await
+ }
+ };
+
+ let (socket, remote_addr) = tokio::select! {
+ a = tcp.accept() => a?,
+ _ = wait_conn_finished => continue,
+ _ = must_exit.changed() => continue,
+ };
+
+ tracing::info!("AUTH: accepted connection from {}", remote_addr);
+ let conn = tokio::spawn(
+ NetLoop::new(socket, self.login_provider.clone(), must_exit.clone()).run_error(),
+ );
+
+ connections.push(conn);
+ }
+ drop(tcp);
+
+ tracing::info!("AUTH server shutting down, draining remaining connections...");
+ while connections.next().await.is_some() {}
+
+ Ok(())
+ }
+}
+
+struct NetLoop {
+ login: ArcLoginProvider,
+ stream: BufStream<TcpStream>,
+ stop: watch::Receiver<bool>,
+ state: State,
+ read_buf: Vec<u8>,
+ write_buf: BytesMut,
+}
+
+impl NetLoop {
+ fn new(stream: TcpStream, login: ArcLoginProvider, stop: watch::Receiver<bool>) -> Self {
+ Self {
+ login,
+ stream: BufStream::new(stream),
+ state: State::Init,
+ stop,
+ read_buf: Vec::new(),
+ write_buf: BytesMut::new(),
+ }
+ }
+
+ async fn run_error(self) {
+ match self.run().await {
+ Ok(()) => tracing::info!("Auth session succeeded"),
+ Err(e) => tracing::error!(err=?e, "Auth session failed"),
+ }
+ }
+
+ async fn run(mut self) -> Result<()> {
+ loop {
+ tokio::select! {
+ read_res = self.stream.read_until(b'\n', &mut self.read_buf) => {
+ // Detect EOF / socket close
+ let bread = read_res?;
+ if bread == 0 {
+ tracing::info!("Reading buffer empty, connection has been closed. Exiting AUTH session.");
+ return Ok(())
+ }
+
+ // Parse command
+ let (_, cmd) = client_command(&self.read_buf).map_err(|_| anyhow!("Unable to parse command"))?;
+ tracing::trace!(cmd=?cmd, "Received command");
+
+ // Make some progress in our local state
+ let login = async |user: String, pass: String| self.login.login(user.as_str(), pass.as_str()).await.is_ok();
+ self.state.progress(cmd, login).await;
+ if matches!(self.state, State::Error) {
+ bail!("Internal state is in error, previous logs explain what went wrong");
+ }
+
+ // Build response
+ let srv_cmds = self.state.response();
+ srv_cmds.iter().try_for_each(|r| {
+ tracing::trace!(cmd=?r, "Sent command");
+ r.encode(&mut self.write_buf)
+ })?;
+
+ // Send responses if at least one command response has been generated
+ if !srv_cmds.is_empty() {
+ self.stream.write_all(&self.write_buf).await?;
+ self.stream.flush().await?;
+ }
+
+ // Reset buffers
+ self.read_buf.clear();
+ self.write_buf.clear();
+ },
+ _ = self.stop.changed() => {
+ tracing::debug!("Server is stopping, quitting this runner");
+ return Ok(())
+ }
+ }
+ }
+ }
+}
diff --git a/aero-sasl/Cargo.toml b/aero-sasl/Cargo.toml
new file mode 100644
index 0000000..3e66ff3
--- /dev/null
+++ b/aero-sasl/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "aero-sasl"
+version = "0.3.0"
+authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
+edition = "2021"
+license = "EUPL-1.2"
+description = "A partial and standalone implementation of the Dovecot SASL Auth Protocol"
+
+[dependencies]
+
+anyhow.workspace = true
+base64.workspace = true
+futures.workspace = true
+nom.workspace = true
+rand.workspace = true
+tokio.workspace = true
+tokio-util.workspace = true
+tracing.workspace = true
+hex.workspace = true
+
+#log.workspace = true
+#serde.workspace = true
diff --git a/aero-sasl/src/decode.rs b/aero-sasl/src/decode.rs
new file mode 100644
index 0000000..f5d7b53
--- /dev/null
+++ b/aero-sasl/src/decode.rs
@@ -0,0 +1,243 @@
+use base64::Engine;
+use nom::{
+ branch::alt,
+ bytes::complete::{tag, tag_no_case, take, take_while, take_while1},
+ character::complete::{tab, u16, u64},
+ combinator::{map, opt, recognize, rest, value},
+ error::{Error, ErrorKind},
+ multi::{many1, separated_list0},
+ sequence::{pair, preceded, tuple},
+ IResult,
+};
+
+use super::types::*;
+
+pub fn client_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
+ alt((version_command, cpid_command, auth_command, cont_command))(input)
+}
+
+/*
+fn server_command(buf: &u8) -> IResult<&u8, ServerCommand> {
+ unimplemented!();
+}
+*/
+
+// ---------------------
+
+fn version_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
+ let mut parser = tuple((tag_no_case(b"VERSION"), tab, u64, tab, u64));
+
+ let (input, (_, _, major, _, minor)) = parser(input)?;
+ Ok((input, ClientCommand::Version(Version { major, minor })))
+}
+
+pub fn cpid_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
+ preceded(
+ pair(tag_no_case(b"CPID"), tab),
+ map(u64, |v| ClientCommand::Cpid(v)),
+ )(input)
+}
+
+fn mechanism<'a>(input: &'a [u8]) -> IResult<&'a [u8], Mechanism> {
+ alt((
+ value(Mechanism::Plain, tag_no_case(b"PLAIN")),
+ value(Mechanism::Login, tag_no_case(b"LOGIN")),
+ ))(input)
+}
+
+fn is_not_tab_or_esc_or_lf(c: u8) -> bool {
+ c != 0x09 && c != 0x01 && c != 0x0a // TAB or 0x01 or LF
+}
+
+fn is_esc<'a>(input: &'a [u8]) -> IResult<&'a [u8], &[u8]> {
+ preceded(tag(&[0x01]), take(1usize))(input)
+}
+
+fn parameter<'a>(input: &'a [u8]) -> IResult<&'a [u8], &[u8]> {
+ recognize(many1(alt((take_while1(is_not_tab_or_esc_or_lf), is_esc))))(input)
+}
+
+fn parameter_str(input: &[u8]) -> IResult<&[u8], String> {
+ let (input, buf) = parameter(input)?;
+
+ std::str::from_utf8(buf)
+ .map(|v| (input, v.to_string()))
+ .map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))
+}
+
+fn is_param_name_char(c: u8) -> bool {
+ is_not_tab_or_esc_or_lf(c) && c != 0x3d // =
+}
+
+fn parameter_name(input: &[u8]) -> IResult<&[u8], String> {
+ let (input, buf) = take_while1(is_param_name_char)(input)?;
+
+ std::str::from_utf8(buf)
+ .map(|v| (input, v.to_string()))
+ .map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))
+}
+
+fn service<'a>(input: &'a [u8]) -> IResult<&'a [u8], String> {
+ preceded(tag_no_case("service="), parameter_str)(input)
+}
+
+fn auth_option<'a>(input: &'a [u8]) -> IResult<&'a [u8], AuthOption> {
+ use AuthOption::*;
+ alt((
+ alt((
+ value(Debug, tag_no_case(b"debug")),
+ value(NoPenalty, tag_no_case(b"no-penalty")),
+ value(ClientId, tag_no_case(b"client_id")),
+ value(NoLogin, tag_no_case(b"nologin")),
+ map(preceded(tag_no_case(b"session="), u64), |id| Session(id)),
+ map(preceded(tag_no_case(b"lip="), parameter_str), |ip| {
+ LocalIp(ip)
+ }),
+ map(preceded(tag_no_case(b"rip="), parameter_str), |ip| {
+ RemoteIp(ip)
+ }),
+ map(preceded(tag_no_case(b"lport="), u16), |port| {
+ LocalPort(port)
+ }),
+ map(preceded(tag_no_case(b"rport="), u16), |port| {
+ RemotePort(port)
+ }),
+ map(preceded(tag_no_case(b"real_rip="), parameter_str), |ip| {
+ RealRemoteIp(ip)
+ }),
+ map(preceded(tag_no_case(b"real_lip="), parameter_str), |ip| {
+ RealLocalIp(ip)
+ }),
+ map(preceded(tag_no_case(b"real_lport="), u16), |port| {
+ RealLocalPort(port)
+ }),
+ map(preceded(tag_no_case(b"real_rport="), u16), |port| {
+ RealRemotePort(port)
+ }),
+ )),
+ alt((
+ map(
+ preceded(tag_no_case(b"local_name="), parameter_str),
+ |name| LocalName(name),
+ ),
+ map(
+ preceded(tag_no_case(b"forward_views="), parameter),
+ |views| ForwardViews(views.into()),
+ ),
+ map(preceded(tag_no_case(b"secured="), parameter_str), |info| {
+ Secured(Some(info))
+ }),
+ value(Secured(None), tag_no_case(b"secured")),
+ value(CertUsername, tag_no_case(b"cert_username")),
+ map(preceded(tag_no_case(b"transport="), parameter_str), |ts| {
+ Transport(ts)
+ }),
+ map(
+ preceded(tag_no_case(b"tls_cipher="), parameter_str),
+ |cipher| TlsCipher(cipher),
+ ),
+ map(
+ preceded(tag_no_case(b"tls_cipher_bits="), parameter_str),
+ |bits| TlsCipherBits(bits),
+ ),
+ map(preceded(tag_no_case(b"tls_pfs="), parameter_str), |pfs| {
+ TlsPfs(pfs)
+ }),
+ map(
+ preceded(tag_no_case(b"tls_protocol="), parameter_str),
+ |proto| TlsProtocol(proto),
+ ),
+ map(
+ preceded(tag_no_case(b"valid-client-cert="), parameter_str),
+ |cert| ValidClientCert(cert),
+ ),
+ )),
+ alt((
+ map(preceded(tag_no_case(b"resp="), base64), |data| Resp(data)),
+ map(
+ tuple((parameter_name, tag(b"="), parameter)),
+ |(n, _, v)| UnknownPair(n, v.into()),
+ ),
+ map(parameter, |v| UnknownBool(v.into())),
+ )),
+ ))(input)
+}
+
+fn auth_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
+ let mut parser = tuple((
+ tag_no_case(b"AUTH"),
+ tab,
+ u64,
+ tab,
+ mechanism,
+ tab,
+ service,
+ map(opt(preceded(tab, separated_list0(tab, auth_option))), |o| {
+ o.unwrap_or(vec![])
+ }),
+ ));
+ let (input, (_, _, id, _, mech, _, service, options)) = parser(input)?;
+ Ok((
+ input,
+ ClientCommand::Auth {
+ id,
+ mech,
+ service,
+ options,
+ },
+ ))
+}
+
+fn is_base64_core(c: u8) -> bool {
+ c >= 0x30 && c <= 0x39 // 0-9
+ || c >= 0x41 && c <= 0x5a // A-Z
+ || c >= 0x61 && c <= 0x7a // a-z
+ || c == 0x2b // +
+ || c == 0x2f // /
+}
+
+fn is_base64_pad(c: u8) -> bool {
+ c == 0x3d // =
+}
+
+fn base64(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
+ let (input, (b64, _)) = tuple((take_while1(is_base64_core), take_while(is_base64_pad)))(input)?;
+
+ let data = base64::engine::general_purpose::STANDARD_NO_PAD
+ .decode(b64)
+ .map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))?;
+
+ Ok((input, data))
+}
+
+/// @FIXME Dovecot does not say if base64 content must be padded or not
+fn cont_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
+ let mut parser = tuple((tag_no_case(b"CONT"), tab, u64, tab, base64));
+
+ let (input, (_, _, id, _, data)) = parser(input)?;
+ Ok((input, ClientCommand::Cont { id, data }))
+}
+
+// -----------------------------------------------------------------
+//
+// SASL DECODING
+//
+// -----------------------------------------------------------------
+
+fn not_null(c: u8) -> bool {
+ c != 0x0
+}
+
+// impersonated user, login, password
+pub fn auth_plain<'a>(input: &'a [u8]) -> IResult<&'a [u8], (&'a [u8], &'a [u8], &'a [u8])> {
+ map(
+ tuple((
+ take_while(not_null),
+ take(1usize),
+ take_while(not_null),
+ take(1usize),
+ rest,
+ )),
+ |(imp, _, user, _, pass)| (imp, user, pass),
+ )(input)
+}
diff --git a/aero-sasl/src/encode.rs b/aero-sasl/src/encode.rs
new file mode 100644
index 0000000..625d035
--- /dev/null
+++ b/aero-sasl/src/encode.rs
@@ -0,0 +1,157 @@
+use anyhow::Result;
+use base64::Engine;
+use tokio_util::bytes::{BufMut, BytesMut};
+
+use super::types::*;
+
+pub trait Encode {
+ fn encode(&self, out: &mut BytesMut) -> Result<()>;
+}
+
+fn tab_enc(out: &mut BytesMut) {
+ out.put(&[0x09][..])
+}
+
+fn lf_enc(out: &mut BytesMut) {
+ out.put(&[0x0A][..])
+}
+
+impl Encode for Mechanism {
+ fn encode(&self, out: &mut BytesMut) -> Result<()> {
+ match self {
+ Self::Plain => out.put(&b"PLAIN"[..]),
+ Self::Login => out.put(&b"LOGIN"[..]),
+ }
+ Ok(())
+ }
+}
+
+impl Encode for MechanismParameters {
+ fn encode(&self, out: &mut BytesMut) -> Result<()> {
+ match self {
+ Self::Anonymous => out.put(&b"anonymous"[..]),
+ Self::PlainText => out.put(&b"plaintext"[..]),
+ Self::Dictionary => out.put(&b"dictionary"[..]),
+ Self::Active => out.put(&b"active"[..]),
+ Self::ForwardSecrecy => out.put(&b"forward-secrecy"[..]),
+ Self::MutualAuth => out.put(&b"mutual-auth"[..]),
+ Self::Private => out.put(&b"private"[..]),
+ }
+ Ok(())
+ }
+}
+
+impl Encode for FailCode {
+ fn encode(&self, out: &mut BytesMut) -> Result<()> {
+ match self {
+ Self::TempFail => out.put(&b"temp_fail"[..]),
+ Self::AuthzFail => out.put(&b"authz_fail"[..]),
+ Self::UserDisabled => out.put(&b"user_disabled"[..]),
+ Self::PassExpired => out.put(&b"pass_expired"[..]),
+ };
+ Ok(())
+ }
+}
+
+impl Encode for ServerCommand {
+ fn encode(&self, out: &mut BytesMut) -> Result<()> {
+ match self {
+ Self::Version(Version { major, minor }) => {
+ out.put(&b"VERSION"[..]);
+ tab_enc(out);
+ out.put(major.to_string().as_bytes());
+ tab_enc(out);
+ out.put(minor.to_string().as_bytes());
+ lf_enc(out);
+ }
+ Self::Spid(pid) => {
+ out.put(&b"SPID"[..]);
+ tab_enc(out);
+ out.put(pid.to_string().as_bytes());
+ lf_enc(out);
+ }
+ Self::Cuid(pid) => {
+ out.put(&b"CUID"[..]);
+ tab_enc(out);
+ out.put(pid.to_string().as_bytes());
+ lf_enc(out);
+ }
+ Self::Cookie(cval) => {
+ out.put(&b"COOKIE"[..]);
+ tab_enc(out);
+ out.put(hex::encode(cval).as_bytes());
+ lf_enc(out);
+ }
+ Self::Mech { kind, parameters } => {
+ out.put(&b"MECH"[..]);
+ tab_enc(out);
+ kind.encode(out)?;
+ for p in parameters.iter() {
+ tab_enc(out);
+ p.encode(out)?;
+ }
+ lf_enc(out);
+ }
+ Self::Done => {
+ out.put(&b"DONE"[..]);
+ lf_enc(out);
+ }
+ Self::Cont { id, data } => {
+ out.put(&b"CONT"[..]);
+ tab_enc(out);
+ out.put(id.to_string().as_bytes());
+ tab_enc(out);
+ if let Some(rdata) = data {
+ let b64 = base64::engine::general_purpose::STANDARD.encode(rdata);
+ out.put(b64.as_bytes());
+ }
+ lf_enc(out);
+ }
+ Self::Ok {
+ id,
+ user_id,
+ extra_parameters,
+ } => {
+ out.put(&b"OK"[..]);
+ tab_enc(out);
+ out.put(id.to_string().as_bytes());
+ if let Some(user) = user_id {
+ tab_enc(out);
+ out.put(&b"user="[..]);
+ out.put(user.as_bytes());
+ }
+ for p in extra_parameters.iter() {
+ tab_enc(out);
+ out.put(&p[..]);
+ }
+ lf_enc(out);
+ }
+ Self::Fail {
+ id,
+ user_id,
+ code,
+ extra_parameters,
+ } => {
+ out.put(&b"FAIL"[..]);
+ tab_enc(out);
+ out.put(id.to_string().as_bytes());
+ if let Some(user) = user_id {
+ tab_enc(out);
+ out.put(&b"user="[..]);
+ out.put(user.as_bytes());
+ }
+ if let Some(code_val) = code {
+ tab_enc(out);
+ out.put(&b"code="[..]);
+ code_val.encode(out)?;
+ }
+ for p in extra_parameters.iter() {
+ tab_enc(out);
+ out.put(&p[..]);
+ }
+ lf_enc(out);
+ }
+ }
+ Ok(())
+ }
+}
diff --git a/aero-sasl/src/flow.rs b/aero-sasl/src/flow.rs
new file mode 100644
index 0000000..5aa4869
--- /dev/null
+++ b/aero-sasl/src/flow.rs
@@ -0,0 +1,201 @@
+use futures::Future;
+use rand::prelude::*;
+
+use super::decode::auth_plain;
+use super::types::*;
+
+#[derive(Debug)]
+pub enum AuthRes {
+ Success(String),
+ Failed(Option<String>, Option<FailCode>),
+}
+
+#[derive(Debug)]
+pub enum State {
+ Error,
+ Init,
+ HandshakePart(Version),
+ HandshakeDone,
+ AuthPlainProgress { id: u64 },
+ AuthDone { id: u64, res: AuthRes },
+}
+
+const SERVER_MAJOR: u64 = 1;
+const SERVER_MINOR: u64 = 2;
+const EMPTY_AUTHZ: &[u8] = &[];
+impl State {
+ pub fn new() -> Self {
+ Self::Init
+ }
+
+ async fn try_auth_plain<X, F>(&self, data: &[u8], login: X) -> AuthRes
+ where
+ X: FnOnce(String, String) -> F,
+ F: Future<Output = bool>,
+ {
+ // Check that we can extract user's login+pass
+ let (ubin, pbin) = match auth_plain(&data) {
+ Ok(([], (authz, user, pass))) if authz == user || authz == EMPTY_AUTHZ => (user, pass),
+ Ok(_) => {
+ tracing::error!("Impersonating user is not supported");
+ return AuthRes::Failed(None, None);
+ }
+ Err(e) => {
+ tracing::error!(err=?e, "Could not parse the SASL PLAIN data chunk");
+ return AuthRes::Failed(None, None);
+ }
+ };
+
+ // Try to convert it to UTF-8
+ let (user, password) = match (std::str::from_utf8(ubin), std::str::from_utf8(pbin)) {
+ (Ok(u), Ok(p)) => (u, p),
+ _ => {
+ tracing::error!("Username or password contain invalid UTF-8 characters");
+ return AuthRes::Failed(None, None);
+ }
+ };
+
+ // Try to connect user
+ match login(user.to_string(), password.to_string()).await {
+ true => AuthRes::Success(user.to_string()),
+ false => {
+ tracing::warn!("login failed");
+ AuthRes::Failed(Some(user.to_string()), None)
+ }
+ }
+ }
+
+ pub async fn progress<F, X>(&mut self, cmd: ClientCommand, login: X)
+ where
+ X: FnOnce(String, String) -> F,
+ F: Future<Output = bool>,
+ {
+ let new_state = 'state: {
+ match (std::mem::replace(self, State::Error), cmd) {
+ (Self::Init, ClientCommand::Version(v)) => Self::HandshakePart(v),
+ (Self::HandshakePart(version), ClientCommand::Cpid(_cpid)) => {
+ if version.major != SERVER_MAJOR {
+ tracing::error!(
+ client_major = version.major,
+ server_major = SERVER_MAJOR,
+ "Unsupported client major version"
+ );
+ break 'state Self::Error;
+ }
+
+ Self::HandshakeDone
+ }
+ (
+ Self::HandshakeDone { .. },
+ ClientCommand::Auth {
+ id, mech, options, ..
+ },
+ )
+ | (
+ Self::AuthDone { .. },
+ ClientCommand::Auth {
+ id, mech, options, ..
+ },
+ ) => {
+ if mech != Mechanism::Plain {
+ tracing::error!(mechanism=?mech, "Unsupported Authentication Mechanism");
+ break 'state Self::AuthDone {
+ id,
+ res: AuthRes::Failed(None, None),
+ };
+ }
+
+ match options.last() {
+ Some(AuthOption::Resp(data)) => Self::AuthDone {
+ id,
+ res: self.try_auth_plain(&data, login).await,
+ },
+ _ => Self::AuthPlainProgress { id },
+ }
+ }
+ (Self::AuthPlainProgress { id }, ClientCommand::Cont { id: cid, data }) => {
+ // Check that ID matches
+ if cid != id {
+ tracing::error!(
+ auth_id = id,
+ cont_id = cid,
+ "CONT id does not match AUTH id"
+ );
+ break 'state Self::AuthDone {
+ id,
+ res: AuthRes::Failed(None, None),
+ };
+ }
+
+ Self::AuthDone {
+ id,
+ res: self.try_auth_plain(&data, login).await,
+ }
+ }
+ _ => {
+ tracing::error!("This command is not valid in this context");
+ Self::Error
+ }
+ }
+ };
+ tracing::debug!(state=?new_state, "Made progress");
+ *self = new_state;
+ }
+
+ pub fn response(&self) -> Vec<ServerCommand> {
+ let mut srv_cmd: Vec<ServerCommand> = Vec::new();
+
+ match self {
+ Self::HandshakeDone { .. } => {
+ srv_cmd.push(ServerCommand::Version(Version {
+ major: SERVER_MAJOR,
+ minor: SERVER_MINOR,
+ }));
+
+ srv_cmd.push(ServerCommand::Mech {
+ kind: Mechanism::Plain,
+ parameters: vec![MechanismParameters::PlainText],
+ });
+
+ srv_cmd.push(ServerCommand::Spid(15u64));
+ srv_cmd.push(ServerCommand::Cuid(19350u64));
+
+ let mut cookie = [0u8; 16];
+ thread_rng().fill(&mut cookie);
+ srv_cmd.push(ServerCommand::Cookie(cookie));
+
+ srv_cmd.push(ServerCommand::Done);
+ }
+ Self::AuthPlainProgress { id } => {
+ srv_cmd.push(ServerCommand::Cont {
+ id: *id,
+ data: None,
+ });
+ }
+ Self::AuthDone {
+ id,
+ res: AuthRes::Success(user),
+ } => {
+ srv_cmd.push(ServerCommand::Ok {
+ id: *id,
+ user_id: Some(user.to_string()),
+ extra_parameters: vec![],
+ });
+ }
+ Self::AuthDone {
+ id,
+ res: AuthRes::Failed(maybe_user, maybe_failcode),
+ } => {
+ srv_cmd.push(ServerCommand::Fail {
+ id: *id,
+ user_id: maybe_user.clone(),
+ code: maybe_failcode.clone(),
+ extra_parameters: vec![],
+ });
+ }
+ _ => (),
+ };
+
+ srv_cmd
+ }
+}
diff --git a/aero-sasl/src/lib.rs b/aero-sasl/src/lib.rs
new file mode 100644
index 0000000..fdaa8a7
--- /dev/null
+++ b/aero-sasl/src/lib.rs
@@ -0,0 +1,43 @@
+pub mod decode;
+pub mod encode;
+pub mod flow;
+/// Seek compatibility with the Dovecot Authentication Protocol
+///
+/// ## Trace
+///
+/// ```text
+/// S: VERSION 1 2
+/// S: MECH PLAIN plaintext
+/// S: MECH LOGIN plaintext
+/// S: SPID 15
+/// S: CUID 17654
+/// S: COOKIE f56692bee41f471ed01bd83520025305
+/// S: DONE
+/// C: VERSION 1 2
+/// C: CPID 1
+///
+/// C: AUTH 2 PLAIN service=smtp
+/// S: CONT 2
+/// C: CONT 2 base64stringFollowingRFC4616==
+/// S: OK 2 user=alice@example.tld
+///
+/// C: AUTH 42 LOGIN service=smtp
+/// S: CONT 42 VXNlcm5hbWU6
+/// C: CONT 42 b64User
+/// S: CONT 42 UGFzc3dvcmQ6
+/// C: CONT 42 b64Pass
+/// S: FAIL 42 user=alice
+/// ```
+///
+/// ## RFC References
+///
+/// PLAIN SASL - https://datatracker.ietf.org/doc/html/rfc4616
+///
+///
+/// ## Dovecot References
+///
+/// https://doc.dovecot.org/developer_manual/design/auth_protocol/
+/// https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms
+/// https://doc.dovecot.org/configuration_manual/howto/simple_virtual_install/#simple-virtual-install-smtp-auth
+/// https://doc.dovecot.org/configuration_manual/howto/postfix_and_dovecot_sasl/#howto-postfix-and-dovecot-sasl
+pub mod types;
diff --git a/aero-sasl/src/types.rs b/aero-sasl/src/types.rs
new file mode 100644
index 0000000..2686677
--- /dev/null
+++ b/aero-sasl/src/types.rs
@@ -0,0 +1,161 @@
+#[derive(Debug, Clone, PartialEq)]
+pub enum Mechanism {
+ Plain,
+ Login,
+}
+
+#[derive(Clone, Debug)]
+pub enum AuthOption {
+ /// Unique session ID. Mainly used for logging.
+ Session(u64),
+ /// Local IP connected to by the client. In standard string format, e.g. 127.0.0.1 or ::1.
+ LocalIp(String),
+ /// Remote client IP
+ RemoteIp(String),
+ /// Local port connected to by the client.
+ LocalPort(u16),
+ /// Remote client port
+ RemotePort(u16),
+ /// When Dovecot proxy is used, the real_rip/real_port are the proxy’s IP/port and real_lip/real_lport are the backend’s IP/port where the proxy was connected to.
+ RealRemoteIp(String),
+ RealLocalIp(String),
+ RealLocalPort(u16),
+ RealRemotePort(u16),
+ /// TLS SNI name
+ LocalName(String),
+ /// Enable debugging for this lookup.
+ Debug,
+ /// List of fields that will become available via %{forward_*} variables. The list is double-tab-escaped, like: tab_escaped[tab_escaped(key=value)[<TAB>...]
+ /// Note: we do not unescape the tabulation, and thus we don't parse the data
+ ForwardViews(Vec<u8>),
+ /// Remote user has secured transport to auth client (e.g. localhost, SSL, TLS).
+ Secured(Option<String>),
+ /// The value can be “insecure”, “trusted” or “TLS”.
+ Transport(String),
+ /// TLS cipher being used.
+ TlsCipher(String),
+ /// The number of bits in the TLS cipher.
+ /// @FIXME: I don't know how if it's a string or an integer
+ TlsCipherBits(String),
+ /// TLS perfect forward secrecy algorithm (e.g. DH, ECDH)
+ TlsPfs(String),
+ /// TLS protocol name (e.g. SSLv3, TLSv1.2)
+ TlsProtocol(String),
+ /// Remote user has presented a valid SSL certificate.
+ ValidClientCert(String),
+ /// Ignore auth penalty tracking for this request
+ NoPenalty,
+ /// Unknown option sent by Postfix
+ NoLogin,
+ /// Username taken from client’s SSL certificate.
+ CertUsername,
+ /// IMAP ID string
+ ClientId,
+ /// An unknown key
+ UnknownPair(String, Vec<u8>),
+ UnknownBool(Vec<u8>),
+ /// Initial response for authentication mechanism.
+ /// NOTE: This must be the last parameter. Everything after it is ignored.
+ /// This is to avoid accidental security holes if user-given data is directly put to base64 string without filtering out tabs.
+ /// **This field is used when the data to pass is small, it's a way to "inline a continuation".
+ Resp(Vec<u8>),
+}
+
+#[derive(Debug, Clone)]
+pub struct Version {
+ pub major: u64,
+ pub minor: u64,
+}
+
+#[derive(Debug)]
+pub enum ClientCommand {
+ /// Both client and server should check that they support the same major version number. If they don’t, the other side isn’t expected to be talking the same protocol and should be disconnected. Minor version can be ignored. This document specifies the version number 1.2.
+ Version(Version),
+ /// CPID finishes the handshake from client.
+ Cpid(u64),
+ Auth {
+ /// ID is a connection-specific unique request identifier. It must be a 32bit number, so typically you’d just increment it by one.
+ id: u64,
+ /// A SASL mechanism (eg. LOGIN, PLAIN, etc.)
+ /// See: https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms
+ mech: Mechanism,
+ /// Service is the service requesting authentication, eg. pop3, imap, smtp.
+ service: String,
+ /// All the optional parameters
+ options: Vec<AuthOption>,
+ },
+ Cont {
+ /// The <id> must match the <id> of the AUTH command.
+ id: u64,
+ /// Data that will be serialized to / deserialized from base64
+ data: Vec<u8>,
+ },
+}
+
+#[derive(Debug)]
+pub enum MechanismParameters {
+ /// Anonymous authentication
+ Anonymous,
+ /// Transfers plaintext passwords
+ PlainText,
+ /// Subject to passive (dictionary) attack
+ Dictionary,
+ /// Subject to active (non-dictionary) attack
+ Active,
+ /// Provides forward secrecy between sessions
+ ForwardSecrecy,
+ /// Provides mutual authentication
+ MutualAuth,
+ /// Don’t advertise this as available SASL mechanism (eg. APOP)
+ Private,
+}
+
+#[derive(Debug, Clone)]
+pub enum FailCode {
+ /// This is a temporary internal failure, e.g. connection was lost to SQL database.
+ TempFail,
+ /// Authentication succeeded, but authorization failed (master user’s password was ok, but destination user was not ok).
+ AuthzFail,
+ /// User is disabled (password may or may not have been correct)
+ UserDisabled,
+ /// User’s password has expired.
+ PassExpired,
+}
+
+#[derive(Debug)]
+pub enum ServerCommand {
+ /// Both client and server should check that they support the same major version number. If they don’t, the other side isn’t expected to be talking the same protocol and should be disconnected. Minor version can be ignored. This document specifies the version number 1.2.
+ Version(Version),
+ /// CPID and SPID specify client and server Process Identifiers (PIDs). They should be unique identifiers for the specific process. UNIX process IDs are good choices.
+ /// SPID can be used by authentication client to tell master which server process handled the authentication.
+ Spid(u64),
+ /// CUID is a server process-specific unique connection identifier. It’s different each time a connection is established for the server.
+ /// CUID is currently useful only for APOP authentication.
+ Cuid(u64),
+ Mech {
+ kind: Mechanism,
+ parameters: Vec<MechanismParameters>,
+ },
+ /// COOKIE returns connection-specific 128 bit cookie in hex. It must be given to REQUEST command. (Protocol v1.1+ / Dovecot v2.0+)
+ Cookie([u8; 16]),
+ /// DONE finishes the handshake from server.
+ Done,
+
+ Fail {
+ id: u64,
+ user_id: Option<String>,
+ code: Option<FailCode>,
+ extra_parameters: Vec<Vec<u8>>,
+ },
+ Cont {
+ id: u64,
+ data: Option<Vec<u8>>,
+ },
+ /// FAIL and OK may contain multiple unspecified parameters which authentication client may handle specially.
+ /// The only one specified here is user=<userid> parameter, which should always be sent if the userid is known.
+ Ok {
+ id: u64,
+ user_id: Option<String>,
+ extra_parameters: Vec<Vec<u8>>,
+ },
+}
diff --git a/aero-user/Cargo.toml b/aero-user/Cargo.toml
new file mode 100644
index 0000000..fc851e2
--- /dev/null
+++ b/aero-user/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "aero-user"
+version = "0.3.0"
+authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
+edition = "2021"
+license = "EUPL-1.2"
+description = "Represent an encrypted user profile"
+
+[dependencies]
+anyhow.workspace = true
+serde.workspace = true
+zstd.workspace = true
+sodiumoxide.workspace = true
+log.workspace = true
+async-trait.workspace = true
+ldap3.workspace = true
+base64.workspace = true
+rand.workspace = true
+tokio.workspace = true
+aws-config.workspace = true
+aws-sdk-s3.workspace = true
+aws-smithy-runtime.workspace = true
+aws-smithy-runtime-api.workspace = true
+hyper-rustls.workspace = true
+hyper-util.workspace = true
+k2v-client.workspace = true
+rmp-serde.workspace = true
+toml.workspace = true
+tracing.workspace = true
+argon2.workspace = true
diff --git a/src/config.rs b/aero-user/src/config.rs
index faaa1ba..cea4520 100644
--- a/src/config.rs
+++ b/aero-user/src/config.rs
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
pub struct CompanionConfig {
pub pid: Option<PathBuf>,
pub imap: ImapUnsecureConfig,
-
+ // @FIXME Add DAV
#[serde(flatten)]
pub users: LoginStaticConfig,
}
@@ -22,6 +22,8 @@ pub struct ProviderConfig {
pub imap_unsecure: Option<ImapUnsecureConfig>,
pub lmtp: Option<LmtpConfig>,
pub auth: Option<AuthConfig>,
+ pub dav: Option<DavConfig>,
+ pub dav_unsecure: Option<DavUnsecureConfig>,
pub users: UserManagement,
}
@@ -52,6 +54,18 @@ pub struct ImapConfig {
}
#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct DavUnsecureConfig {
+ pub bind_addr: SocketAddr,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct DavConfig {
+ pub bind_addr: SocketAddr,
+ pub certs: PathBuf,
+ pub key: PathBuf,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ImapUnsecureConfig {
pub bind_addr: SocketAddr,
}
diff --git a/src/cryptoblob.rs b/aero-user/src/cryptoblob.rs
index 327a642..327a642 100644
--- a/src/cryptoblob.rs
+++ b/aero-user/src/cryptoblob.rs
diff --git a/aero-user/src/lib.rs b/aero-user/src/lib.rs
new file mode 100644
index 0000000..9b08fe2
--- /dev/null
+++ b/aero-user/src/lib.rs
@@ -0,0 +1,9 @@
+pub mod config;
+pub mod cryptoblob;
+pub mod login;
+pub mod storage;
+
+// A user is composed of 3 things:
+// - An identity (login)
+// - A storage profile (storage)
+// - Some cryptography data (cryptoblob)
diff --git a/src/login/demo_provider.rs b/aero-user/src/login/demo_provider.rs
index 11c7d54..11c7d54 100644
--- a/src/login/demo_provider.rs
+++ b/aero-user/src/login/demo_provider.rs
diff --git a/src/login/ldap_provider.rs b/aero-user/src/login/ldap_provider.rs
index 0af5676..22b301e 100644
--- a/src/login/ldap_provider.rs
+++ b/aero-user/src/login/ldap_provider.rs
@@ -1,10 +1,9 @@
-use anyhow::Result;
use async_trait::async_trait;
use ldap3::{LdapConnAsync, Scope, SearchEntry};
use log::debug;
+use super::*;
use crate::config::*;
-use crate::login::*;
use crate::storage;
pub struct LdapLoginProvider {
diff --git a/src/login/mod.rs b/aero-user/src/login/mod.rs
index 4a1dee1..5e54b4a 100644
--- a/src/login/mod.rs
+++ b/aero-user/src/login/mod.rs
@@ -2,11 +2,11 @@ pub mod demo_provider;
pub mod ldap_provider;
pub mod static_provider;
-use base64::Engine;
use std::sync::Arc;
use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait;
+use base64::Engine;
use rand::prelude::*;
use crate::cryptoblob::*;
diff --git a/src/login/static_provider.rs b/aero-user/src/login/static_provider.rs
index 79626df..ed39343 100644
--- a/src/login/static_provider.rs
+++ b/aero-user/src/login/static_provider.rs
@@ -1,11 +1,10 @@
use std::collections::HashMap;
use std::path::PathBuf;
-use std::sync::Arc;
-use tokio::signal::unix::{signal, SignalKind};
-use tokio::sync::watch;
-use anyhow::{anyhow, bail, Result};
+use anyhow::{anyhow, bail};
use async_trait::async_trait;
+use tokio::signal::unix::{signal, SignalKind};
+use tokio::sync::watch;
use crate::config::*;
use crate::login::*;
diff --git a/src/storage/garage.rs b/aero-user/src/storage/garage.rs
index 7152764..1164839 100644
--- a/src/storage/garage.rs
+++ b/aero-user/src/storage/garage.rs
@@ -6,7 +6,7 @@ use hyper_util::client::legacy::{connect::HttpConnector, Client as HttpClient};
use hyper_util::rt::TokioExecutor;
use serde::Serialize;
-use crate::storage::*;
+use super::*;
pub struct GarageRoot {
k2v_http: HttpClient<HttpsConnector<HttpConnector>, k2v_client::Body>,
@@ -426,15 +426,16 @@ impl IStore for GarageStore {
tracing::debug!("Fetched {}/{}", self.bucket, blob_ref.0);
Ok(bv)
}
- async fn blob_insert(&self, blob_val: BlobVal) -> Result<(), StorageError> {
+ async fn blob_insert(&self, blob_val: BlobVal) -> Result<String, StorageError> {
tracing::trace!(entry=%blob_val.blob_ref, command="blob_insert");
let streamable_value = s3::primitives::ByteStream::from(blob_val.value);
+ let obj_key = blob_val.blob_ref.0;
let maybe_send = self
.s3
.put_object()
.bucket(self.bucket.to_string())
- .key(blob_val.blob_ref.0.to_string())
+ .key(obj_key.to_string())
.set_metadata(Some(blob_val.meta))
.body(streamable_value)
.send()
@@ -445,9 +446,12 @@ impl IStore for GarageStore {
tracing::error!("unable to send object: {}", e);
Err(StorageError::Internal)
}
- Ok(_) => {
- tracing::debug!("Inserted {}/{}", self.bucket, blob_val.blob_ref.0);
- Ok(())
+ Ok(put_output) => {
+ tracing::debug!("Inserted {}/{}", self.bucket, obj_key);
+ Ok(put_output
+ .e_tag()
+ .map(|v| format!("\"{}\"", v))
+ .unwrap_or(format!("W/\"{}\"", obj_key)))
}
}
}
diff --git a/src/storage/in_memory.rs b/aero-user/src/storage/in_memory.rs
index 3c3a94c..5c8eb26 100644
--- a/src/storage/in_memory.rs
+++ b/aero-user/src/storage/in_memory.rs
@@ -1,9 +1,12 @@
-use crate::storage::*;
-use std::collections::{BTreeMap, HashMap};
+use std::collections::BTreeMap;
use std::ops::Bound::{self, Excluded, Included, Unbounded};
-use std::sync::{Arc, RwLock};
+use std::sync::RwLock;
+
+use sodiumoxide::{crypto::hash, hex};
use tokio::sync::Notify;
+use crate::storage::*;
+
/// This implementation is very inneficient, and not completely correct
/// Indeed, when the connector is dropped, the memory is freed.
/// It means that when a user disconnects, its data are lost.
@@ -80,6 +83,12 @@ impl InternalBlobVal {
value: self.data.clone(),
}
}
+ fn etag(&self) -> String {
+ let digest = hash::hash(self.data.as_ref());
+ let buff = digest.as_ref();
+ let hexstr = hex::encode(buff);
+ format!("\"{}\"", hexstr)
+ }
}
type ArcRow = Arc<RwLock<HashMap<String, BTreeMap<String, InternalRowVal>>>>;
@@ -300,13 +309,14 @@ impl IStore for MemStore {
.ok_or(StorageError::NotFound)
.map(|v| v.to_blob_val(blob_ref))
}
- async fn blob_insert(&self, blob_val: BlobVal) -> Result<(), StorageError> {
+ async fn blob_insert(&self, blob_val: BlobVal) -> Result<String, StorageError> {
tracing::trace!(entry=%blob_val.blob_ref, command="blob_insert");
let mut store = self.blob.write().or(Err(StorageError::Internal))?;
let entry = store.entry(blob_val.blob_ref.0.clone()).or_default();
entry.data = blob_val.value.clone();
entry.metadata = blob_val.meta.clone();
- Ok(())
+
+ Ok(entry.etag())
}
async fn blob_copy(&self, src: &BlobRef, dst: &BlobRef) -> Result<(), StorageError> {
tracing::trace!(src=%src, dst=%dst, command="blob_copy");
diff --git a/src/storage/mod.rs b/aero-user/src/storage/mod.rs
index 1f86f71..527765f 100644
--- a/src/storage/mod.rs
+++ b/aero-user/src/storage/mod.rs
@@ -11,11 +11,12 @@
pub mod garage;
pub mod in_memory;
-use async_trait::async_trait;
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
+use async_trait::async_trait;
+
#[derive(Debug, Clone)]
pub enum Alternative {
Tombstone,
@@ -158,7 +159,7 @@ pub trait IStore {
async fn row_poll(&self, value: &RowRef) -> Result<RowVal, StorageError>;
async fn blob_fetch(&self, blob_ref: &BlobRef) -> Result<BlobVal, StorageError>;
- async fn blob_insert(&self, blob_val: BlobVal) -> Result<(), StorageError>;
+ async fn blob_insert(&self, blob_val: BlobVal) -> Result<String, StorageError>;
async fn blob_copy(&self, src: &BlobRef, dst: &BlobRef) -> Result<(), StorageError>;
async fn blob_list(&self, prefix: &str) -> Result<Vec<BlobRef>, StorageError>;
async fn blob_rm(&self, blob_ref: &BlobRef) -> Result<(), StorageError>;
diff --git a/aerogramme/Cargo.toml b/aerogramme/Cargo.toml
new file mode 100644
index 0000000..77f3584
--- /dev/null
+++ b/aerogramme/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "aerogramme"
+version = "0.3.0"
+authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
+edition = "2021"
+license = "EUPL-1.2"
+description = "A robust email server"
+
+[dependencies]
+aero-user.workspace = true
+aero-proto.workspace = true
+
+anyhow.workspace = true
+backtrace.workspace = true
+futures.workspace = true
+tokio.workspace = true
+log.workspace = true
+nix.workspace = true
+clap.workspace = true
+tracing.workspace = true
+tracing-subscriber.workspace = true
+rpassword.workspace = true
+
+[dev-dependencies]
+reqwest.workspace = true
+aero-dav.workspace = true
+quick-xml.workspace = true
+
+[[test]]
+name = "behavior"
+path = "tests/behavior.rs"
+harness = false
diff --git a/src/main.rs b/aerogramme/src/main.rs
index 12a5895..39b5075 100644
--- a/src/main.rs
+++ b/aerogramme/src/main.rs
@@ -1,17 +1,4 @@
-#![feature(async_fn_in_trait)]
-
-mod auth;
-mod bayou;
-mod config;
-mod cryptoblob;
-mod imap;
-mod k2v_util;
-mod lmtp;
-mod login;
-mod mail;
mod server;
-mod storage;
-mod timestamp;
use std::io::Read;
use std::path::PathBuf;
@@ -20,9 +7,9 @@ use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand};
use nix::{sys::signal, unistd::Pid};
-use config::*;
-use login::{static_provider::*, *};
-use server::Server;
+use crate::server::Server;
+use aero_user::config::*;
+use aero_user::login::{static_provider::*, *};
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
@@ -166,7 +153,7 @@ fn tracer() {
#[tokio::main]
async fn main() -> Result<()> {
if std::env::var("RUST_LOG").is_err() {
- std::env::set_var("RUST_LOG", "main=info,aerogramme=info,k2v_client=info")
+ std::env::set_var("RUST_LOG", "info")
}
// Abort on panic (same behavior as in Go)
@@ -184,9 +171,13 @@ async fn main() -> Result<()> {
AnyConfig::Provider(ProviderConfig {
pid: None,
imap: None,
+ dav: None,
imap_unsecure: Some(ImapUnsecureConfig {
bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1143),
}),
+ dav_unsecure: Some(DavUnsecureConfig {
+ bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8087),
+ }),
lmtp: Some(LmtpConfig {
bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1025),
hostname: "example.tld".to_string(),
diff --git a/src/server.rs b/aerogramme/src/server.rs
index 9899981..3b3f6eb 100644
--- a/src/server.rs
+++ b/aerogramme/src/server.rs
@@ -7,18 +7,21 @@ use futures::try_join;
use log::*;
use tokio::sync::watch;
-use crate::auth;
-use crate::config::*;
-use crate::imap;
-use crate::lmtp::*;
-use crate::login::ArcLoginProvider;
-use crate::login::{demo_provider::*, ldap_provider::*, static_provider::*};
+use aero_proto::dav;
+use aero_proto::imap;
+use aero_proto::lmtp::*;
+use aero_proto::sasl as auth;
+use aero_user::config::*;
+use aero_user::login::ArcLoginProvider;
+use aero_user::login::{demo_provider::*, ldap_provider::*, static_provider::*};
pub struct Server {
lmtp_server: Option<Arc<LmtpServer>>,
imap_unsecure_server: Option<imap::Server>,
imap_server: Option<imap::Server>,
auth_server: Option<auth::AuthServer>,
+ dav_unsecure_server: Option<dav::Server>,
+ dav_server: Option<dav::Server>,
pid_file: Option<PathBuf>,
}
@@ -34,6 +37,8 @@ impl Server {
imap_unsecure_server,
imap_server: None,
auth_server: None,
+ dav_unsecure_server: None,
+ dav_server: None,
pid_file: config.pid,
})
}
@@ -57,11 +62,20 @@ impl Server {
let auth_server = config
.auth
.map(|auth| auth::AuthServer::new(auth, login.clone()));
+ let dav_unsecure_server = config
+ .dav_unsecure
+ .map(|dav_config| dav::new_unsecure(dav_config, login.clone()));
+ let dav_server = config
+ .dav
+ .map(|dav_config| dav::new(dav_config, login.clone()))
+ .transpose()?;
Ok(Self {
lmtp_server,
imap_unsecure_server,
imap_server,
+ dav_unsecure_server,
+ dav_server,
auth_server,
pid_file: config.pid,
})
@@ -112,6 +126,18 @@ impl Server {
None => Ok(()),
Some(a) => a.run(exit_signal.clone()).await,
}
+ },
+ async {
+ match self.dav_unsecure_server {
+ None => Ok(()),
+ Some(s) => s.run(exit_signal.clone()).await,
+ }
+ },
+ async {
+ match self.dav_server {
+ None => Ok(()),
+ Some(s) => s.run(exit_signal.clone()).await,
+ }
}
)?;
diff --git a/aerogramme/tests/behavior.rs b/aerogramme/tests/behavior.rs
new file mode 100644
index 0000000..f8d609a
--- /dev/null
+++ b/aerogramme/tests/behavior.rs
@@ -0,0 +1,1292 @@
+use anyhow::Context;
+
+mod common;
+use crate::common::constants::*;
+use crate::common::fragments::*;
+
+fn main() {
+ // IMAP
+ rfc3501_imap4rev1_base();
+ rfc6851_imapext_move();
+ rfc4551_imapext_condstore();
+ rfc2177_imapext_idle();
+ rfc5161_imapext_enable();
+ rfc3691_imapext_unselect();
+ rfc7888_imapext_literal();
+ rfc4315_imapext_uidplus();
+ rfc5819_imapext_liststatus();
+
+ // WebDAV
+ rfc4918_webdav_core();
+ rfc5397_webdav_principal();
+ rfc4791_webdav_caldav();
+ rfc6578_webdav_sync();
+ println!("✅ SUCCESS 🌟🚀🥳🙏🥹");
+}
+
+fn rfc3501_imap4rev1_base() {
+ println!("🧪 rfc3501_imap4rev1_base");
+ common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket, _dav_socket| {
+ connect(imap_socket).context("server says hello")?;
+ capability(imap_socket, Extension::None).context("check server capabilities")?;
+ login(imap_socket, Account::Alice).context("login test")?;
+ create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?;
+ let select_res =
+ select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?;
+ assert!(select_res.contains("* 0 EXISTS"));
+
+ check(imap_socket).context("check must run")?;
+ status(imap_socket, Mailbox::Archive, StatusKind::UidNext)
+ .context("status of archive from inbox")?;
+ lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
+ lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?;
+ noop_exists(imap_socket, 1).context("noop loop must detect a new email")?;
+
+ let srv_msg = fetch(
+ imap_socket,
+ Selection::FirstId,
+ FetchKind::Rfc822,
+ FetchMod::None,
+ )
+ .context("fetch rfc822 message, should be our first message")?;
+ let orig_email = std::str::from_utf8(EMAIL1)?;
+ assert!(srv_msg.contains(orig_email));
+
+ copy(imap_socket, Selection::FirstId, Mailbox::Archive)
+ .context("copy message to the archive mailbox")?;
+ append(imap_socket, Email::Basic).context("insert email in INBOX")?;
+ noop_exists(imap_socket, 2).context("noop loop must detect a new email")?;
+ search(imap_socket, SearchKind::Text("OoOoO")).expect("search should return something");
+ store(
+ imap_socket,
+ Selection::FirstId,
+ Flag::Deleted,
+ StoreAction::AddFlags,
+ StoreMod::None,
+ )
+ .context("should add delete flag to the email")?;
+ expunge(imap_socket).context("expunge emails")?;
+ rename_mailbox(imap_socket, Mailbox::Archive, Mailbox::Drafts)
+ .context("Archive mailbox is renamed Drafts")?;
+ delete_mailbox(imap_socket, Mailbox::Drafts).context("Drafts mailbox is deleted")?;
+ Ok(())
+ })
+ .expect("test fully run");
+}
+
+fn rfc3691_imapext_unselect() {
+ println!("🧪 rfc3691_imapext_unselect");
+ common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket, _dav_socket| {
+ connect(imap_socket).context("server says hello")?;
+
+ lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
+ lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?;
+
+ capability(imap_socket, Extension::Unselect).context("check server capabilities")?;
+ login(imap_socket, Account::Alice).context("login test")?;
+ let select_res =
+ select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?;
+ assert!(select_res.contains("* 0 EXISTS"));
+
+ noop_exists(imap_socket, 1).context("noop loop must detect a new email")?;
+ store(
+ imap_socket,
+ Selection::FirstId,
+ Flag::Deleted,
+ StoreAction::AddFlags,
+ StoreMod::None,
+ )
+ .context("add delete flags to the email")?;
+ unselect(imap_socket)
+ .context("unselect inbox while preserving email with the \\Delete flag")?;
+ let select_res =
+ select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox again")?;
+ assert!(select_res.contains("* 1 EXISTS"));
+
+ let srv_msg = fetch(
+ imap_socket,
+ Selection::FirstId,
+ FetchKind::Rfc822,
+ FetchMod::None,
+ )
+ .context("message is still present")?;
+ let orig_email = std::str::from_utf8(EMAIL2)?;
+ assert!(srv_msg.contains(orig_email));
+
+ close(imap_socket).context("close inbox and expunge message")?;
+ let select_res = select(imap_socket, Mailbox::Inbox, SelectMod::None)
+ .context("select inbox again and check it's empty")?;
+ assert!(select_res.contains("* 0 EXISTS"));
+
+ Ok(())
+ })
+ .expect("test fully run");
+}
+
+fn rfc5161_imapext_enable() {
+ println!("🧪 rfc5161_imapext_enable");
+ common::aerogramme_provider_daemon_dev(|imap_socket, _lmtp_socket, _dav_socket| {
+ connect(imap_socket).context("server says hello")?;
+ login(imap_socket, Account::Alice).context("login test")?;
+ enable(imap_socket, Enable::Utf8Accept, Some(Enable::Utf8Accept))?;
+ enable(imap_socket, Enable::Utf8Accept, None)?;
+ logout(imap_socket)?;
+
+ Ok(())
+ })
+ .expect("test fully run");
+}
+
+fn rfc6851_imapext_move() {
+ println!("🧪 rfc6851_imapext_move");
+ common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket, _dav_socket| {
+ connect(imap_socket).context("server says hello")?;
+
+ capability(imap_socket, Extension::Move).context("check server capabilities")?;
+ login(imap_socket, Account::Alice).context("login test")?;
+ create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?;
+ let select_res =
+ select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?;
+ assert!(select_res.contains("* 0 EXISTS"));
+
+ lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
+ lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?;
+
+ noop_exists(imap_socket, 1).context("noop loop must detect a new email")?;
+ r#move(imap_socket, Selection::FirstId, Mailbox::Archive)
+ .context("message from inbox moved to archive")?;
+
+ unselect(imap_socket)
+ .context("unselect inbox while preserving email with the \\Delete flag")?;
+ let select_res =
+ select(imap_socket, Mailbox::Archive, SelectMod::None).context("select archive")?;
+ assert!(select_res.contains("* 1 EXISTS"));
+
+ let srv_msg = fetch(
+ imap_socket,
+ Selection::FirstId,
+ FetchKind::Rfc822,
+ FetchMod::None,
+ )
+ .context("check mail exists")?;
+ let orig_email = std::str::from_utf8(EMAIL2)?;
+ assert!(srv_msg.contains(orig_email));
+
+ logout(imap_socket).context("must quit")?;
+
+ Ok(())
+ })
+ .expect("test fully run");
+}
+
+fn rfc7888_imapext_literal() {
+ println!("🧪 rfc7888_imapext_literal");
+ common::aerogramme_provider_daemon_dev(|imap_socket, _lmtp_socket, _dav_socket| {
+ connect(imap_socket).context("server says hello")?;
+
+ capability(imap_socket, Extension::LiteralPlus).context("check server capabilities")?;
+ login_with_literal(imap_socket, Account::Alice).context("use literal to connect Alice")?;
+
+ Ok(())
+ })
+ .expect("test fully run");
+}
+
+fn rfc4551_imapext_condstore() {
+ println!("🧪 rfc4551_imapext_condstore");
+ common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket, _dav_socket| {
+ // Setup the test
+ connect(imap_socket).context("server says hello")?;
+
+ // RFC 3.1.1 Advertising Support for CONDSTORE
+ capability(imap_socket, Extension::Condstore).context("check server capabilities")?;
+ login(imap_socket, Account::Alice).context("login test")?;
+
+ // RFC 3.1.8. CONDSTORE Parameter to SELECT and EXAMINE
+ let select_res =
+ select(imap_socket, Mailbox::Inbox, SelectMod::Condstore).context("select inbox")?;
+ // RFC 3.1.2 New OK Untagged Responses for SELECT and EXAMINE
+ assert!(select_res.contains("[HIGHESTMODSEQ 1]"));
+
+ // RFC 3.1.3. STORE and UID STORE Commands
+ lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
+ lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?;
+ lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?;
+ noop_exists(imap_socket, 2).context("noop loop must detect a new email")?;
+ let store_res = store(
+ imap_socket,
+ Selection::All,
+ Flag::Important,
+ StoreAction::AddFlags,
+ StoreMod::UnchangedSince(1),
+ )?;
+ assert!(store_res.contains("[MODIFIED 2]"));
+ assert!(store_res.contains("* 1 FETCH (FLAGS (\\Important) MODSEQ (3))"));
+ assert!(!store_res.contains("* 2 FETCH"));
+ assert_eq!(store_res.lines().count(), 2);
+
+ // RFC 3.1.4. FETCH and UID FETCH Commands
+ let fetch_res = fetch(
+ imap_socket,
+ Selection::All,
+ FetchKind::Rfc822Size,
+ FetchMod::ChangedSince(2),
+ )?;
+ assert!(fetch_res.contains("* 1 FETCH (RFC822.SIZE 81 MODSEQ (3))"));
+ assert!(!fetch_res.contains("* 2 FETCH"));
+ assert_eq!(store_res.lines().count(), 2);
+
+ // RFC 3.1.5. MODSEQ Search Criterion in SEARCH
+ let search_res = search(imap_socket, SearchKind::ModSeq(3))?;
+ // RFC 3.1.6. Modified SEARCH Untagged Response
+ assert!(search_res.contains("* SEARCH 1 (MODSEQ 3)"));
+
+ // RFC 3.1.7 HIGHESTMODSEQ Status Data Items
+ let status_res = status(imap_socket, Mailbox::Inbox, StatusKind::HighestModSeq)?;
+ assert!(status_res.contains("HIGHESTMODSEQ 3"));
+
+ Ok(())
+ })
+ .expect("test fully run");
+}
+
+fn rfc2177_imapext_idle() {
+ println!("🧪 rfc2177_imapext_idle");
+ common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket, _dav_socket| {
+ // Test setup, check capability
+ connect(imap_socket).context("server says hello")?;
+ capability(imap_socket, Extension::Idle).context("check server capabilities")?;
+ login(imap_socket, Account::Alice).context("login test")?;
+ select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?;
+
+ // Check that new messages from LMTP are correctly detected during idling
+ start_idle(imap_socket).context("can't start idling")?;
+ lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
+ lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?;
+ let srv_msg = stop_idle(imap_socket).context("stop idling")?;
+ assert!(srv_msg.contains("* 1 EXISTS"));
+
+ Ok(())
+ })
+ .expect("test fully run");
+}
+
+fn rfc4315_imapext_uidplus() {
+ println!("🧪 rfc4315_imapext_uidplus");
+ common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket, _dav_socket| {
+ // Test setup, check capability, insert 2 emails
+ connect(imap_socket).context("server says hello")?;
+ capability(imap_socket, Extension::UidPlus).context("check server capabilities")?;
+ login(imap_socket, Account::Alice).context("login test")?;
+ select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?;
+ lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
+ lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?;
+ lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?;
+ noop_exists(imap_socket, 2).context("noop loop must detect a new email")?;
+
+ // Check UID EXPUNGE seqset
+ store(
+ imap_socket,
+ Selection::All,
+ Flag::Deleted,
+ StoreAction::AddFlags,
+ StoreMod::None,
+ )?;
+ let res = uid_expunge(imap_socket, Selection::FirstId)?;
+ assert_eq!(res.lines().count(), 2);
+ assert!(res.contains("* 1 EXPUNGE"));
+
+ // APPENDUID check UID + UID VALIDITY
+ // Note: 4 and not 3, as we update the UID counter when we delete an email
+ // it's part of our UID proof
+ let res = append(imap_socket, Email::Multipart)?;
+ assert!(res.contains("[APPENDUID 1 4]"));
+
+ // COPYUID, check
+ create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?;
+ let res = copy(imap_socket, Selection::FirstId, Mailbox::Archive)?;
+ assert!(res.contains("[COPYUID 1 2 1]"));
+
+ // MOVEUID, check
+ let res = r#move(imap_socket, Selection::FirstId, Mailbox::Archive)?;
+ assert!(res.contains("[COPYUID 1 2 2]"));
+
+ Ok(())
+ })
+ .expect("test fully run");
+}
+
+///
+/// Example
+///
+/// ```text
+/// 30 list "" "*" RETURN (STATUS (MESSAGES UNSEEN))
+/// * LIST (\Subscribed) "." INBOX
+/// * STATUS INBOX (MESSAGES 2 UNSEEN 1)
+/// 30 OK LIST completed
+/// ```
+fn rfc5819_imapext_liststatus() {
+ println!("🧪 rfc5819_imapext_liststatus");
+ common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket, _dav_socket| {
+ // Test setup, check capability, add 2 emails, read 1
+ connect(imap_socket).context("server says hello")?;
+ capability(imap_socket, Extension::ListStatus).context("check server capabilities")?;
+ login(imap_socket, Account::Alice).context("login test")?;
+ select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?;
+ lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
+ lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?;
+ lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?;
+ noop_exists(imap_socket, 2).context("noop loop must detect a new email")?;
+ fetch(
+ imap_socket,
+ Selection::FirstId,
+ FetchKind::Rfc822,
+ FetchMod::None,
+ )
+ .context("read one message")?;
+ close(imap_socket).context("close inbox")?;
+
+ // Test return status MESSAGES UNSEEN
+ let ret = list(
+ imap_socket,
+ MbxSelect::All,
+ ListReturn::StatusMessagesUnseen,
+ )?;
+ assert!(ret.contains("* STATUS INBOX (MESSAGES 2 UNSEEN 1)"));
+
+ // Test that without RETURN, no status is sent
+ let ret = list(imap_socket, MbxSelect::All, ListReturn::None)?;
+ assert!(!ret.contains("* STATUS"));
+
+ Ok(())
+ })
+ .expect("test fully run");
+}
+
+use aero_dav::acltypes as acl;
+use aero_dav::caltypes as cal;
+use aero_dav::realization::{self, All};
+use aero_dav::synctypes as sync;
+use aero_dav::types as dav;
+use aero_dav::versioningtypes as vers;
+
+use crate::common::{dav_deserialize, dav_serialize};
+
+fn rfc4918_webdav_core() {
+ println!("🧪 rfc4918_webdav_core");
+ common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| {
+ // --- PROPFIND ---
+ // empty request body (assume "allprop")
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087").send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ let root_propstats = multistatus.responses.iter()
+ .find_map(|v| match &v.status_or_propstat {
+ dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/" => Some(x),
+ _ => None,
+ })
+ .expect("propstats for root must exist");
+
+ let root_success = root_propstats.iter().find(|p| p.status.0.as_u16() == 200).expect("some propstats for root must be 200");
+ let display_name = root_success.prop.0.iter()
+ .find_map(|v| match v { dav::AnyProperty::Value(dav::Property::DisplayName(x)) => Some(x), _ => None } )
+ .expect("root has a display name");
+ let content_type = root_success.prop.0.iter()
+ .find_map(|v| match v { dav::AnyProperty::Value(dav::Property::GetContentType(x)) => Some(x), _ => None } )
+ .expect("root has a content type");
+ let resource_type = root_success.prop.0.iter()
+ .find_map(|v| match v { dav::AnyProperty::Value(dav::Property::ResourceType(x)) => Some(x), _ => None } )
+ .expect("root has a resource type");
+
+ assert_eq!(display_name, "DAV Root");
+ assert_eq!(content_type, "httpd/unix-directory");
+ assert_eq!(resource_type, &[ dav::ResourceType::Collection ]);
+
+ // propname
+ let propfind_req = r#"<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><propname/></propfind>"#;
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087").body(propfind_req).send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ let root_propstats = multistatus.responses.iter()
+ .find_map(|v| match &v.status_or_propstat {
+ dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/" => Some(x),
+ _ => None,
+ })
+ .expect("propstats for root must exist");
+ let root_success = root_propstats.iter().find(|p| p.status.0.as_u16() == 200).expect("some propstats for root must be 200");
+ assert!(root_success.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Request(dav::PropertyRequest::DisplayName))).is_some());
+ assert!(root_success.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Request(dav::PropertyRequest::ResourceType))).is_some());
+ assert!(root_success.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Request(dav::PropertyRequest::GetContentType))).is_some());
+
+ // list of properties
+ let propfind_req = r#"<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><prop><displayname/><getcontentlength/></prop></propfind>"#;
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087").body(propfind_req).send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ let root_propstats = multistatus.responses.iter()
+ .find_map(|v| match &v.status_or_propstat {
+ dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/" => Some(x),
+ _ => None,
+ })
+ .expect("propstats for root must exist");
+
+ let root_success = root_propstats.iter().find(|p| p.status.0.as_u16() == 200).expect("some propstats for root must be 200");
+ let root_not_found = root_propstats.iter().find(|p| p.status.0.as_u16() == 404).expect("some propstats for root must be not found");
+
+ assert!(root_success.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Value(dav::Property::DisplayName(x)) if x == "DAV Root")).is_some());
+ assert!(root_success.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Value(dav::Property::ResourceType(_)))).is_none());
+ assert!(root_success.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Value(dav::Property::GetContentType(_)))).is_none());
+ assert!(root_not_found.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Request(dav::PropertyRequest::GetContentLength))).is_some());
+
+ // -- HIERARCHY EXPLORATION WITH THE DEPTH: X HEADER FIELD --
+ // depth 1 / -> /alice/
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087").header("Depth", "1").send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ let _user_propstats = multistatus.responses.iter()
+ .find_map(|v| match &v.status_or_propstat {
+ dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/alice/" => Some(x),
+ _ => None,
+ })
+ .expect("user collection must exist");
+
+ // depth 1 /alice/ -> /alice/calendar/
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/").header("Depth", "1").send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ let _user_calendars_propstats = multistatus.responses.iter()
+ .find_map(|v| match &v.status_or_propstat {
+ dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/alice/calendar/" => Some(x),
+ _ => None,
+ })
+ .expect("user collection must exist");
+
+ // depth 1 /alice/calendar/ -> /alice/calendar/Personal/
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/").header("Depth", "1").send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ let _user_calendars_propstats = multistatus.responses.iter()
+ .find_map(|v| match &v.status_or_propstat {
+ dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/alice/calendar/Personal/" => Some(x),
+ _ => None,
+ })
+ .expect("Personal calendar must exist");
+
+ // depth 1 /alice/calendar/Personal/ -> empty for now...
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").header("Depth", "1").send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ assert_eq!(multistatus.responses.len(), 1);
+
+ // --- PUT (add objets) ---
+ // first object
+ let resp = http.put("http://localhost:8087/alice/calendar/Personal/rfc2.ics").header("If-None-Match", "*").body(ICAL_RFC2).send()?;
+ let obj1_etag = resp.headers().get("etag").expect("etag must be set");
+ assert_eq!(resp.status(), 201);
+
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").header("Depth", "1").send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ assert_eq!(multistatus.responses.len(), 2);
+
+ // second object
+ let resp = http.put("http://localhost:8087/alice/calendar/Personal/rfc3.ics").header("If-None-Match", "*").body(ICAL_RFC3).send()?;
+ assert_eq!(resp.status(), 201);
+
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").header("Depth", "1").send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ assert_eq!(multistatus.responses.len(), 3);
+
+ // can't create an event on an existing path
+ let resp = http.put("http://localhost:8087/alice/calendar/Personal/rfc2.ics").header("If-None-Match", "*").body(ICAL_RFC1).send()?;
+ assert_eq!(resp.status(), 412);
+
+ // update first object by knowing its ETag
+ let resp = http.put("http://localhost:8087/alice/calendar/Personal/rfc2.ics").header("If-Match", obj1_etag).body(ICAL_RFC1).send()?;
+ assert_eq!(resp.status(), 201);
+
+ // --- GET (fetch objects) ---
+ let body = http.get("http://localhost:8087/alice/calendar/Personal/rfc2.ics").send()?.text()?;
+ assert_eq!(body.as_bytes(), ICAL_RFC1);
+
+ let body = http.get("http://localhost:8087/alice/calendar/Personal/rfc3.ics").send()?.text()?;
+ assert_eq!(body.as_bytes(), ICAL_RFC3);
+
+ // --- DELETE (delete objects) ---
+ // delete 1st object
+ let resp = http.delete("http://localhost:8087/alice/calendar/Personal/rfc2.ics").send()?;
+ assert_eq!(resp.status(), 204);
+
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").header("Depth", "1").send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ assert_eq!(multistatus.responses.len(), 2);
+
+ // delete 2nd object
+ let resp = http.delete("http://localhost:8087/alice/calendar/Personal/rfc3.ics").send()?;
+ assert_eq!(resp.status(), 204);
+
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").header("Depth", "1").send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ assert_eq!(multistatus.responses.len(), 1);
+
+ Ok(())
+ })
+ .expect("test fully run");
+}
+
+fn rfc5397_webdav_principal() {
+ println!("🧪 rfc5397_webdav_principal");
+ common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| {
+ // -- AUTODISCOVERY: FIND "PRINCIPAL" AS DEFINED IN WEBDAV ACL (~USER'S HOME) --
+ let propfind_req = r#"<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><prop><current-user-principal/></prop></propfind>"#;
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087").body(propfind_req).send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ let root_propstats = multistatus.responses.iter()
+ .find_map(|v| match &v.status_or_propstat {
+ dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/" => Some(x),
+ _ => None,
+ })
+ .expect("propstats for root must exist");
+
+ let root_success = root_propstats.iter().find(|p| p.status.0.as_u16() == 200).expect("current-user-principal must exist");
+ let principal = root_success.prop.0.iter()
+ .find_map(|v| match v {
+ dav::AnyProperty::Value(dav::Property::Extension(realization::Property::Acl(acl::Property::CurrentUserPrincipal(acl::User::Authenticated(dav::Href(x)))))) => Some(x),
+ _ => None,
+ })
+ .expect("request returned an authenticated principal");
+ assert_eq!(principal, "/alice/");
+
+ Ok(())
+ })
+ .expect("test fully run")
+}
+
+fn rfc4791_webdav_caldav() {
+ println!("🧪 rfc4791_webdav_caldav");
+ common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| {
+ // --- INITIAL TEST SETUP ---
+ // Add entries
+ let resp = http
+ .put("http://localhost:8087/alice/calendar/Personal/rfc1.ics")
+ .header("If-None-Match", "*")
+ .body(ICAL_RFC1)
+ .send()?;
+ let obj1_etag = resp.headers().get("etag").expect("etag must be set");
+ assert_eq!(resp.status(), 201);
+ let resp = http
+ .put("http://localhost:8087/alice/calendar/Personal/rfc2.ics")
+ .header("If-None-Match", "*")
+ .body(ICAL_RFC2)
+ .send()?;
+ let obj2_etag = resp.headers().get("etag").expect("etag must be set");
+ assert_eq!(resp.status(), 201);
+ let resp = http
+ .put("http://localhost:8087/alice/calendar/Personal/rfc3.ics")
+ .header("If-None-Match", "*")
+ .body(ICAL_RFC3)
+ .send()?;
+ let obj3_etag = resp.headers().get("etag").expect("etag must be set");
+ assert_eq!(resp.status(), 201);
+ let resp = http
+ .put("http://localhost:8087/alice/calendar/Personal/rfc4.ics")
+ .header("If-None-Match", "*")
+ .body(ICAL_RFC4)
+ .send()?;
+ let _obj4_etag = resp.headers().get("etag").expect("etag must be set");
+ assert_eq!(resp.status(), 201);
+ let resp = http
+ .put("http://localhost:8087/alice/calendar/Personal/rfc5.ics")
+ .header("If-None-Match", "*")
+ .body(ICAL_RFC5)
+ .send()?;
+ let _obj5_etag = resp.headers().get("etag").expect("etag must be set");
+ assert_eq!(resp.status(), 201);
+ let resp = http
+ .put("http://localhost:8087/alice/calendar/Personal/rfc6.ics")
+ .header("If-None-Match", "*")
+ .body(ICAL_RFC6)
+ .send()?;
+ let obj6_etag = resp.headers().get("etag").expect("etag must be set");
+ assert_eq!(resp.status(), 201);
+ let resp = http
+ .put("http://localhost:8087/alice/calendar/Personal/rfc7.ics")
+ .header("If-None-Match", "*")
+ .body(ICAL_RFC7)
+ .send()?;
+ let obj7_etag = resp.headers().get("etag").expect("etag must be set");
+ assert_eq!(resp.status(), 201);
+
+ // A generic function to check a <calendar-data/> query result
+ let check_cal =
+ |multistatus: &dav::Multistatus<All>,
+ (ref_path, ref_etag, ref_ical): (&str, Option<&str>, Option<&[u8]>)| {
+ let obj_stats = multistatus
+ .responses
+ .iter()
+ .find_map(|v| match &v.status_or_propstat {
+ dav::StatusOrPropstat::PropStat(dav::Href(p), x)
+ if p.as_str() == ref_path =>
+ {
+ Some(x)
+ }
+ _ => None,
+ })
+ .expect("propstats must exist");
+ let obj_success = obj_stats
+ .iter()
+ .find(|p| p.status.0.as_u16() == 200)
+ .expect("some propstats must be 200");
+ let etag = obj_success.prop.0.iter().find_map(|p| match p {
+ dav::AnyProperty::Value(dav::Property::GetEtag(x)) => Some(x.as_str()),
+ _ => None,
+ });
+ assert_eq!(etag, ref_etag);
+ let calendar_data = obj_success.prop.0.iter().find_map(|p| match p {
+ dav::AnyProperty::Value(dav::Property::Extension(
+ realization::Property::Cal(cal::Property::CalendarData(x)),
+ )) => Some(x.payload.as_bytes()),
+ _ => None,
+ });
+ assert_eq!(calendar_data, ref_ical);
+ };
+
+ // --- AUTODISCOVERY ---
+ // Check calendar discovery from principal
+ let propfind_req = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop><C:calendar-home-set/></D:prop>
+ </D:propfind>"#;
+
+ let body = http
+ .request(
+ reqwest::Method::from_bytes(b"PROPFIND")?,
+ "http://localhost:8087/alice/",
+ )
+ .body(propfind_req)
+ .send()?
+ .text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ let principal_propstats = multistatus
+ .responses
+ .iter()
+ .find_map(|v| match &v.status_or_propstat {
+ dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/alice/" => {
+ Some(x)
+ }
+ _ => None,
+ })
+ .expect("propstats for root must exist");
+ let principal_success = principal_propstats
+ .iter()
+ .find(|p| p.status.0.as_u16() == 200)
+ .expect("current-user-principal must exist");
+ let calendar_home_set = principal_success
+ .prop
+ .0
+ .iter()
+ .find_map(|v| match v {
+ dav::AnyProperty::Value(dav::Property::Extension(realization::Property::Cal(
+ cal::Property::CalendarHomeSet(dav::Href(x)),
+ ))) => Some(x),
+ _ => None,
+ })
+ .expect("request returns a calendar home set");
+ assert_eq!(calendar_home_set, "/alice/calendar/");
+
+ // Check calendar access support
+ let _resp = http
+ .request(
+ reqwest::Method::from_bytes(b"OPTIONS")?,
+ "http://localhost:8087/alice/calendar/",
+ )
+ .send()?;
+ //@FIXME not yet supported. returns DAV: 1 ; expects DAV: 1 calendar-access
+ // Not used by any client I know, so not implementing it now.
+
+ // --- REPORT calendar-multiget ---
+ let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop>
+ <D:getetag/>
+ <C:calendar-data/>
+ </D:prop>
+ <D:href>/alice/calendar/Personal/rfc1.ics</D:href>
+ <D:href>/alice/calendar/Personal/rfc3.ics</D:href>
+ </C:calendar-multiget>"#;
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(cal_query)
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 2);
+ [
+ ("/alice/calendar/Personal/rfc1.ics", obj1_etag, ICAL_RFC1),
+ ("/alice/calendar/Personal/rfc3.ics", obj3_etag, ICAL_RFC3),
+ ]
+ .iter()
+ .for_each(|(ref_path, ref_etag, ref_ical)| {
+ check_cal(
+ &multistatus,
+ (
+ ref_path,
+ Some(ref_etag.to_str().expect("etag header convertible to str")),
+ Some(ref_ical),
+ ),
+ )
+ });
+
+ // --- REPORT calendar-query, only filtering ---
+ // 7.8.8. Example: Retrieval of Events Only
+ let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop xmlns:D="DAV:">
+ <D:getetag/>
+ <C:calendar-data/>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VEVENT"/>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(cal_query)
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 4);
+
+ [
+ ("/alice/calendar/Personal/rfc1.ics", obj1_etag, ICAL_RFC1),
+ ("/alice/calendar/Personal/rfc2.ics", obj2_etag, ICAL_RFC2),
+ ("/alice/calendar/Personal/rfc3.ics", obj3_etag, ICAL_RFC3),
+ ("/alice/calendar/Personal/rfc7.ics", obj7_etag, ICAL_RFC7),
+ ]
+ .iter()
+ .for_each(|(ref_path, ref_etag, ref_ical)| {
+ check_cal(
+ &multistatus,
+ (
+ ref_path,
+ Some(ref_etag.to_str().expect("etag header convertible to str")),
+ Some(ref_ical),
+ ),
+ )
+ });
+
+ // 8.2.1.2. Synchronize by Time Range (here: July 2006)
+ let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop>
+ <D:getetag/>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VEVENT">
+ <C:time-range start="20060701T000000Z" end="20060801T000000Z"/>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(cal_query)
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 1);
+ check_cal(
+ &multistatus,
+ (
+ "/alice/calendar/Personal/rfc2.ics",
+ Some(obj2_etag.to_str().expect("etag header convertible to str")),
+ None,
+ ),
+ );
+
+ // 7.8.5. Example: Retrieval of To-Dos by Alarm Time Range
+ let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop xmlns:D="DAV:">
+ <D:getetag/>
+ <C:calendar-data/>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VTODO">
+ <C:comp-filter name="VALARM">
+ <C:time-range start="20060201T000000Z" end="20060301T000000Z"/>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(cal_query)
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 1);
+ check_cal(
+ &multistatus,
+ (
+ "/alice/calendar/Personal/rfc6.ics",
+ Some(obj6_etag.to_str().expect("etag header convertible to str")),
+ Some(ICAL_RFC6),
+ ),
+ );
+
+ // 7.8.6. Example: Retrieval of Event by UID
+ let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop xmlns:D="DAV:">
+ <D:getetag/>
+ <C:calendar-data/>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VEVENT">
+ <C:prop-filter name="UID">
+ <C:text-match collation="i;octet">DC6C50A017428C5216A2F1CD@example.com</C:text-match>
+ </C:prop-filter>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(cal_query)
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 1);
+ check_cal(
+ &multistatus,
+ (
+ "/alice/calendar/Personal/rfc3.ics",
+ Some(obj3_etag.to_str().expect("etag header convertible to str")),
+ Some(ICAL_RFC3),
+ ),
+ );
+
+
+ // 7.8.7. Example: Retrieval of Events by PARTSTAT
+ let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop xmlns:D="DAV:">
+ <D:getetag/>
+ <C:calendar-data/>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VEVENT">
+ <C:prop-filter name="ATTENDEE">
+ <C:text-match collation="i;ascii-casemap">mailto:lisa@example.com</C:text-match>
+ <C:param-filter name="PARTSTAT">
+ <C:text-match collation="i;ascii-casemap">NEEDS-ACTION</C:text-match>
+ </C:param-filter>
+ </C:prop-filter>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(cal_query)
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 1);
+ check_cal(
+ &multistatus,
+ (
+ "/alice/calendar/Personal/rfc7.ics",
+ Some(obj7_etag.to_str().expect("etag header convertible to str")),
+ Some(ICAL_RFC7),
+ ),
+ );
+
+ // 7.8.9. Example: Retrieval of All Pending To-Dos
+ let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop xmlns:D="DAV:">
+ <D:getetag/>
+ <C:calendar-data/>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VTODO">
+ <C:prop-filter name="COMPLETED">
+ <C:is-not-defined/>
+ </C:prop-filter>
+ <C:prop-filter name="STATUS">
+ <C:text-match negate-condition="yes">CANCELLED</C:text-match>
+ </C:prop-filter>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(cal_query)
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 1);
+ check_cal(
+ &multistatus,
+ (
+ "/alice/calendar/Personal/rfc6.ics",
+ Some(obj6_etag.to_str().expect("etag header convertible to str")),
+ Some(ICAL_RFC6),
+ ),
+ );
+
+ // --- REPORT calendar-query, with calendar-data tx ---
+ let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop>
+ <D:getetag/>
+ <C:calendar-data>
+ <C:comp name="VCALENDAR">
+ <C:prop name="VERSION"/>
+ <C:comp name="VEVENT">
+ <C:prop name="UID"/>
+ <C:prop name="DTSTART"/>
+ <C:prop name="DTEND"/>
+ <C:prop name="DURATION"/>
+ <C:prop name="RRULE"/>
+ <C:prop name="RDATE"/>
+ <C:prop name="EXRULE"/>
+ <C:prop name="EXDATE"/>
+ <C:prop name="RECURRENCE-ID"/>
+ </C:comp>
+ <C:comp name="VTIMEZONE"/>
+ </C:comp>
+ </C:calendar-data>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VEVENT">
+ <C:time-range start="20060104T000000Z" end="20060105T000000Z"/>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(cal_query)
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 1);
+ check_cal(
+ &multistatus,
+ (
+ "/alice/calendar/Personal/rfc3.ics",
+ Some(obj3_etag.to_str().expect("etag header convertible to str")),
+ Some(ICAL_RFC3_STRIPPED),
+ ),
+ );
+
+ Ok(())
+ })
+ .expect("test fully run")
+}
+
+fn rfc6578_webdav_sync() {
+ println!("🧪 rfc6578_webdav_sync");
+ common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| {
+ // -- PROPFIND --
+ // propname must return sync-token & supported-report-set (from webdav versioning)
+ let propfind_req = r#"<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><propname/></propfind>"#;
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").body(propfind_req).send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ let root_propstats = multistatus.responses.iter()
+ .find_map(|v| match &v.status_or_propstat {
+ dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/alice/calendar/Personal/" => Some(x),
+ _ => None,
+ })
+ .expect("propstats for target must exist");
+ let root_success = root_propstats.iter().find(|p| p.status.0.as_u16() == 200).expect("some propstats for root must be 200");
+ assert!(root_success.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Request(dav::PropertyRequest::Extension(
+ realization::PropertyRequest::Sync(sync::PropertyRequest::SyncToken)
+ )))).is_some());
+ assert!(root_success.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Request(dav::PropertyRequest::Extension(
+ realization::PropertyRequest::Vers(vers::PropertyRequest::SupportedReportSet)
+ )))).is_some());
+
+ // synctoken and supported report set must contains a meaningful value when queried
+ let propfind_req = r#"<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><prop><sync-token/><supported-report-set/></prop></propfind>"#;
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").body(propfind_req).send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+ let root_propstats = multistatus.responses.iter()
+ .find_map(|v| match &v.status_or_propstat {
+ dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/alice/calendar/Personal/" => Some(x),
+ _ => None,
+ })
+ .expect("propstats for target must exist");
+
+ let root_success = root_propstats.iter().find(|p| p.status.0.as_u16() == 200).expect("some propstats for root must be 200");
+
+ let init_sync_token = root_success.prop.0.iter().find_map(|p| match p {
+ dav::AnyProperty::Value(dav::Property::Extension(realization::Property::Sync(sync::Property::SyncToken(st)))) => Some(st),
+ _ => None,
+ }).expect("sync_token exists");
+
+ let supported = root_success.prop.0.iter().find_map(|p| match p {
+ dav::AnyProperty::Value(dav::Property::Extension(realization::Property::Vers(vers::Property::SupportedReportSet(s)))) => Some(s),
+ _ => None
+ }).expect("supported report set exists");
+ assert_eq!(&supported[..], &[
+ vers::SupportedReport(vers::ReportName::Extension(realization::ReportTypeName::Cal(cal::ReportTypeName::Multiget))),
+ vers::SupportedReport(vers::ReportName::Extension(realization::ReportTypeName::Cal(cal::ReportTypeName::Query))),
+ vers::SupportedReport(vers::ReportName::Extension(realization::ReportTypeName::Sync(sync::ReportTypeName::SyncCollection))),
+ ]);
+
+
+ // synctoken must change if we add a file
+ let resp = http
+ .put("http://localhost:8087/alice/calendar/Personal/rfc1.ics")
+ .header("If-None-Match", "*")
+ .body(ICAL_RFC1)
+ .send()?;
+ assert_eq!(resp.status(), 201);
+
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").body(propfind_req).send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+
+ let root_propstats = multistatus.responses.iter()
+ .find_map(|v| match &v.status_or_propstat {
+ dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/alice/calendar/Personal/" => Some(x),
+ _ => None,
+ })
+ .expect("propstats for target must exist");
+ let root_success = root_propstats.iter().find(|p| p.status.0.as_u16() == 200).expect("some propstats for root must be 200");
+ let rfc1_sync_token = root_success.prop.0.iter().find_map(|p| match p {
+ dav::AnyProperty::Value(dav::Property::Extension(realization::Property::Sync(sync::Property::SyncToken(st)))) => Some(st),
+ _ => None,
+ }).expect("sync_token exists");
+ assert!(init_sync_token != rfc1_sync_token);
+
+
+ // synctoken must change if we delete a file
+ let resp = http.delete("http://localhost:8087/alice/calendar/Personal/rfc1.ics").send()?;
+ assert_eq!(resp.status(), 204);
+
+ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").body(propfind_req).send()?.text()?;
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
+
+ let root_propstats = multistatus.responses.iter()
+ .find_map(|v| match &v.status_or_propstat {
+ dav::StatusOrPropstat::PropStat(dav::Href(p), x) if p.as_str() == "/alice/calendar/Personal/" => Some(x),
+ _ => None,
+ })
+ .expect("propstats for target must exist");
+ let root_success = root_propstats.iter().find(|p| p.status.0.as_u16() == 200).expect("some propstats for root must be 200");
+ let del_sync_token = root_success.prop.0.iter().find_map(|p| match p {
+ dav::AnyProperty::Value(dav::Property::Extension(realization::Property::Sync(sync::Property::SyncToken(st)))) => Some(st),
+ _ => None,
+ }).expect("sync_token exists");
+ assert!(init_sync_token != del_sync_token);
+ assert!(rfc1_sync_token != del_sync_token);
+
+ // -- TEST SYNC CUSTOM REPORT: SYNC-COLLECTION --
+ // 3.8. Example: Initial DAV:sync-collection Report
+ // Part 1: check the empty case
+ let sync_query = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <D:sync-collection xmlns:D="DAV:">
+ <D:sync-token/>
+ <D:sync-level>1</D:sync-level>
+ <D:prop>
+ <D:getetag/>
+ </D:prop>
+ </D:sync-collection>
+ "#;
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(sync_query)
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 0);
+ let empty_token = match &multistatus.extension {
+ Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x,
+ _ => anyhow::bail!("wrong content"),
+ };
+
+ // Part 2: check with one file
+ let resp = http
+ .put("http://localhost:8087/alice/calendar/Personal/rfc1.ics")
+ .header("If-None-Match", "*")
+ .body(ICAL_RFC1)
+ .send()?;
+ assert_eq!(resp.status(), 201);
+
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(sync_query)
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 1);
+ let initial_one_file_token = match &multistatus.extension {
+ Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x,
+ _ => anyhow::bail!("wrong content"),
+ };
+ assert!(empty_token != initial_one_file_token);
+
+ // 3.9. Example: DAV:sync-collection Report with Token
+ // Part 1: nothing changed, response must be empty
+ let sync_query = |token: &str| vers::Report::<realization::All>::Extension(realization::ReportType::Sync(sync::SyncCollection {
+ sync_token: sync::SyncTokenRequest::IncrementalSync(token.into()),
+ sync_level: sync::SyncLevel::One,
+ limit: None,
+ prop: dav::PropName(vec![
+ dav::PropertyRequest::GetEtag,
+ ]),
+ }));
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(dav_serialize(&sync_query(initial_one_file_token)))
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 0);
+ let no_change = match &multistatus.extension {
+ Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x,
+ _ => anyhow::bail!("wrong content"),
+ };
+ assert_eq!(initial_one_file_token, no_change);
+
+ // Part 2: add a new node (rfc2) + remove a node (rfc1)
+ // add rfc2
+ let resp = http
+ .put("http://localhost:8087/alice/calendar/Personal/rfc2.ics")
+ .header("If-None-Match", "*")
+ .body(ICAL_RFC2)
+ .send()?;
+ assert_eq!(resp.status(), 201);
+
+ // delete rfc1
+ let resp = http.delete("http://localhost:8087/alice/calendar/Personal/rfc1.ics").send()?;
+ assert_eq!(resp.status(), 204);
+
+ // call REPORT <sync-collection>
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(dav_serialize(&sync_query(initial_one_file_token)))
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 2);
+ let token_addrm = match &multistatus.extension {
+ Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x,
+ _ => anyhow::bail!("wrong content"),
+ };
+ assert!(initial_one_file_token != token_addrm);
+
+ // Part 3: remove a node (rfc2) and add it again with new content
+ // delete rfc2
+ let resp = http.delete("http://localhost:8087/alice/calendar/Personal/rfc2.ics").send()?;
+ assert_eq!(resp.status(), 204);
+
+ // add rfc2 with ICAL_RFC3 content
+ let resp = http
+ .put("http://localhost:8087/alice/calendar/Personal/rfc2.ics")
+ .header("If-None-Match", "*")
+ .body(ICAL_RFC3)
+ .send()?;
+ let rfc2_etag = resp.headers().get("etag").expect("etag must be set");
+ assert_eq!(resp.status(), 201);
+
+ // call REPORT <sync-collection>
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(dav_serialize(&sync_query(token_addrm)))
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 1);
+ let token_addrm_same = match &multistatus.extension {
+ Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x,
+ _ => anyhow::bail!("wrong content"),
+ };
+ assert!(token_addrm_same != token_addrm);
+
+ // Part 4: overwrite an event (rfc1) with new content
+ let resp = http
+ .put("http://localhost:8087/alice/calendar/Personal/rfc1.ics")
+ .header("If-Match", rfc2_etag)
+ .body(ICAL_RFC4)
+ .send()?;
+ assert_eq!(resp.status(), 201);
+
+ // call REPORT <sync-collection>
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(dav_serialize(&sync_query(token_addrm_same)))
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 1);
+ let token_addrm_same = match &multistatus.extension {
+ Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x,
+ _ => anyhow::bail!("wrong content"),
+ };
+ assert!(token_addrm_same != token_addrm);
+
+ // Unknown token must return 410 GONE.
+ // Token can be forgotten as we garbage collect the DAG.
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(dav_serialize(&sync_query("https://aerogramme.0/sync/000000000000000000000000000000000000000000000000")))
+ .send()?;
+ assert_eq!(resp.status(), 410);
+
+ Ok(())
+ })
+ .expect("test fully run")
+}
diff --git a/aerogramme/tests/common/constants.rs b/aerogramme/tests/common/constants.rs
new file mode 100644
index 0000000..16daec6
--- /dev/null
+++ b/aerogramme/tests/common/constants.rs
@@ -0,0 +1,243 @@
+use std::time;
+
+pub static SMALL_DELAY: time::Duration = time::Duration::from_millis(200);
+
+pub static EMAIL1: &[u8] = b"Date: Sat, 8 Jul 2023 07:14:29 +0200\r
+From: Bob Robert <bob@example.tld>\r
+To: Alice Malice <alice@example.tld>\r
+CC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>\r
+Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\r
+ =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\r
+X-Unknown: something something\r
+Bad entry\r
+ on multiple lines\r
+Message-ID: <NTAxNzA2AC47634Y366BAMTY4ODc5MzQyODY0ODY5@www.grrrndzero.org>\r
+MIME-Version: 1.0\r
+Content-Type: multipart/alternative;\r
+ boundary=\"b1_e376dc71bafc953c0b0fdeb9983a9956\"\r
+Content-Transfer-Encoding: 7bit\r
+\r
+This is a multi-part message in MIME format.\r
+\r
+--b1_e376dc71bafc953c0b0fdeb9983a9956\r
+Content-Type: text/plain; charset=utf-8\r
+Content-Transfer-Encoding: quoted-printable\r
+\r
+GZ\r
+OoOoO\r
+oOoOoOoOo\r
+oOoOoOoOoOoOoOoOo\r
+oOoOoOoOoOoOoOoOoOoOoOo\r
+oOoOoOoOoOoOoOoOoOoOoOoOoOoOo\r
+OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO\r
+\r
+--b1_e376dc71bafc953c0b0fdeb9983a9956\r
+Content-Type: text/html; charset=us-ascii\r
+\r
+<div style=\"text-align: center;\"><strong>GZ</strong><br />\r
+OoOoO<br />\r
+oOoOoOoOo<br />\r
+oOoOoOoOoOoOoOoOo<br />\r
+oOoOoOoOoOoOoOoOoOoOoOo<br />\r
+oOoOoOoOoOoOoOoOoOoOoOoOoOoOo<br />\r
+OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO<br />\r
+</div>\r
+\r
+--b1_e376dc71bafc953c0b0fdeb9983a9956--\r
+";
+
+pub static EMAIL2: &[u8] = b"From: alice@example.com\r
+To: alice@example.tld\r
+Subject: Test\r
+\r
+Hello world!\r
+";
+
+pub static ICAL_RFC1: &[u8] = b"BEGIN:VCALENDAR
+PRODID:-//Example Corp.//CalDAV Client//EN
+VERSION:2.0
+BEGIN:VEVENT
+UID:1@example.com
+SUMMARY:One-off Meeting
+DTSTAMP:20041210T183904Z
+DTSTART:20041207T120000Z
+DTEND:20041207T130000Z
+END:VEVENT
+BEGIN:VEVENT
+UID:2@example.com
+SUMMARY:Weekly Meeting
+DTSTAMP:20041210T183838Z
+DTSTART:20041206T120000Z
+DTEND:20041206T130000Z
+RRULE:FREQ=WEEKLY
+END:VEVENT
+BEGIN:VEVENT
+UID:2@example.com
+SUMMARY:Weekly Meeting
+RECURRENCE-ID:20041213T120000Z
+DTSTAMP:20041210T183838Z
+DTSTART:20041213T130000Z
+DTEND:20041213T140000Z
+END:VEVENT
+END:VCALENDAR
+";
+
+pub static ICAL_RFC2: &[u8] = b"BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Example Corp.//CalDAV Client//EN
+BEGIN:VEVENT
+UID:20010712T182145Z-123401@example.com
+DTSTAMP:20060712T182145Z
+DTSTART:20060714T170000Z
+DTEND:20060715T040000Z
+SUMMARY:Bastille Day Party
+END:VEVENT
+END:VCALENDAR
+";
+
+pub static ICAL_RFC3: &[u8] = b"BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Example Corp.//CalDAV Client//EN
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US/Eastern
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=US/Eastern:20060104T100000
+DURATION:PT1H
+SUMMARY:Event #3
+UID:DC6C50A017428C5216A2F1CD@example.com
+END:VEVENT
+END:VCALENDAR
+";
+
+pub static ICAL_RFC3_STRIPPED: &[u8] = b"BEGIN:VCALENDAR\r
+VERSION:2.0\r
+BEGIN:VTIMEZONE\r
+LAST-MODIFIED:20040110T032845Z\r
+TZID:US/Eastern\r
+BEGIN:DAYLIGHT\r
+DTSTART:20000404T020000\r
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\r
+TZNAME:EDT\r
+TZOFFSETFROM:-0500\r
+TZOFFSETTO:-0400\r
+END:DAYLIGHT\r
+BEGIN:STANDARD\r
+DTSTART:20001026T020000\r
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\r
+TZNAME:EST\r
+TZOFFSETFROM:-0400\r
+TZOFFSETTO:-0500\r
+END:STANDARD\r
+END:VTIMEZONE\r
+BEGIN:VEVENT\r
+DTSTART;TZID=US/Eastern:20060104T100000\r
+DURATION:PT1H\r
+UID:DC6C50A017428C5216A2F1CD@example.com\r
+END:VEVENT\r
+END:VCALENDAR\r
+";
+
+pub static ICAL_RFC4: &[u8] = br#"BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Example Corp.//CalDAV Client//EN
+BEGIN:VFREEBUSY
+ORGANIZER;CN="Bernard Desruisseaux":mailto:bernard@example.com
+UID:76ef34-54a3d2@example.com
+DTSTAMP:20050530T123421Z
+DTSTART:20060101T000000Z
+DTEND:20060108T000000Z
+FREEBUSY:20050531T230000Z/20050601T010000Z
+FREEBUSY;FBTYPE=BUSY-TENTATIVE:20060102T100000Z/20060102T120000Z
+FREEBUSY:20060103T100000Z/20060103T120000Z
+FREEBUSY:20060104T100000Z/20060104T120000Z
+FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20060105T100000Z/20060105T120000Z
+FREEBUSY:20060106T100000Z/20060106T120000Z
+END:VFREEBUSY
+END:VCALENDAR
+"#;
+
+pub static ICAL_RFC5: &[u8] = br#"BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Example Corp.//CalDAV Client//EN
+BEGIN:VTODO
+DTSTAMP:20060205T235600Z
+DUE;VALUE=DATE:20060101
+LAST-MODIFIED:20060205T235308Z
+SEQUENCE:1
+STATUS:CANCELLED
+SUMMARY:Task #4
+UID:E10BA47467C5C69BB74E8725@example.com
+END:VTODO
+END:VCALENDAR
+"#;
+
+pub static ICAL_RFC6: &[u8] = br#"BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Example Corp.//CalDAV Client//EN
+BEGIN:VTODO
+DTSTART:20060205T235335Z
+DUE;VALUE=DATE:20060104
+STATUS:NEEDS-ACTION
+SUMMARY:Task #1
+UID:DDDEEB7915FA61233B861457@example.com
+BEGIN:VALARM
+ACTION:AUDIO
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VTODO
+END:VCALENDAR
+"#;
+
+pub static ICAL_RFC7: &[u8] = br#"BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Example Corp.//CalDAV Client//EN
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US/Eastern
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
+DTSTAMP:20090206T001220Z
+DTSTART;TZID=US/Eastern:20090104T100000
+DURATION:PT1H
+LAST-MODIFIED:20090206T001330Z
+ORGANIZER:mailto:cyrus@example.com
+SEQUENCE:1
+STATUS:TENTATIVE
+SUMMARY:Event #3
+UID:DC6C50A017428C5216A2F1CA@example.com
+X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
+END:VEVENT
+END:VCALENDAR
+"#;
diff --git a/tests/common/fragments.rs b/aerogramme/tests/common/fragments.rs
index 606af2b..606af2b 100644
--- a/tests/common/fragments.rs
+++ b/aerogramme/tests/common/fragments.rs
diff --git a/tests/common/mod.rs b/aerogramme/tests/common/mod.rs
index cbe0271..bc65305 100644
--- a/tests/common/mod.rs
+++ b/aerogramme/tests/common/mod.rs
@@ -8,10 +8,13 @@ use std::net::{Shutdown, TcpStream};
use std::process::Command;
use std::thread;
+use reqwest::blocking::Client;
+use reqwest::header;
+
use constants::SMALL_DELAY;
pub fn aerogramme_provider_daemon_dev(
- mut fx: impl FnMut(&mut TcpStream, &mut TcpStream) -> Result<()>,
+ mut fx: impl FnMut(&mut TcpStream, &mut TcpStream, &mut Client) -> Result<()>,
) -> Result<()> {
// Check port is not used (= free) before starting the test
let mut max_retry = 20;
@@ -53,8 +56,15 @@ pub fn aerogramme_provider_daemon_dev(
let mut lmtp_socket =
TcpStream::connect("[::1]:1025").context("lmtp socket must be connected")?;
- println!("-- ready to test imap features --");
- let result = fx(&mut imap_socket, &mut lmtp_socket);
+ let mut headers = header::HeaderMap::new();
+ headers.insert(
+ header::AUTHORIZATION,
+ header::HeaderValue::from_static("Basic YWxpY2U6aHVudGVyMg=="),
+ );
+ let mut http_client = Client::builder().default_headers(headers).build()?;
+
+ println!("-- ready to test features --");
+ let result = fx(&mut imap_socket, &mut lmtp_socket, &mut http_client);
println!("-- test teardown --");
imap_socket
@@ -97,3 +107,30 @@ pub fn read_first_u32(inp: &str) -> Result<u32> {
.collect::<String>()
.parse::<u32>()?)
}
+
+use aero_dav::xml::{Node, Reader, Writer};
+use tokio::io::AsyncWriteExt;
+pub fn dav_deserialize<T: Node<T>>(src: &str) -> T {
+ futures::executor::block_on(async {
+ let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes()))
+ .await
+ .expect("build reader");
+ rdr.find().await.expect("parse XML")
+ })
+}
+pub fn dav_serialize<T: Node<T>>(src: &T) -> String {
+ futures::executor::block_on(async {
+ let mut buffer = Vec::new();
+ let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
+ let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
+ let ns_to_apply = vec![
+ ("xmlns:D".into(), "DAV:".into()),
+ ("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()),
+ ];
+ let mut writer = Writer { q, ns_to_apply };
+
+ src.qwrite(&mut writer).await.expect("xml serialization");
+ tokio_buffer.flush().await.expect("tokio buffer flush");
+ std::str::from_utf8(buffer.as_slice()).unwrap().into()
+ })
+}
diff --git a/doc/.gitignore b/doc/.gitignore
deleted file mode 100644
index 7585238..0000000
--- a/doc/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-book
diff --git a/doc/book.toml b/doc/book.toml
deleted file mode 100644
index 338ad63..0000000
--- a/doc/book.toml
+++ /dev/null
@@ -1,9 +0,0 @@
-[book]
-authors = ["Quentin Dufour"]
-language = "en"
-multilingual = false
-src = "src"
-title = "Aerogramme - Encrypted e-mail storage over Garage"
-
-[output.html]
-mathjax-support = true
diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md
deleted file mode 100644
index 92d7932..0000000
--- a/doc/src/SUMMARY.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Summary
-
-[Introduction](./index.md)
-
-# Quick start
-
-- [Installation](./installation.md)
-- [Setup](./setup.md)
-- [Validation](./validate.md)
-
-# Cookbook
-
- - [Not ready for production]()
-
-# Reference
-
-- [Configuration file](./config.md)
-- [RFC coverage](./rfc.md)
-
-# Design
-
-- [Overview](./overview.md)
-- [Mailboxes](./mailbox.md)
-- [Mutation Log](./log.md)
-- [IMAP UID proof](./imap_uid.md)
-
-# Internals
-
-- [Persisted data structures](./data_format.md)
-- [Cryptography & key management](./crypt-key.md)
-
-# Development
-
-- [Notes](./notes.md)
diff --git a/doc/src/aero-compo.png b/doc/src/aero-compo.png
deleted file mode 100644
index fb81b46..0000000
--- a/doc/src/aero-compo.png
+++ /dev/null
Binary files differ
diff --git a/doc/src/aero-paranoid.png b/doc/src/aero-paranoid.png
deleted file mode 100644
index f9e2df1..0000000
--- a/doc/src/aero-paranoid.png
+++ /dev/null
Binary files differ
diff --git a/doc/src/aero-schema.png b/doc/src/aero-schema.png
deleted file mode 100644
index 3206245..0000000
--- a/doc/src/aero-schema.png
+++ /dev/null
Binary files differ
diff --git a/doc/src/aero-states.png b/doc/src/aero-states.png
deleted file mode 100644
index c3b015a..0000000
--- a/doc/src/aero-states.png
+++ /dev/null
Binary files differ
diff --git a/doc/src/aero-states2.png b/doc/src/aero-states2.png
deleted file mode 100644
index ed2077d..0000000
--- a/doc/src/aero-states2.png
+++ /dev/null
Binary files differ
diff --git a/doc/src/aerogramme.jpg b/doc/src/aerogramme.jpg
deleted file mode 100644
index c1fe11b..0000000
--- a/doc/src/aerogramme.jpg
+++ /dev/null
Binary files differ
diff --git a/doc/src/config.md b/doc/src/config.md
deleted file mode 100644
index 732ecb7..0000000
--- a/doc/src/config.md
+++ /dev/null
@@ -1,126 +0,0 @@
-# Configuration file
-
-A configuration file that illustrate all the possible options,
-in practise, many fields are omitted:
-
-```toml
-s3_endpoint = "s3.garage.tld"
-k2v_endpoint = "k2v.garage.tld"
-aws_region = "garage"
-
-[lmtp]
-bind_addr = "[::1]:2525"
-hostname = "aerogramme.tld"
-
-[imap]
-bind_addr = "[::1]:993"
-
-[login_static]
-default_bucket = "aerogramme"
-
-[login_static.user.alan]
-email_addresses = [
- "alan@smith.me"
- "aln@example.com"
-]
-password = "$argon2id$v=19$m=4096,t=3,p=1$..."
-
-aws_access_key_id = "GK..."
-aws_secret_access_key = "c0ffee"
-bucket = "aerogramme-alan"
-
-user_secret = "s3cr3t"
-alternate_user_secrets = [ "s3cr3t2" "s3cr3t3" ]
-
-master_key = "..."
-secret_key = "..."
-
-[login_ldap]
-ldap_server = "ldap.example.com"
-
-pre_bind_on_login = true
-bind_dn = "cn=admin,dc=example,dc=com"
-bind_password = "s3cr3t"
-
-search_base = "ou=users,dc=example,dc=com"
-username_attr = "cn"
-mail_attr = "mail"
-
-aws_access_key_id_attr = "garage_s3_access_key"
-aws_secret_access_key_attr = "garage_s3_secret_key"
-user_secret_attr = "secret"
-alternate_user_secrets_attr = "secret_alt"
-
-# bucket = "aerogramme"
-bucket_attr = "bucket"
-
-```
-
-## Global configuration options
-
-### `s3_endpoint`
-
-### `k2v_endpoint`
-
-### `aws_region`
-
-## LMTP configuration options
-
-### `lmtp.bind_addr`
-
-### `lmtp.hostname`
-
-## IMAP configuration options
-
-### `imap.bind_addr`
-
-## Static login configuration options
-
-### `login_static.default_bucket`
-
-### `login_static.user.<name>.email_addresses`
-
-### `login_static.user.<name>.password`
-
-### `login_static.user.<name>.aws_access_key_id`
-
-### `login_static.user.<name>.aws_secret_access_key`
-
-### `login_static.user.<name>.bucket`
-
-### `login_static.user.<name>.user_secret`
-
-### `login_static.user.<name>.master_key`
-
-### `login_static.user.<name>.secret_key`
-
-## LDAP login configuration options
-
-### `login_ldap.ldap_server`
-
-### `login_ldap.pre_bind_on`
-
-### `login_ldap.bind_dn`
-
-### `login_ldap.bind_password`
-
-### `login_ldap.search_base`
-
-### `login_ldap.username_attr`
-
-### `login_ldap.mail_attr`
-
-### `login_ldap.aws_access_key_id_attr`
-
-### `login_ldap.aws_secret_access_key_attr`
-
-### `login_ldap.user_secret_attr`
-
-### `login_ldap.alternate_user_secrets_attr`
-
-### `login_ldap.bucket`
-
-### `login_ldap.bucket_attr`
-
-
-
diff --git a/doc/src/crypt-key.md b/doc/src/crypt-key.md
deleted file mode 100644
index 9fb199b..0000000
--- a/doc/src/crypt-key.md
+++ /dev/null
@@ -1,82 +0,0 @@
-# Cryptography & key management
-
-Keys that are used:
-
-- master secret key (for indexes)
-- curve25519 public/private key pair (for incoming mail)
-
-Keys that are stored in K2V under PK `keys`:
-
-- `public`: the public curve25519 key (plain text)
-- `salt`: the 32-byte salt `S` used to calculate digests that index keys below
-- if a password is used, `password:<truncated(128bit) argon2 digest of password using salt S>`:
- - a 32-byte salt `Skey`
- - followed a secret box
- - that is encrypted with a strong argon2 digest of the password (using the salt `Skey`) and a user secret (see below)
- - that contains the master secret key and the curve25519 private key
-
-User secret: an additionnal secret that is added to the password when deriving the encryption key for the secret box.
-This additionnal secret should not be stored in K2V/S3, so that just knowing a user's password isn't enough to be able
-to decrypt their mailbox (supposing the attacker has a dump of their K2V/S3 bucket).
-This user secret should typically be stored in the LDAP database or just in the configuration file when using
-the static login provider.
-
-Operations:
-
-- **Initialize**(`user_secret`, `password`):
- - if `"salt"` or `"public"` already exist, BAIL
- - generate salt `S` (32 random bytes)
- - generate `public`, `private` (curve25519 keypair)
- - generate `master` (secretbox secret key)
- - calculate `digest = argon2_S(password)`
- - generate salt `Skey` (32 random bytes)
- - calculate `key = argon2_Skey(user_secret + password)`
- - serialize `box_contents = (private, master)`
- - seal box `blob = seal_key(box_contents)`
- - write `S` at `"salt"`
- - write `concat(Skey, blob)` at `"password:{hex(digest[..16])}"`
- - write `public` at `"public"`
-
-- **InitializeWithoutPassword**(`private`, `master`):
- - if `"salt"` or `"public"` already exist, BAIL
- - generate salt `S` (32 random bytes)
- - write `S` at `"salt"`
- - calculate `public` the public key associated with `private`
- - write `public` at `"public"`
-
-- **Open**(`user_secret`, `password`):
- - load `S = read("salt")`
- - calculate `digest = argon2_S(password)`
- - load `blob = read("password:{hex(digest[..16])}")
- - set `Skey = blob[..32]`
- - calculate `key = argon2_Skey(user_secret + password)`
- - open secret box `box_contents = open_key(blob[32..])`
- - retrieve `master` and `private` from `box_contents`
- - retrieve `public = read("public")`
-
-- **OpenWithoutPassword**(`private`, `master`):
- - load `public = read("public")`
- - check that `public` is the correct public key associated with `private`
-
-- **AddPassword**(`user_secret`, `existing_password`, `new_password`):
- - load `S = read("salt")`
- - calculate `digest = argon2_S(existing_password)`
- - load `blob = read("existing_password:{hex(digest[..16])}")
- - set `Skey = blob[..32]`
- - calculate `key = argon2_Skey(user_secret + existing_password)`
- - open secret box `box_contents = open_key(blob[32..])`
- - retrieve `master` and `private` from `box_contents`
-
- - calculate `digest_new = argon2_S(new_password)`
- - generate salt `Skeynew` (32 random bytes)
- - calculate `key_new = argon2_Skeynew(user_secret + new_password)`
- - serialize `box_contents_new = (private, master)`
- - seal box `blob_new = seal_key_new(box_contents_new)`
- - write `concat(Skeynew, blob_new)` at `"new_password:{hex(digest_new[..16])}"`
-
-- **RemovePassword**(`password`):
- - load `S = read("salt")`
- - calculate `digest = argon2_S(existing_password)`
- - check that `"password:{hex(digest[..16])}"` exists
- - check that other passwords exist ?? (or not)
- - delete `"password:{hex(digest[..16])}"`
diff --git a/doc/src/data_format.md b/doc/src/data_format.md
deleted file mode 100644
index 32aa2c3..0000000
--- a/doc/src/data_format.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# Data format
-
-## Bay(ou)
-
-Checkpoints are stored in S3 at `<path>/checkpoint/<timestamp>`. Example:
-
-```
-348 TestMailbox/checkpoint/00000180d77400dc126b16aac546b769
-369 TestMailbox/checkpoint/00000180d776e509b68fdc5c376d0abc
-357 TestMailbox/checkpoint/00000180d77a7fe68f4f76e3b45aa751
-```
-
-Operations are stored in K2V at PK `<path>`, SK `<timestamp>`. Example:
-
-```
-TestMailbox 00000180d77400dc126b16aac546b769 RcIsESv7WrjMuHwyI/dvCnkIfy6op5Tiylf0WSnn94aMS2uagl7YeMBwdv09TiSXBpu5nJ5e/9QFSfuEI/NqKrdQkX54MOsnaIGhRb0oqUG3KNaar3BiVSvYvXuzYhk4ii+TUS2Eyd6fCCaNVNM5
-TestMailbox 00000180d775f27f5542a13fc21c665e RrTSOup/zO1Ei+QrjBcDLt4vvFSY+WJPBodwY64wy2ftW+Oh3VSArvlO4SAEPmdsx1gt0HPBZYR/OkVWsZpmix1ZLFUmvdib+rjNkorHQW1p+oLVK8tolGrqk4SRwl88cqu466T4vBEpDu7tRbH0
-TestMailbox 00000180d775f292b3c8da00718389b4 VAwd8SRycIwsipZW5AcSG+EIYZVWn/Uj/TADbWhb4x5LVMceiRBHWVquY08RgT/lJKdhIcUqBA15bVG3klIg8tLsWJVG784NbsZwdGRczWmngcA=
-TestMailbox 00000180d775f29d24842cf375d679e0 /FbXtEwm/bijtvOdqM1XFvKUalQFAOPHp+vF9jZThZn/viY5a6W1PyHeI8kTusF6EsVPAwPHpQyjIv/ghskC0f+zUEsSUhDwQANdwLNqDLAvTA==
-TestMailbox 00000180d7768ab1dc01ff504e887c62 W/fF0WitpxJ05yHeOv96BlpGymT1kVOjkIW00t9e6UE7mxkvNflu9cZSCd8PDJd2ymC0sC9bLVFAXKmNZsmCFEEHMQSyrX61qTYo4KFCZMp5zm6fXubaYuurrzjXzfUP/R7kBvICFZlF0daf0SwX
-TestMailbox 00000180d7768aba629c7ad6adf25228 IPzYGNsSepCX2AEnee/1Eas9a3c5esPSmrNkvaj4XcFb6Ft2KC8N6ubUR3wB+K0oYCTQym6nhHG5dlAxf6NRu7Rk8YtBTBmSqtGqd6kMZ3bU5b8=
-TestMailbox 00000180d7768ac1870cda61784114d4 aaLiaWxfx1mxh6aoKE3xUUfZWhivZ/K7ixabflFDW7FO/qbpvCaa+Y6w4lQemTy6m+leAhXGN+Dbyv2qP20yJ9O4oJF5d3Lz5Iv5uF18OxhVZzw=
-TestMailbox 00000180d776e4fb294ccdab2612b406 EtUPrLgEeOyab2QRnSie4I3Me9dDh10UdwWnUKdGa/8ezMJDtiy7XlW+tUfJdqtu6Vj7nduT0emDOXbBZsNwlcmzgYNwuNu3I9AfhZTFWtwLgB+wnAgB/jim82DDrJfLia8kB2eA2ao5jfJ3uMSZ
-TestMailbox 00000180d776e501528546d340490291 Lz4Z9wCTk1lZ86lL01urhAan4oHcr1NBqdRe+CDpA51D9IncA5+Fhc8I6knUIh2qQ5/woWgISLAVwzSS+0+TxrYoqxf5FumIQtUJfwDER5La3n0=
-TestMailbox 00000180d776e509b68fdc5c376d0abc RUGE2xB3fFX/wRH/p2fHIUa+rMaXSRd7fY9zglw0pRfVPqJfpniOjAe4GHIwGlwbwjtFOwS5a+Q7yr0Wez6QwD+ohhqRFKpbjcFcN7VfMyVAf+k=
-TestMailbox 00000180d7784b987a8ad8106dc400c9 K+0LVEtBbTnWNS67jy9DtTvQyd5arovduvu490tLOE2TzVhuVoF4pfvTMTN12bH3KwEAHeDfuwKkKJFqldOywouTYPzEjZFkJzyagHrkl6dfnE5CqmlDv+Vc5TOQRskxjW+wQiZdjU8wGiBiBGYh
-TestMailbox 00000180d7784bede69ac3cff2c6b724 XMFY3+b1r1//uolVz80JSI3g/84XCk3Tm7/S0BFv+Qe/Xv3/poLrOvAKEe+GzD2s22j8p/T2RXR/JSZckzgjEZeO0wbPDXVQd94di2Pff7jxAH8=
-TestMailbox 00000180d7784bffe2595abe7ed81858 QQZhF+7wSHfikoAp93a+UY/XDIX7TVnnVYOtmQ2XHnDKA2F6snRJCPbYBO4IRHCRfVrjDGi32c41it2C3Mu5PBepabxapsW1rfIV3rlX2lkKHtI=
-TestMailbox 00000180d77a7fb3f01dbb147c20cf7f IHOlOa1JI11RUKVvQUq3HQPxiRr4UCeE+pHmL8DtNMkOh62V4spuP0VvvQTJCQcPQ1EQR/QcxZ3s7uHLkrZAHF30BkpUkGqsLBWpnyug/puhdiixWsMyLLb6G90zFjiComUwptnDc/CCXtGEHdSW
-TestMailbox 00000180d77a7fbb54b100f521ceb347 Ze4KyyTCgrYbZlXlJSY5hNob8sMXvBAmwIx2cADbX5P0M1IHXwXfloEzvvd6WYOtatFC2GnDSrmQ6RdCfeZ3WV9TZilqa0Fv0XEg48sVyVCcguw=
-TestMailbox 00000180d77a7fe68f4f76e3b45aa751 cJJVvvRzTVNKUaIHPCCDY2uY7/HlmkxGgo3ozWBlBSRDeBqU65zgZD3QIPCxa6xaqB/Gc0bQ9BGzfU0cvVmO5jgNeeDnbqqs3oeA2jml/Qv2YO9upApfNQtDT1GiwJ8vrgaIow==
-TestMailbox 00000180d8e513d3ea58c679a13178ac Ce5su2YOxNmTzk2dK8SX8V/Uue5uAC7oklEjhesY9wCMqGphhOkdWjzCqq0xOzcb/ZzzZ58t+mTksNSYIU4kddHIHBFPgqIwKthVk2mlUdqYiN/Y2vEGqv+YmtKY+GST/7Ee87ZHpU/5sv0GoXxT
-TestMailbox 00000180d8e5145a23f8faee86283900 sp3D8xFZcM9icNlDJXIUDJb3mo6VGD9f1aDHD+4RbPdx6mTYF+qNTsPHKCxHHxT/9NfNe8XPg2+8xYRtm7SXfgERZBDB8ye+Xt3fM1k+wbL6RsaJmDHVECeXeL5KHuITzpI22A==
-TestMailbox 00000180d8e51465c38f0585f9bb760e FF0VId2O/bBNzYD5ABWReMs5hHoHwynOoJRKj9vyaUMZ3JykInFmvvRgtCbJBDjTQPwPU8apphKQfwuicO76H7GtZqH009Cbv5l8ZTRJKrmzOQmtjzBQc2eGEUMPfbml5t0GCg==
-```
-
-The timestamp of a checkpoint corresponds to the timestamp of the first operation NOT included in the checkpoint.
-In other words, to reconstruct the final state:
-
-- find timestamp `<ts>` of last checkpoint
-- load checkpoint `<ts>`
-- load and apply all operations starting from `<ts>`, included
-
-## UID index
-
-The UID index is an application of the Bayou storage module
-used to assign UID numbers to e-mails.
-See document we sent to NGI for properties on UIDVALIDITY.
-
-
diff --git a/doc/src/imap_uid.md b/doc/src/imap_uid.md
deleted file mode 100644
index ecdd52b..0000000
--- a/doc/src/imap_uid.md
+++ /dev/null
@@ -1,203 +0,0 @@
-# IMAP UID proof
-
-**Notations**
-
-- $h$: the hash of a message, $\mathbb{H}$ is the set of hashes
-- $i$: the UID of a message $(i \in \mathbb{N})$
-- $f$: a flag attributed to a message (it's a string), we write
- $\mathbb{F}$ the set of possible flags
-- if $M$ is a map (aka a dictionnary), if $x$ has no assigned value in
- $M$ we write $M [x] = \bot$ or equivalently $x \not\in M$. If $x$ has a value
- in the map we write $x \in M$ and $M [x] \neq \bot$
-
-**State**
-
-- A map $I$ such that $I [h]$ is the UID of the message whose hash is
- $h$ is the mailbox, or $\bot$ if there is no such message
-
-- A map $F$ such that $F [h]$ is the set of flags attributed to the
- message whose hash is $h$
-
-- $v$: the UIDVALIDITY value
-
-- $n$: the UIDNEXT value
-
-- $s$: an internal sequence number that is mostly equal to UIDNEXT but
- also grows when mails are deleted
-
-**Operations**
-
- - MAIL\_ADD$(h, i)$: the value of $i$ that is put in this operation is
- the value of $s$ in the state resulting of all already known operations,
- i.e. $s (O_{gen})$ in the notation below where $O_{gen}$ is
- the set of all operations known at the time when the MAIL\_ADD is generated.
- Moreover, such an operation can only be generated if $I (O_{gen}) [h]
- = \bot$, i.e. for a mail $h$ that is not already in the state at
- $O_{gen}$.
-
- - MAIL\_DEL$(h)$
-
- - FLAG\_ADD$(h, f)$
-
- - FLAG\_DEL$(h, f)$
-
-**Algorithms**
-
-
-**apply** MAIL\_ADD$(h, i)$:
-&nbsp;&nbsp; *if* $i < s$:
-&nbsp;&nbsp;&nbsp;&nbsp; $v \leftarrow v + s - i$
-&nbsp;&nbsp; *if* $F [h] = \bot$:
-&nbsp;&nbsp;&nbsp;&nbsp; $F [h] \leftarrow F_{initial}$
-&nbsp;&nbsp;$I [h] \leftarrow s$
-&nbsp;&nbsp;$s \leftarrow s + 1$
-&nbsp;&nbsp;$n \leftarrow s$
-
-**apply** MAIL\_DEL$(h)$:
-&nbsp;&nbsp; $I [h] \leftarrow \bot$
-&nbsp;&nbsp;$F [h] \leftarrow \bot$
-&nbsp;&nbsp;$s \leftarrow s + 1$
-
-**apply** FLAG\_ADD$(h, f)$:
-&nbsp;&nbsp; *if* $h \in F$:
-&nbsp;&nbsp;&nbsp;&nbsp; $F [h] \leftarrow F [h] \cup \{ f \}$
-
-**apply** FLAG\_DEL$(h, f)$:
-&nbsp;&nbsp; *if* $h \in F$:
-&nbsp;&nbsp;&nbsp;&nbsp; $F [h] \leftarrow F [h] \backslash \{ f \}$
-
-
-**More notations**
-
-- $o$ is an operation such as MAIL\_ADD, MAIL\_DEL, etc. $O$ is a set of
- operations. Operations embed a timestamp, so a set of operations $O$ can be
- written as $O = [o_1, o_2, \ldots, o_n]$ by ordering them by timestamp.
-
-- if $o \in O$, we write $O_{\leqslant o}$, $O_{< o}$, $O_{\geqslant
- o}$, $O_{> o}$ the set of items of $O$ that are respectively earlier or
- equal, strictly earlier, later or equal, or strictly later than $o$. In
- other words, if we write $O = [o_1, \ldots, o_n]$, where $o$ is a certain
- $o_i$ in this sequence, then:
-$$
-\begin{aligned}
-O_{\leqslant o} &= \{ o_1, \ldots, o_i \}\\
-O_{< o} &= \{ o_1, \ldots, o_{i - 1} \}\\
-O_{\geqslant o} &= \{ o_i, \ldots, o_n \}\\
-O_{> o} &= \{ o_{i + 1}, \ldots, o_n \}
-\end{aligned}
-$$
-
-- If $O$ is a set of operations, we write $I (O)$, $F (O)$, $n (O), s
- (O)$, and $v (O)$ the values of $I, F, n, s$ and $v$ in the state that
- results of applying all of the operations in $O$ in their sorted order. (we
- thus write $I (O) [h]$ the value of $I [h]$ in this state)
-
-**Hypothesis:**
-An operation $o$ can only be in a set $O$ if it was
-generated after applying operations of a set $O_{gen}$ such that
-$O_{gen} \subset O$ (because causality is respected in how we deliver
-operations). Sets of operations that do not respect this property are excluded
-from all of the properties, lemmas and proofs below.
-
-**Simplification:** We will now exclude FLAG\_ADD and FLAG\_DEL
-operations, as they do not manipulate $n$, $s$ and $v$, and adding them should
-have no impact on the properties below.
-
-**Small lemma:** If there are no FLAG\_ADD and FLAG\_DEL operations,
-then $s (O) = | O |$. This is easy to see because the possible operations are
-only MAIL\_ADD and MAIL\_DEL, and both increment the value of $s$ by 1.
-
-**Defnition:** If $o$ is a MAIL\_ADD$(h, i)$ operation, and $O$ is a
-set of operations such that $o \in O$, then we define the following value:
-$$
-C (o, O) = s (O_{< o}) - i
-$$
-We say that $C (o, O)$ is the *number of conflicts of $o$ in $O$*: it
-corresponds to the number of operations that were added before $o$ in $O$ that
-were not in $O_{gen}$.
-
-**Property:**
-
-We have that:
-
-$$
-v (O) = \sum_{o \in O} C (o, O)
-$$
-
-Or in English: $v (O)$ is the sum of the number of conflicts of all of the
-MAIL\_ADD operations in $O$. This is easy to see because indeed $v$ is
-incremented by $C (o, O)$ for each operation $o \in O$ that is applied.
-
-
-**Property:**
- If $O$ and $O'$ are two sets of operations, and $O \subseteq O'$, then:
-
-$$
-\begin{aligned}
-\forall o \in O, \qquad C (o, O) \leqslant C (o, O')
-\end{aligned}
-$$
-
-This is easy to see because $O_{< o} \subseteq O'_{< o}$ and $C (o, O') - C
- (o, O) = s (O'_{< o}) - s (O_{< o}) = | O'_{< o} | - | O_{< o} | \geqslant
- 0$
-
-**Theorem:**
-
-If $O$ and $O'$ are two sets of operations:
-
-$$
-\begin{aligned}
-O \subseteq O' & \Rightarrow & v (O) \leqslant v (O')
-\end{aligned}
-$$
-
-**Proof:**
-
-$$
-\begin{aligned}
-v (O') &= \sum_{o \in O'} C (o, O')\\
- & \geqslant \sum_{o \in O} C (o, O') \qquad \text{(because $O \subseteq
- O'$)}\\
- & \geqslant \sum_{o \in O} C (o, O) \qquad \text{(because $\forall o \in
- O, C (o, O) \leqslant C (o, O')$)}\\
- & \geqslant v (O)
-\end{aligned}
-$$
-
-**Theorem:**
-
-If $O$ and $O'$ are two sets of operations, such that $O \subset O'$,
-
-and if there are two different mails $h$ and $h'$ $(h \neq h')$ such that $I
- (O) [h] = I (O') [h']$
-
- then:
- $$v (O) < v (O')$$
-
-**Proof:**
-
-We already know that $v (O) \leqslant v (O')$ because of the previous theorem.
-We will now look at the sum:
-$$
-v (O') = \sum_{o \in O'} C (o, O')
-$$
-and show that there is at least one term in this sum that is strictly larger
-than the corresponding term in the other sum:
-$$
-v (O) = \sum_{o \in O} C (o, O)
-$$
-Let $o$ be the last MAIL\_ADD$(h, \_)$ operation in $O$, i.e. the operation
-that gives its definitive UID to mail $h$ in $O$, and similarly $o'$ be the
-last MAIL\_ADD($h', \_$) operation in $O'$.
-
-Let us write $I = I (O) [h] = I (O') [h']$
-
-$o$ is the operation at position $I$ in $O$, and $o'$ is the operation at
-position $I$ in $O'$. But $o \neq o'$, so if $o$ is not the operation at
-position $I$ in $O'$ then it has to be at a later position $I' > I$ in $O'$,
-because no operations are removed between $O$ and $O'$, the only possibility
-is that some other operations (including $o'$) are added before $o$. Therefore
-we have that $C (o, O') > C (o, O)$, i.e. at least one term in the sum above
-is strictly larger in the first sum than in the second one. Since all other
-terms are greater or equal, we have $v (O') > v (O)$.
diff --git a/doc/src/index.md b/doc/src/index.md
deleted file mode 100644
index 9d8f910..0000000
--- a/doc/src/index.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# Introduction
-
-<p align="center" style="text-align:center;">
- <img alt="A scan of an Aerogramme dating from 1955" src="./aerogramme.jpg" style="margin:auto; max-width:300px"/>
-<br>
-[ <strong><a href="https://aerogramme.deuxfleurs.fr/">Documentation</a></strong>
-| <a href="https://git.deuxfleurs.fr/Deuxfleurs/aerogramme">Git repository</a>
-]
-<br>
-<em>stability status: technical preview (do not use in production)</em>
-</p>
-
-Aerogramme is an open-source **IMAP server** targeted at **distributed** infrastructures and written in **Rust**.
-It is designed to be resilient, easy to operate and private by design.
-
-**Resilient** - Aerogramme is built on top of Garage, a (geographically) distributed object storage software. Aerogramme thus inherits Garage resiliency: its mailboxes are spread on multiple distant regions, regions can go offline while keeping mailboxes available, storage nodes can be added or removed on the fly, etc.
-
-**Easy to operate** - Aerogramme mutualizes the burden of data management by storing all its data in an object store and nothing on the local filesystem or any relational database. It can be seen as a proxy between the IMAP protocol and Garage protocols (S3 and K2V). It can thus be freely moved between machines. Multiple instances can also be run in parallel.
-
-**Private by design** - As emails are very sensitive, Aerogramme encrypts users' mailboxes with their passwords. Data is decrypted in RAM upon user login: the Garage storage layer handles only encrypted blobs. It is even possible to run locally Aerogramme while connecting it to a remote, third-party, untrusted Garage provider; in this case clear text emails never leak outside of your computer.
-
-Our main use case is to provide a modern email stack for autonomously hosted communities such as [Deuxfleurs](https://deuxfleurs.fr). More generally, we want to set new standards in term of email ethic by lowering the bar to become an email provider while making it harder to spy users' emails.
diff --git a/doc/src/installation.md b/doc/src/installation.md
deleted file mode 100644
index 7f722e7..0000000
--- a/doc/src/installation.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Installation
-
-Install a Rust nightly toolchain: [go to Rustup](https://rustup.rs/).
-
-Install and deploy a Garage cluster: [go to Garage documentation](https://garagehq.deuxfleurs.fr/documentation/quick-start/). Make sure that you download a binary that supports K2V. Currently, you will find them in the "Extra build" section of the Download page.
-
-Clone Aerogramme's repository:
-
-```bash
-git clone https://git.deuxfleurs.fr/Deuxfleurs/aerogramme/
-```
-
-Compile Aerogramme:
-
-```bash
-cargo build
-```
-
-Check that your compiled binary works:
-
-```bash
-cargo run
-```
-
-You are now ready to [setup Aerogramme!](./setup.md)
diff --git a/doc/src/log.md b/doc/src/log.md
deleted file mode 100644
index f29ecee..0000000
--- a/doc/src/log.md
+++ /dev/null
@@ -1,149 +0,0 @@
-# Mutation Log
-
-
-Back to our data structure, we note that one major challenge with this project is to *correctly* handle mutable data.
-With our current design, multiple processes can interact with the same mutable data without coordination, and we need a way to detect and solve conflicts.
-Directly storing the result in a single k2v key would not work as we have no transaction or lock mechanism, and our state would be always corrupted.
-Instead, we choose to record an ordered log of operations, ie. transitions, that each client can use locally to rebuild the state, each transition has its own immutable identifier.
-This technique is sometimes referred to as event sourcing.
-
-With this system, we can't have conflict anymore at Garage level, but conflicts at the IMAP level can still occur, like 2 processes assigning the same identifier to different emails.
-We thus need a logic to handle these conflicts that is flexible enough to accommodate the application's specific logic.
-
-Our solution is inspired by the work conducted by Terry et al. on [Bayou](https://dl.acm.org/doi/10.1145/224056.224070).
-Clients fetch regularly the log from Garage, each entry is ordered by a timestamp and a unique identifier.
-One of the 2 conflicting clients will be in the state where it has executed a log entry in the wrong order according to the specified ordering.
-This client will need to roll back its changes to reapply the log in the same order as the others, and on conflicts, the same logic will be applied by all the clients to get, in the end, the same state.
-
-**Command definitions**
-
-The log is made of a sequence of ordered commands that can be run to get a deterministic state in the end.
-We define the following commands:
-
-`FLAG_ADD <email_uuid> <flag>` - Add a flag to the target email
-`FLAG_DEL <email_uuid> <flag>` - Remove a flag from a target email
-`MAIL_DEL <email_uuid>` - Remove an email
-`MAIL_ADD <email_uuid> <uid>` - Register an email in the mailbox with the given identifier
-`REMOTE <s3 url>` - Command is not directly stored here, instead it must be fetched from S3, see batching to understand why.
-
-*Note: FLAG commands could be enhanced with a MODSEQ field similar to the uid field for the emails, in order to implement IMAP RFC4551. Adding this field would force us to handle conflicts on flags
-the same way as on emails, as MODSEQ must be monotonically incremented but is reset by a uid-validity change. This is out of the scope of this document.*
-
-**A note on UUID**
-
-When adding an email to the system, we associate it with a *universally unique identifier* or *UUID.*
-We can then reference this email in the rest of the system without fearing a conflict or a race condition are we are confident that this UUID is unique.
-
-We could have used the email hash instead, but we identified some benefits in using UUID.
-First, sometimes a mail must be duplicated, because the user received it from 2 different sources, so it is more correct to have 2 entries in the system.
-Additionally, UUIDs are smaller and better compressible than a hash, which will lead to better performances.
-
-**Batching commands**
-
-Commands that are executed at the same time can be batched together.
-Let's imagine a user is deleting its trash containing thousands of emails.
-Instead of writing thousands of log lines, we can append them in a single entry.
-If this entry becomes big (eg. > 100 commands), we can store it to S3 with the `REMOTE` command.
-Batching is important as we want to keep the number of log entries small to be able to fetch them regularly and quickly.
-
-## Fixing conflicts in the operation log
-
-The log is applied in order from the last checkpoint.
-To stay in sync, the client regularly asks the server for the last commands.
-
-When the log is applied, our system must enforce the following invariants:
-
-- For all emails e1 and e2 in the log, such as e2.order > e1.order, then e2.uid > e1.uid
-
-- For all emails e1 and e2 in the log, such as e1.uuid == e2.uuid, then e1.order == e2.order
-
-If an invariant is broken, the conflict is solved with the following algorithm and the `uidvalidity` value is increased.
-
-
-```python
-def apply_mail_add(uuid, imap_uid):
- if imap_uid < internalseq:
- uidvalidity += internalseq - imap_uid
- mails.insert(uuid, internalseq, flags=["\Recent"])
- internalseq = internalseq + 1
- uidnext = internalseq
-
-def apply_mail_del(uuid):
- mails.remove(uuid)
- internalseq = internalseq + 1
-```
-
-A mathematical demonstration in Appendix D. shows that this algorithm indeed guarantees that under the same `uidvalidity`, different e-mails cannot share the same IMAP UID.
-
-To illustrate, let us imagine two processes that have a first operation A in common, and then had a divergent state when one applied an operation B, and another one applied an operation C. For process 1, we have:
-
-```python
-# state: uid-validity = 1, uid_next = 1, internalseq = 1
-(A) MAIL_ADD x 1
-# state: uid-validity = 1, x = 1, uid_next = 2, internalseq = 2
-(B) MAIL_ADD y 2
-# state: uid-validity = 1, x = 1, y = 2, uid_next = 3, internalseq = 3
-```
-
-And for process 2 we have:
-
-```python
-# state: uid-validity = 1, uid_next = 1, internalseq = 1
-(A) MAIL_ADD x 1
-# state: uid-validity = 1, x = 1, uid_next = 2, internalseq = 2
-(C) MAIL_ADD z 2
-# state: uid-validity = 1, x = 1, z = 2, uid_next = 3, internalseq = 3
-```
-
-Suppose that a new client connects to one of the two processes after the conflicting operations have been communicated between them. They may have before connected either to process 1 or to process 2, so they might have observed either mail `y` or mail `z` with UID 2. The only way to make sure that the client will not be confused about mail UIDs is to bump the uidvalidity when the conflict is solved. This is indeed what happens with our algorithm: for both processes, once they have learned of the other's conflicting operation, they will execute the following set of operations and end in a deterministic state:
-
-```python
-# state: uid-validity = 1, uid_next = 1, internalseq = 1
-(A) MAIL_ADD x 1
-# state: uid-validity = 1, x = 1, uid_next = 2, internalseq = 2
-(B) MAIL_ADD y 2
-# state: uid-validity = 1, x = 1, y = 2, uid_next = 3, internalseq = 3
-(C) MAIL_ADD z 2
-# conflict detected !
-# state: uid-validity = 2, x = 1, y = 2, z = 3, uid_next = 4, internalseq = 4
-```
-
-## A computed state for efficient requests
-
-From a data structure perspective, a list of commands is very inefficient to get the current state of the mailbox.
-Indeed, we don't want an `O(n)` complexity (where `n` is the number of log commands in the log) each time we want to know how many emails are stored in the mailbox.
-<!--To address this issue, we plan to maintain a locally computed (rollbackable) state of the mailbox.-->
-To address this issue, and thus query the mailbox efficiently, the MDA keeps an in-memory computed version of the logs, ie. the computed state.
-
-**Mapping IMAP identifiers to email identifiers with B-Tree**
-
-Core features of IMAP are synchronization and listing of emails.
-Its associated command is `FETCH`, it has 2 parameters, a range of `uid` (or `seq`) and a filter.
-For us, it means that we must be able to efficiently select a range of emails by their identifier, otherwise the user experience will be bad, and compute resources will be wasted.
-
-We identified that by using an ordered map based on a B-Tree, we can satisfy this requirement in an optimal manner.
-For example, Rust defines a [BTreeMap](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html) object in its standard library.
-We define the following structure for our mailbox:
-
-```rust
-struct mailbox {
- emails: BTreeMap<ImapUid, (EmailUuid, Flags)>,
- flags: BTreeMap<Flag, BTreeMap<ImapUid, EmailUuid>>,
- name: String,
- uid_next: u32,
- uid_validity: u32,
- /* other fields */
-}
-```
-
-This data structure allows us to efficiently select a range of emails by their identifier by walking the tree, allowing the server to be responsive to syncronisation request from clients.
-
-**Checkpoints**
-
-Having an in-memory computed state does not solve all the problems of operation on a log only, as 1) bootstrapping a fresh client is expensive as we have to replay possibly thousand of logs, and 2) logs would be kept indefinitely, wasting valuable storage resources.
-
-As a solution to these limitations, the MDA regularly checkpoints the in-memory state. More specifically, it serializes it (eg. with MessagePack), compresses it (eg. with zstd), and then stores it on Garage through the S3 API.
-A fresh client would then only have to download the latest checkpoint and the range of logs between the checkpoint and now, allowing swift bootstraping while retaining all of the value of the log model.
-
-Old logs and old checkpoints can be garbage collected after a few days for example as long as 1) the most recent checkpoint remains, 2) that all the logs after this checkpoint remain and 3) that we are confident enough that no log before this checkpoint will appear in the future.
-
diff --git a/doc/src/mailbox.md b/doc/src/mailbox.md
deleted file mode 100644
index 02d0e5a..0000000
--- a/doc/src/mailbox.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# Mailboxes
-
-IMAP servers, at their root, handle mailboxes.
-In this document, we explain the domain logic of IMAP and how we map it to Garage data
-with Aerogramme.
-
-## IMAP Domain Logic
-
-The main specification of IMAP is defined in [RFC3501](https://datatracker.ietf.org/doc/html/rfc3501).
-It defines 3 main objects: Mailboxes, Emails, and Flags. The following figure depicts how they work together:
-
-![An IMAP mailbox schema](./mailbox.png)
-
-Emails are stored ordered inside the mailbox, and for legacy reasons, the mailbox assigns 2 identifiers to each email we name `uid` and `seq`.
-
-`seq` is the legacy identifier, it numbers messages in a sequence. Each time an email is deleted, the message numbering will change to keep a continuous sequence without holes.
-While this numbering is convenient for interactive operations, it is not efficient to synchronize mail locally and quickly detect missing new emails.
-
-To solve this problem, `uid` identifiers were introduced later. They are monotonically increasing integers that must remain stable across time and sessions: when an email is deleted, its identifier is never reused.
-This is what Thunderbird uses for example when it synchronizes its mailboxes.
-
-If this ordering cannot be kept, for example because two independent IMAP daemons were adding an email to the same mailbox at the same time, it is possible to change the ordering as long as we change a value named `uid-validity` to trigger a full resynchronization of all clients. As this operation is expensive, we want to minimize the probability of having to trigger a full resynchronization, but in practice, having this recovery mechanism simplifies the operation of an IMAP server by providing a rather simple solution to rare collision situations.
-
-Flags are tags put on an email, some are defined at the protocol level, like `\Recent`, `\Deleted` or `\Seen`, which can be assigned or removed directly by the IMAP daemon.
-Others can be defined arbitrarily by the client, for which the MUA will apply its own logic.
-There is no mechanism in RFC3501 to synchronize flags between MUA besides listing the flags of all the emails.
-
-IMAP has many extensions, such as [RFC5465](https://www.rfc-editor.org/rfc/rfc5465.html) or [RFC7162](https://datatracker.ietf.org/doc/html/rfc7162).
-They are referred to as capabilities and are [referenced by the IANA](https://www.iana.org/assignments/imap-capabilities/imap-capabilities.xhtml).
-For this project, we are aiming to implement only IMAP4rev1 and no extension at all.
-
-
-## Aerogramme Implementation
-
-From a high-level perspective, we will handle _immutable_ emails differently from _mutable_ mailboxes and flags.
-Immutable data can be stored directly on Garage, as we do not fear reading an outdated value.
-For mutable data, we cannot store them directly in Garage.
-Instead, we choose to store a log of operations. Each client then applies this log of operation locally to rebuild its local state.
-
-During this design phase, we noted that the S3 API semantic was too limited for us, so we introduced a second API, K2V, to have more flexibility.
-K2V is designed to store and fetch small values in batches, it uses 2 different keys: one to spread the data on the cluster (`P`), and one to sort linked data on the same node (`S`).
-Having data on the same node allows for more efficient queries among this data.
-
-For performance reasons, we plan to introduce 2 optimizations.
-First, we store an email summary in K2V that allows fetching multiple entries at once.
-Second, we also store checkpoints of the logs in S3 to avoid keeping and replaying all the logs each time a client starts a session.
-We have the following data handled by Garage:
-
-![Aerogramme Datatypes](./aero-states.png)
-
-In Garage, it is important to carefully choose the key(s) that are used to store data to have fast queries, we propose the following model:
-
-![Aerogramme Key Choice](./aero-states2.png)
-
-
-
diff --git a/doc/src/mailbox.png b/doc/src/mailbox.png
deleted file mode 100644
index 038e3ac..0000000
--- a/doc/src/mailbox.png
+++ /dev/null
Binary files differ
diff --git a/doc/src/mutt_mail.png b/doc/src/mutt_mail.png
deleted file mode 100644
index e8d04e4..0000000
--- a/doc/src/mutt_mail.png
+++ /dev/null
Binary files differ
diff --git a/doc/src/mutt_mb.png b/doc/src/mutt_mb.png
deleted file mode 100644
index d1bafaf..0000000
--- a/doc/src/mutt_mb.png
+++ /dev/null
Binary files differ
diff --git a/doc/src/notes.md b/doc/src/notes.md
deleted file mode 100644
index 3a4c954..0000000
--- a/doc/src/notes.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Notes
-
-An IMAP trace extracted from Aerogramme:
-
-```
-S: * OK Hello
-C: A1 LOGIN alan p455w0rd
-S: A1 OK Completed
-C: A2 SELECT INBOX
-S: * 0 EXISTS
-S: * 0 RECENT
-S: * FLAGS (\Seen \Answered \Flagged \Deleted \Draft)
-S: * OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft \*)] Flags permitted
-S: * OK [UIDVALIDITY 1] UIDs valid
-S: * OK [UIDNEXT 1] Predict next UID
-S: A2 OK [READ-WRITE] Select completed
-C: A3 NOOP
-S: A3 OK NOOP completed.
- <---- e-mail arrives through LMTP server ---->
-C: A4 NOOP
-S: * 1 EXISTS
-S: A4 OK NOOP completed.
-C: A5 FETCH 1 FULL
-S: * 1 FETCH (UID 1 FLAGS () INTERNALDATE "06-Jul-2022 14:46:42 +0000"
- RFC822.SIZE 117 ENVELOPE (NIL "test" (("Alan Smith" NIL "alan" "smith.me"))
- NIL NIL (("Alan Smith" NIL "alan" "aerogramme.tld")) NIL NIL NIL NIL)
- BODY ("TEXT" "test" NIL "test" "test" "test" 1 1))
-S: A5 OK FETCH completed
-C: A6 FETCH 1 (RFC822)
-S: * 1 FETCH (UID 1 RFC822 {117}
-S: Subject: test
-S: From: Alan Smith <alan@smith.me>
-S: To: Alan Smith <alan@aerogramme.tld>
-S:
-S: Hello, world!
-S: .
-S: )
-S: A6 OK FETCH completed
-C: A7 LOGOUT
-S: * BYE Logging out
-S: A7 OK Logout completed
-```
diff --git a/doc/src/overview.md b/doc/src/overview.md
deleted file mode 100644
index ca75a29..0000000
--- a/doc/src/overview.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# Overview
-
-Aérogramme stands at the interface between the Garage storage server, and the user's e-mail client. It provides regular IMAP access on the client-side, and stores encrypted e-mail data on the server-side. Aérogramme also provides an LMTP server interface through which incoming mail can be forwarded by the MTA (e.g. Postfix).
-
-<center>
-<img src="./aero-compo.png" alt="Aerogramme components"/>
-<br>
-<i>Figure 1: Aérogramme, our IMAP daemon, stores its data encrypted in Garage and provides regular IMAP access to mail clients</i></center>
-
-
-**Overview of architecture**
-
-Figure 2 below shows an overview of Aérogramme's architecture. Each user has a personal Garage bucket in which to store their mailbox contents. We will document below the details of the components that make up Aérogramme, but let us first provide a high-level overview. The two main classes, `User` and `Mailbox`, define how data is stored in this bucket, and provide a high-level interface with primitives such as reading the message index, loading a mail's content, copying, moving, and deleting messages, etc. This mail storage system is supported by two important primitives: a cryptography management system that provides encryption keys for user's data, and a simple log-like database system inspired by Bayou [1] which we have called Bay, that we use to store the index of messages in each mailbox. The mail storage system is made accessible to the outside world by two subsystems: an LMTP server that allows for incoming mail to be received and stored in a user's bucket, in a staging area, and the IMAP server itself which allows full-fledged manipulation of mailbox data by users.
-
-<center>
-<img src="./aero-schema.png" alt="Aerogramme internals"/>
-<i>Figure 2: Overview of Aérogramme's architecture and internal data structures for a given user, Alice</i></center>
-
-
-**Cryptography**
-
-Our cryptography module is taking care of: authenticating users against a data source (using their IMAP login and password), returning a set of credentials that allow read/write access to a Garage bucket, as well as a set of secret encryption keys used to encrypt and decrypt data stored in the bucket.
-The cryptography module makes use of the user's authentication password as a passphrase to decrypt the user's secret keys, which are stored in the user's bucket in a dedicated K2V section.
-
-This module can use either of two data sources for user authentication:
-
-- LDAP, in which case the password (which is also the passphrase for decrypting the user's secret keys) must match the LDAP password of the user.
-- Static, in which case the users are statically declared in Aérogramme's configuration file, and can have any password.
-
-The static authentication source can be used in a deployment scenario shown in Figure 3, where Aérogramme is not running on the side of the service provider, but on the user's device itself. In this case, the user can use any password to encrypt their data in the bucket; the only credentials they need for authentication against the service provider are the S3 and K2V API access keys.
-
-<center>
-<img src="./aero-paranoid.png" alt="user side encryption" />
-<br>
-<i>Figure 3: alternative deployment of Aérogramme on the user's device: the service provider never gets access to the plaintext data.</i></center>
-
-The cryptography module also has a "public authentication" method, which allows the LMTP module to retrieve only a public key for the user to write incoming messages to the user's bucket but without having access to all of the existing encrypted data.
-
-The cryptography module of Aérogramme is based on standard cryptographic primitives from `libsodium` and follows best practices in the domain.
-
-**Bay, a simplification of Bayou**
-
-In our last milestone report, we described how we intended to implement the message index for IMAP mailboxes, based on an eventually-consistent log-like data structure. The principles of this system have been established in Bayou in 1995 [1], allowing users to use a weakly-coordinated datastore to exchange data and solve write conflicts. Bayou is based on a sequential specification, which defines the action that operations in the log have on the shared object's state. To handle concurrent modification, Bayou allows for log entries to be appended in non-sequential order: in case a process reads a log entry that was written earlier by another process, it can rewind its execution of the sequential specification to the point where the newly acquired operation should have been executed, and then execute the log again starting from this point. The challenge then consists in defining a sequential specification that provides the desired semantics for the application. In our last milestone report (milestone 3.A), we described a sequential specification that solves the UID assignment problem in IMAP and proved it correct. We refer the reader to that document for more details.
-
-For milestone 3B, we have implemented our customized version of Bayou, which we call Bay. Bay implements the log-like semantics and the rewind ability of Bayou, however, it makes use of a much simpler data system: Bay is not operating on a relational database that is stored on disk, but simply on a data structure in RAM, for which a full checkpoint is written regularly. We decided against using a complex database as we observed that the expected size of the data structures we would be handling (the message indexes for each mailbox) wouldn't be so big most of the time, and having a full copy in RAM was perfectly acceptable. This allows for a drastic simplification in comparison to the proposal of the original Bayou paper [1]. On the other side, we added encryption in Bay so that both log entries and checkpoints are stored encrypted in Garage using the user's secret key, meaning that a malicious Garage administrator cannot read the content of a user's mailbox index.
-
-**LMTP server and incoming mail handler**
-
-To handle incoming mail, we had to add a simple LMTP server to Aérogramme. This server uses the public authentication method of the cryptography module to retrieve a set of public credentials (in particular, a public key for asymmetric encryption) for storing incoming messages. The incoming messages are stored in their raw RFC822 form (encrypted) in a specific folder of the Garage bucket called `incoming/`. When a user logs in with their username and password, at which time Aérogramme can decrypt the user's secret keys, a special process is launched that watches the incoming folder and moves these messages to the `INBOX` folder. This task can only be done by a process that knows the user's secret keys, as it has to modify the mailbox index of the `INBOX` folder, which is encrypted using the user's secret keys. In later versions of Aérogramme, this process would be the perfect place to implement mail filtering logic using user-specified rules. These rules could be stored in a dedicated section of the bucket, again encrypted with the user's secret keys.
-
-To implement the LMTP server, we chose to make use of the `smtp-server` crate from the [Kannader](https://github.com/Ekleog/kannader) project (an MTA written in Rust). The `smtp-server` crate had all of the necessary functionality for building SMTP servers, however, it did not handle LMTP. As LMTP is extremely close to SMTP, we were able to extend the `smtp-server` module to allow it to be used for the implementation of both SMTP and LMTP servers. Our work has been proposed as a [pull request](https://github.com/Ekleog/kannader/pull/178) to be merged back upstream in Kannader, which should be integrated soon.
-
-**IMAP server**
-
-The last part that remains to build Aérogramme is to implement the logic behind the IMAP protocol and to link it with the mail storage primitives. We started by implementing a state machine that handled the transitions between the different states in the IMAP protocol: ANONYMOUS (before login), AUTHENTICATED (after login), and SELECTED (once a mailbox has been selected for reading/writing). In the SELECTED state, the IMAP session is linked to a given mailbox of the user. In addition, the IMAP server has to keep track of which updates to the mailbox it has sent (or not) to the client so that it can produce IMAP messages consistent with what the client believes to be in the mailbox. In particular, many IMAP commands make use of mail sequence numbers to identify messages, which are indices in the sorted array of all of the messages in the mailbox. However, if messages are added or removed concurrently, these sequence numbers change: hence we must keep a snapshot of the mailbox's index *as the client knows it*, which is not necessarily the same as what is _actually_ in the mailbox, to generate messages that the client will understand correctly. This snapshot is called a *mailbox view* and is synced regularly with the actual mailbox, at which time the corresponding IMAP updates are sent. This can be done only at specific moments when permitted by the IMAP protocol.
-
-The second part of this task consisted in implementing all of the IMAP protocol commands. Most are relatively straightforward, however, one command, in particular, needed special care: the FETCH command. The FETCH command in the IMAP protocol can return the contents of a message to the client. However, it must also understand precisely the semantics of the content of an e-mail message, as the client can specify very precisely how the message should be returned. For instance, in the case of a multipart message with attachments, the client can emit a FECTH command requesting only a certain attachment of the message to be returned, and not the whole message. To implement such semantics, we have based ourselves on the [`mail-parser`](https://docs.rs/mail-parser/latest/mail_parser/) crate, which can fully parse an RFC822-formatted e-mail message, and also supports some extensions such as MIME. To validate that we were correctly converting the parsed message structure to IMAP messages, we designed a test suite composed of several weirdly shaped e-mail messages, whose IMAP structure definition we extracted by taking Dovecot as a reference. We were then able to compare the output of Aérogramme on these messages with the reference consisting in what was returned by Dovecot.
-
-## References
-
-- [1] Terry, D. B., Theimer, M. M., Petersen, K., Demers, A. J., Spreitzer, M. J., & Hauser, C. H. (1995). Managing update conflicts in Bayou, a weakly connected replicated storage system. *ACM SIGOPS Operating Systems Review*, 29(5), 172-182. ([PDF](https://dl.acm.org/doi/pdf/10.1145/224057.224070))
diff --git a/doc/src/rfc.md b/doc/src/rfc.md
deleted file mode 100644
index 5b42c92..0000000
--- a/doc/src/rfc.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# RFC coverage
-
-*Not yet written*
diff --git a/doc/src/setup.md b/doc/src/setup.md
deleted file mode 100644
index f954ae3..0000000
--- a/doc/src/setup.md
+++ /dev/null
@@ -1,90 +0,0 @@
-# Setup
-
-You must start by creating a user profile in Garage. Run the following command after adjusting the parameters to your configuration:
-
-```bash
-cargo run -- first-login \
- --region garage \
- --k2v-endpoint http://127.0.0.1:3904 \
- --s3-endpoint http://127.0.0.1:3900 \
- --aws-access-key-id GK... \
- --aws-secret-access-key c0ffee... \
- --bucket mailrage-me \
- --user-secret s3cr3t
-```
-
-*Note: user-secret is not the user's password. It is an additional secret used when deriving user's secret key from their password. The idea is that, even if user leaks their password, their encrypted data remain safe as long as this additional secret does not leak. You can generate it with openssl for example: `openssl rand -base64 30`. Read [Cryptography & key management](./crypt-key.md) for more details.*
-
-
-The program will interactively ask you some questions and finally generates for you a snippet of configuration:
-
-```
-Please enter your password for key decryption.
-If you are using LDAP login, this must be your LDAP password.
-If you are using the static login provider, enter any password, and this will also become your password for local IMAP access.
-Enter password:
-Confirm password:
-
-Cryptographic key setup is complete.
-
-If you are using the static login provider, add the following section to your .toml configuration file:
-
-[login_static.users.<username>]
-password = "$argon2id$v=19$m=4096,t=3,p=1$..."
-aws_access_key_id = "GK..."
-aws_secret_access_key = "c0ffee..."
-```
-
-In this tutorial, we will use the static login provider (and not the LDAP one).
-We will thus create a config file named `aerogramme.toml` in which we will paste the previous snippet. You also need to enter some other keys. In the end, your file should look like that:
-
-```toml
-s3_endpoint = "http://127.0.0.1:3900"
-k2v_endpoint = "http://127.0.0.1:3904"
-aws_region = "garage"
-
-[lmtp]
-bind_addr = "[::1]:12024"
-hostname = "aerogramme.tld"
-
-[imap]
-bind_addr = "[::1]:1993"
-
-[login_static]
-default_bucket = "mailrage"
-
-[login_static.users.me]
-bucket = "mailrage-me"
-user_secret = "s3cr3t"
-email_addresses = [
- "me@aerogramme.tld"
-]
-
-# copy pasted values from first-login
-password = "$argon2id$v=19$m=4096,t=3,p=1$..."
-aws_access_key_id = "GK..."
-aws_secret_access_key = "c0ffee..."
-```
-
-If you fear to loose your password, you can backup your key with the following command:
-
-```bash
-cargo run -- show-keys \
- --region garage \
- --k2v-endpoint http://127.0.0.1:3904 \
- --s3-endpoint http://127.0.0.1:3900 \
- --aws-access-key-id GK... \
- --aws-secret-access-key c0ffee... \
- --bucket mailrage-me \
- --user-secret s3cr3t
-```
-
-You will then be asked for your key decryption password:
-
-```
-Enter key decryption password:
-master_key = "..."
-secret_key = "..."
-```
-
-You are now ready to [validate your installation](./validate.md).
diff --git a/doc/src/validate.md b/doc/src/validate.md
deleted file mode 100644
index 57903f6..0000000
--- a/doc/src/validate.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Validate
-
-Start a server as follow:
-
-```bash
-cargo run -- server
-```
-
-Inject emails:
-
-```bash
-./test/inject_emails.sh '<me@aerogramme.tld>' dxflrs
-```
-
-Now you can connect your mailbox with `mutt`.
-Start by creating a config file, for example we used the following `~/.muttrc` file:
-
-```ini
-set imap_user = quentin
-set imap_pass = p455w0rd
-set folder = imap://localhost:1993
-set spoolfile = +INBOX
-set ssl_starttls = no
-set ssl_force_tls = no
-mailboxes = +INBOX
-bind index G imap-fetch-mail
-```
-
-And then simply launch `mutt`.
-The first time nothing will happen as Aerogramme must
-process your incoming emails. Just ask `mutt` to refresh its
-view by pressing `G` (for *Get*).
-
-Now, you should see some emails:
-
-![Screenshot of mutt mailbox](./mutt_mb.png)
-
-And you can read them:
-
-![Screenshot of mutt mail view](./mutt_mail.png)
diff --git a/flake.nix b/flake.nix
index 01dfda1..8dcd326 100644
--- a/flake.nix
+++ b/flake.nix
@@ -185,13 +185,15 @@
# Shell
shell = gpkgs.mkShell {
buildInputs = [
+ gpkgs.openssl
+ gpkgs.pkg-config
cargo2nix.packages.x86_64-linux.default
- fenix.packages.x86_64-linux.minimal.toolchain
- fenix.packages.x86_64-linux.rust-analyzer
+ fenix.packages.x86_64-linux.complete.toolchain
+ #fenix.packages.x86_64-linux.rust-analyzer
];
shellHook = ''
- echo "AEROGRAME DEVELOPMENT SHELL ${fenix.packages.x86_64-linux.minimal.rustc}"
- export RUST_SRC_PATH="${fenix.packages.x86_64-linux.latest.rust-src}/lib/rustlib/src/rust/library"
+ echo "AEROGRAME DEVELOPMENT SHELL ${fenix.packages.x86_64-linux.complete.toolchain}"
+ export RUST_SRC_PATH="${fenix.packages.x86_64-linux.complete.toolchain}/lib/rustlib/src/rust/library"
export RUST_ANALYZER_INTERNALS_DO_NOT_USE='this is unstable'
'';
};
diff --git a/src/auth.rs b/src/auth.rs
deleted file mode 100644
index 064c90c..0000000
--- a/src/auth.rs
+++ /dev/null
@@ -1,941 +0,0 @@
-use std::net::SocketAddr;
-
-use anyhow::{anyhow, bail, Result};
-use futures::stream::{FuturesUnordered, StreamExt};
-use tokio::io::BufStream;
-use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
-use tokio::net::{TcpListener, TcpStream};
-use tokio::sync::watch;
-
-use crate::config::AuthConfig;
-use crate::login::ArcLoginProvider;
-
-/// Seek compatibility with the Dovecot Authentication Protocol
-///
-/// ## Trace
-///
-/// ```text
-/// S: VERSION 1 2
-/// S: MECH PLAIN plaintext
-/// S: MECH LOGIN plaintext
-/// S: SPID 15
-/// S: CUID 17654
-/// S: COOKIE f56692bee41f471ed01bd83520025305
-/// S: DONE
-/// C: VERSION 1 2
-/// C: CPID 1
-///
-/// C: AUTH 2 PLAIN service=smtp
-/// S: CONT 2
-/// C: CONT 2 base64stringFollowingRFC4616==
-/// S: OK 2 user=alice@example.tld
-///
-/// C: AUTH 42 LOGIN service=smtp
-/// S: CONT 42 VXNlcm5hbWU6
-/// C: CONT 42 b64User
-/// S: CONT 42 UGFzc3dvcmQ6
-/// C: CONT 42 b64Pass
-/// S: FAIL 42 user=alice
-/// ```
-///
-/// ## RFC References
-///
-/// PLAIN SASL - https://datatracker.ietf.org/doc/html/rfc4616
-///
-///
-/// ## Dovecot References
-///
-/// https://doc.dovecot.org/developer_manual/design/auth_protocol/
-/// https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms
-/// https://doc.dovecot.org/configuration_manual/howto/simple_virtual_install/#simple-virtual-install-smtp-auth
-/// https://doc.dovecot.org/configuration_manual/howto/postfix_and_dovecot_sasl/#howto-postfix-and-dovecot-sasl
-pub struct AuthServer {
- login_provider: ArcLoginProvider,
- bind_addr: SocketAddr,
-}
-
-impl AuthServer {
- pub fn new(config: AuthConfig, login_provider: ArcLoginProvider) -> Self {
- Self {
- bind_addr: config.bind_addr,
- login_provider,
- }
- }
-
- pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
- let tcp = TcpListener::bind(self.bind_addr).await?;
- tracing::info!(
- "SASL Authentication Protocol listening on {:#}",
- self.bind_addr
- );
-
- let mut connections = FuturesUnordered::new();
-
- while !*must_exit.borrow() {
- let wait_conn_finished = async {
- if connections.is_empty() {
- futures::future::pending().await
- } else {
- connections.next().await
- }
- };
-
- let (socket, remote_addr) = tokio::select! {
- a = tcp.accept() => a?,
- _ = wait_conn_finished => continue,
- _ = must_exit.changed() => continue,
- };
-
- tracing::info!("AUTH: accepted connection from {}", remote_addr);
- let conn = tokio::spawn(
- NetLoop::new(socket, self.login_provider.clone(), must_exit.clone()).run_error(),
- );
-
- connections.push(conn);
- }
- drop(tcp);
-
- tracing::info!("AUTH server shutting down, draining remaining connections...");
- while connections.next().await.is_some() {}
-
- Ok(())
- }
-}
-
-struct NetLoop {
- login: ArcLoginProvider,
- stream: BufStream<TcpStream>,
- stop: watch::Receiver<bool>,
- state: State,
- read_buf: Vec<u8>,
- write_buf: BytesMut,
-}
-
-impl NetLoop {
- fn new(stream: TcpStream, login: ArcLoginProvider, stop: watch::Receiver<bool>) -> Self {
- Self {
- login,
- stream: BufStream::new(stream),
- state: State::Init,
- stop,
- read_buf: Vec::new(),
- write_buf: BytesMut::new(),
- }
- }
-
- async fn run_error(self) {
- match self.run().await {
- Ok(()) => tracing::info!("Auth session succeeded"),
- Err(e) => tracing::error!(err=?e, "Auth session failed"),
- }
- }
-
- async fn run(mut self) -> Result<()> {
- loop {
- tokio::select! {
- read_res = self.stream.read_until(b'\n', &mut self.read_buf) => {
- // Detect EOF / socket close
- let bread = read_res?;
- if bread == 0 {
- tracing::info!("Reading buffer empty, connection has been closed. Exiting AUTH session.");
- return Ok(())
- }
-
- // Parse command
- let (_, cmd) = client_command(&self.read_buf).map_err(|_| anyhow!("Unable to parse command"))?;
- tracing::trace!(cmd=?cmd, "Received command");
-
- // Make some progress in our local state
- self.state.progress(cmd, &self.login).await;
- if matches!(self.state, State::Error) {
- bail!("Internal state is in error, previous logs explain what went wrong");
- }
-
- // Build response
- let srv_cmds = self.state.response();
- srv_cmds.iter().try_for_each(|r| {
- tracing::trace!(cmd=?r, "Sent command");
- r.encode(&mut self.write_buf)
- })?;
-
- // Send responses if at least one command response has been generated
- if !srv_cmds.is_empty() {
- self.stream.write_all(&self.write_buf).await?;
- self.stream.flush().await?;
- }
-
- // Reset buffers
- self.read_buf.clear();
- self.write_buf.clear();
- },
- _ = self.stop.changed() => {
- tracing::debug!("Server is stopping, quitting this runner");
- return Ok(())
- }
- }
- }
- }
-}
-
-// -----------------------------------------------------------------
-//
-// BUSINESS LOGIC
-//
-// -----------------------------------------------------------------
-use rand::prelude::*;
-
-#[derive(Debug)]
-enum AuthRes {
- Success(String),
- Failed(Option<String>, Option<FailCode>),
-}
-
-#[derive(Debug)]
-enum State {
- Error,
- Init,
- HandshakePart(Version),
- HandshakeDone,
- AuthPlainProgress { id: u64 },
- AuthDone { id: u64, res: AuthRes },
-}
-
-const SERVER_MAJOR: u64 = 1;
-const SERVER_MINOR: u64 = 2;
-const EMPTY_AUTHZ: &[u8] = &[];
-impl State {
- async fn try_auth_plain<'a>(&self, data: &'a [u8], login: &ArcLoginProvider) -> AuthRes {
- // Check that we can extract user's login+pass
- let (ubin, pbin) = match auth_plain(&data) {
- Ok(([], (authz, user, pass))) if authz == user || authz == EMPTY_AUTHZ => (user, pass),
- Ok(_) => {
- tracing::error!("Impersonating user is not supported");
- return AuthRes::Failed(None, None);
- }
- Err(e) => {
- tracing::error!(err=?e, "Could not parse the SASL PLAIN data chunk");
- return AuthRes::Failed(None, None);
- }
- };
-
- // Try to convert it to UTF-8
- let (user, password) = match (std::str::from_utf8(ubin), std::str::from_utf8(pbin)) {
- (Ok(u), Ok(p)) => (u, p),
- _ => {
- tracing::error!("Username or password contain invalid UTF-8 characters");
- return AuthRes::Failed(None, None);
- }
- };
-
- // Try to connect user
- match login.login(user, password).await {
- Ok(_) => AuthRes::Success(user.to_string()),
- Err(e) => {
- tracing::warn!(err=?e, "login failed");
- AuthRes::Failed(Some(user.to_string()), None)
- }
- }
- }
-
- async fn progress(&mut self, cmd: ClientCommand, login: &ArcLoginProvider) {
- let new_state = 'state: {
- match (std::mem::replace(self, State::Error), cmd) {
- (Self::Init, ClientCommand::Version(v)) => Self::HandshakePart(v),
- (Self::HandshakePart(version), ClientCommand::Cpid(_cpid)) => {
- if version.major != SERVER_MAJOR {
- tracing::error!(
- client_major = version.major,
- server_major = SERVER_MAJOR,
- "Unsupported client major version"
- );
- break 'state Self::Error;
- }
-
- Self::HandshakeDone
- }
- (
- Self::HandshakeDone { .. },
- ClientCommand::Auth {
- id, mech, options, ..
- },
- )
- | (
- Self::AuthDone { .. },
- ClientCommand::Auth {
- id, mech, options, ..
- },
- ) => {
- if mech != Mechanism::Plain {
- tracing::error!(mechanism=?mech, "Unsupported Authentication Mechanism");
- break 'state Self::AuthDone {
- id,
- res: AuthRes::Failed(None, None),
- };
- }
-
- match options.last() {
- Some(AuthOption::Resp(data)) => Self::AuthDone {
- id,
- res: self.try_auth_plain(&data, login).await,
- },
- _ => Self::AuthPlainProgress { id },
- }
- }
- (Self::AuthPlainProgress { id }, ClientCommand::Cont { id: cid, data }) => {
- // Check that ID matches
- if cid != id {
- tracing::error!(
- auth_id = id,
- cont_id = cid,
- "CONT id does not match AUTH id"
- );
- break 'state Self::AuthDone {
- id,
- res: AuthRes::Failed(None, None),
- };
- }
-
- Self::AuthDone {
- id,
- res: self.try_auth_plain(&data, login).await,
- }
- }
- _ => {
- tracing::error!("This command is not valid in this context");
- Self::Error
- }
- }
- };
- tracing::debug!(state=?new_state, "Made progress");
- *self = new_state;
- }
-
- fn response(&self) -> Vec<ServerCommand> {
- let mut srv_cmd: Vec<ServerCommand> = Vec::new();
-
- match self {
- Self::HandshakeDone { .. } => {
- srv_cmd.push(ServerCommand::Version(Version {
- major: SERVER_MAJOR,
- minor: SERVER_MINOR,
- }));
-
- srv_cmd.push(ServerCommand::Mech {
- kind: Mechanism::Plain,
- parameters: vec![MechanismParameters::PlainText],
- });
-
- srv_cmd.push(ServerCommand::Spid(15u64));
- srv_cmd.push(ServerCommand::Cuid(19350u64));
-
- let mut cookie = [0u8; 16];
- thread_rng().fill(&mut cookie);
- srv_cmd.push(ServerCommand::Cookie(cookie));
-
- srv_cmd.push(ServerCommand::Done);
- }
- Self::AuthPlainProgress { id } => {
- srv_cmd.push(ServerCommand::Cont {
- id: *id,
- data: None,
- });
- }
- Self::AuthDone {
- id,
- res: AuthRes::Success(user),
- } => {
- srv_cmd.push(ServerCommand::Ok {
- id: *id,
- user_id: Some(user.to_string()),
- extra_parameters: vec![],
- });
- }
- Self::AuthDone {
- id,
- res: AuthRes::Failed(maybe_user, maybe_failcode),
- } => {
- srv_cmd.push(ServerCommand::Fail {
- id: *id,
- user_id: maybe_user.clone(),
- code: maybe_failcode.clone(),
- extra_parameters: vec![],
- });
- }
- _ => (),
- };
-
- srv_cmd
- }
-}
-
-// -----------------------------------------------------------------
-//
-// DOVECOT AUTH TYPES
-//
-// -----------------------------------------------------------------
-
-#[derive(Debug, Clone, PartialEq)]
-enum Mechanism {
- Plain,
- Login,
-}
-
-#[derive(Clone, Debug)]
-enum AuthOption {
- /// Unique session ID. Mainly used for logging.
- Session(u64),
- /// Local IP connected to by the client. In standard string format, e.g. 127.0.0.1 or ::1.
- LocalIp(String),
- /// Remote client IP
- RemoteIp(String),
- /// Local port connected to by the client.
- LocalPort(u16),
- /// Remote client port
- RemotePort(u16),
- /// When Dovecot proxy is used, the real_rip/real_port are the proxy’s IP/port and real_lip/real_lport are the backend’s IP/port where the proxy was connected to.
- RealRemoteIp(String),
- RealLocalIp(String),
- RealLocalPort(u16),
- RealRemotePort(u16),
- /// TLS SNI name
- LocalName(String),
- /// Enable debugging for this lookup.
- Debug,
- /// List of fields that will become available via %{forward_*} variables. The list is double-tab-escaped, like: tab_escaped[tab_escaped(key=value)[<TAB>...]
- /// Note: we do not unescape the tabulation, and thus we don't parse the data
- ForwardViews(Vec<u8>),
- /// Remote user has secured transport to auth client (e.g. localhost, SSL, TLS).
- Secured(Option<String>),
- /// The value can be “insecure”, “trusted” or “TLS”.
- Transport(String),
- /// TLS cipher being used.
- TlsCipher(String),
- /// The number of bits in the TLS cipher.
- /// @FIXME: I don't know how if it's a string or an integer
- TlsCipherBits(String),
- /// TLS perfect forward secrecy algorithm (e.g. DH, ECDH)
- TlsPfs(String),
- /// TLS protocol name (e.g. SSLv3, TLSv1.2)
- TlsProtocol(String),
- /// Remote user has presented a valid SSL certificate.
- ValidClientCert(String),
- /// Ignore auth penalty tracking for this request
- NoPenalty,
- /// Unknown option sent by Postfix
- NoLogin,
- /// Username taken from client’s SSL certificate.
- CertUsername,
- /// IMAP ID string
- ClientId,
- /// An unknown key
- UnknownPair(String, Vec<u8>),
- UnknownBool(Vec<u8>),
- /// Initial response for authentication mechanism.
- /// NOTE: This must be the last parameter. Everything after it is ignored.
- /// This is to avoid accidental security holes if user-given data is directly put to base64 string without filtering out tabs.
- /// @FIXME: I don't understand this parameter
- Resp(Vec<u8>),
-}
-
-#[derive(Debug, Clone)]
-struct Version {
- major: u64,
- minor: u64,
-}
-
-#[derive(Debug)]
-enum ClientCommand {
- /// Both client and server should check that they support the same major version number. If they don’t, the other side isn’t expected to be talking the same protocol and should be disconnected. Minor version can be ignored. This document specifies the version number 1.2.
- Version(Version),
- /// CPID finishes the handshake from client.
- Cpid(u64),
- Auth {
- /// ID is a connection-specific unique request identifier. It must be a 32bit number, so typically you’d just increment it by one.
- id: u64,
- /// A SASL mechanism (eg. LOGIN, PLAIN, etc.)
- /// See: https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms
- mech: Mechanism,
- /// Service is the service requesting authentication, eg. pop3, imap, smtp.
- service: String,
- /// All the optional parameters
- options: Vec<AuthOption>,
- },
- Cont {
- /// The <id> must match the <id> of the AUTH command.
- id: u64,
- /// Data that will be serialized to / deserialized from base64
- data: Vec<u8>,
- },
-}
-
-#[derive(Debug)]
-enum MechanismParameters {
- /// Anonymous authentication
- Anonymous,
- /// Transfers plaintext passwords
- PlainText,
- /// Subject to passive (dictionary) attack
- Dictionary,
- /// Subject to active (non-dictionary) attack
- Active,
- /// Provides forward secrecy between sessions
- ForwardSecrecy,
- /// Provides mutual authentication
- MutualAuth,
- /// Don’t advertise this as available SASL mechanism (eg. APOP)
- Private,
-}
-
-#[derive(Debug, Clone)]
-enum FailCode {
- /// This is a temporary internal failure, e.g. connection was lost to SQL database.
- TempFail,
- /// Authentication succeeded, but authorization failed (master user’s password was ok, but destination user was not ok).
- AuthzFail,
- /// User is disabled (password may or may not have been correct)
- UserDisabled,
- /// User’s password has expired.
- PassExpired,
-}
-
-#[derive(Debug)]
-enum ServerCommand {
- /// Both client and server should check that they support the same major version number. If they don’t, the other side isn’t expected to be talking the same protocol and should be disconnected. Minor version can be ignored. This document specifies the version number 1.2.
- Version(Version),
- /// CPID and SPID specify client and server Process Identifiers (PIDs). They should be unique identifiers for the specific process. UNIX process IDs are good choices.
- /// SPID can be used by authentication client to tell master which server process handled the authentication.
- Spid(u64),
- /// CUID is a server process-specific unique connection identifier. It’s different each time a connection is established for the server.
- /// CUID is currently useful only for APOP authentication.
- Cuid(u64),
- Mech {
- kind: Mechanism,
- parameters: Vec<MechanismParameters>,
- },
- /// COOKIE returns connection-specific 128 bit cookie in hex. It must be given to REQUEST command. (Protocol v1.1+ / Dovecot v2.0+)
- Cookie([u8; 16]),
- /// DONE finishes the handshake from server.
- Done,
-
- Fail {
- id: u64,
- user_id: Option<String>,
- code: Option<FailCode>,
- extra_parameters: Vec<Vec<u8>>,
- },
- Cont {
- id: u64,
- data: Option<Vec<u8>>,
- },
- /// FAIL and OK may contain multiple unspecified parameters which authentication client may handle specially.
- /// The only one specified here is user=<userid> parameter, which should always be sent if the userid is known.
- Ok {
- id: u64,
- user_id: Option<String>,
- extra_parameters: Vec<Vec<u8>>,
- },
-}
-
-// -----------------------------------------------------------------
-//
-// DOVECOT AUTH DECODING
-//
-// ------------------------------------------------------------------
-
-use base64::Engine;
-use nom::{
- branch::alt,
- bytes::complete::{is_not, tag, tag_no_case, take, take_while, take_while1},
- character::complete::{tab, u16, u64},
- combinator::{map, opt, recognize, rest, value},
- error::{Error, ErrorKind},
- multi::{many1, separated_list0},
- sequence::{pair, preceded, tuple},
- IResult,
-};
-
-fn version_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
- let mut parser = tuple((tag_no_case(b"VERSION"), tab, u64, tab, u64));
-
- let (input, (_, _, major, _, minor)) = parser(input)?;
- Ok((input, ClientCommand::Version(Version { major, minor })))
-}
-
-fn cpid_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
- preceded(
- pair(tag_no_case(b"CPID"), tab),
- map(u64, |v| ClientCommand::Cpid(v)),
- )(input)
-}
-
-fn mechanism<'a>(input: &'a [u8]) -> IResult<&'a [u8], Mechanism> {
- alt((
- value(Mechanism::Plain, tag_no_case(b"PLAIN")),
- value(Mechanism::Login, tag_no_case(b"LOGIN")),
- ))(input)
-}
-
-fn is_not_tab_or_esc_or_lf(c: u8) -> bool {
- c != 0x09 && c != 0x01 && c != 0x0a // TAB or 0x01 or LF
-}
-
-fn is_esc<'a>(input: &'a [u8]) -> IResult<&'a [u8], &[u8]> {
- preceded(tag(&[0x01]), take(1usize))(input)
-}
-
-fn parameter<'a>(input: &'a [u8]) -> IResult<&'a [u8], &[u8]> {
- recognize(many1(alt((take_while1(is_not_tab_or_esc_or_lf), is_esc))))(input)
-}
-
-fn parameter_str(input: &[u8]) -> IResult<&[u8], String> {
- let (input, buf) = parameter(input)?;
-
- std::str::from_utf8(buf)
- .map(|v| (input, v.to_string()))
- .map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))
-}
-
-fn is_param_name_char(c: u8) -> bool {
- is_not_tab_or_esc_or_lf(c) && c != 0x3d // =
-}
-
-fn parameter_name(input: &[u8]) -> IResult<&[u8], String> {
- let (input, buf) = take_while1(is_param_name_char)(input)?;
-
- std::str::from_utf8(buf)
- .map(|v| (input, v.to_string()))
- .map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))
-}
-
-fn service<'a>(input: &'a [u8]) -> IResult<&'a [u8], String> {
- preceded(tag_no_case("service="), parameter_str)(input)
-}
-
-fn auth_option<'a>(input: &'a [u8]) -> IResult<&'a [u8], AuthOption> {
- use AuthOption::*;
- alt((
- alt((
- value(Debug, tag_no_case(b"debug")),
- value(NoPenalty, tag_no_case(b"no-penalty")),
- value(ClientId, tag_no_case(b"client_id")),
- value(NoLogin, tag_no_case(b"nologin")),
- map(preceded(tag_no_case(b"session="), u64), |id| Session(id)),
- map(preceded(tag_no_case(b"lip="), parameter_str), |ip| {
- LocalIp(ip)
- }),
- map(preceded(tag_no_case(b"rip="), parameter_str), |ip| {
- RemoteIp(ip)
- }),
- map(preceded(tag_no_case(b"lport="), u16), |port| {
- LocalPort(port)
- }),
- map(preceded(tag_no_case(b"rport="), u16), |port| {
- RemotePort(port)
- }),
- map(preceded(tag_no_case(b"real_rip="), parameter_str), |ip| {
- RealRemoteIp(ip)
- }),
- map(preceded(tag_no_case(b"real_lip="), parameter_str), |ip| {
- RealLocalIp(ip)
- }),
- map(preceded(tag_no_case(b"real_lport="), u16), |port| {
- RealLocalPort(port)
- }),
- map(preceded(tag_no_case(b"real_rport="), u16), |port| {
- RealRemotePort(port)
- }),
- )),
- alt((
- map(
- preceded(tag_no_case(b"local_name="), parameter_str),
- |name| LocalName(name),
- ),
- map(
- preceded(tag_no_case(b"forward_views="), parameter),
- |views| ForwardViews(views.into()),
- ),
- map(preceded(tag_no_case(b"secured="), parameter_str), |info| {
- Secured(Some(info))
- }),
- value(Secured(None), tag_no_case(b"secured")),
- value(CertUsername, tag_no_case(b"cert_username")),
- map(preceded(tag_no_case(b"transport="), parameter_str), |ts| {
- Transport(ts)
- }),
- map(
- preceded(tag_no_case(b"tls_cipher="), parameter_str),
- |cipher| TlsCipher(cipher),
- ),
- map(
- preceded(tag_no_case(b"tls_cipher_bits="), parameter_str),
- |bits| TlsCipherBits(bits),
- ),
- map(preceded(tag_no_case(b"tls_pfs="), parameter_str), |pfs| {
- TlsPfs(pfs)
- }),
- map(
- preceded(tag_no_case(b"tls_protocol="), parameter_str),
- |proto| TlsProtocol(proto),
- ),
- map(
- preceded(tag_no_case(b"valid-client-cert="), parameter_str),
- |cert| ValidClientCert(cert),
- ),
- )),
- alt((
- map(preceded(tag_no_case(b"resp="), base64), |data| Resp(data)),
- map(
- tuple((parameter_name, tag(b"="), parameter)),
- |(n, _, v)| UnknownPair(n, v.into()),
- ),
- map(parameter, |v| UnknownBool(v.into())),
- )),
- ))(input)
-}
-
-fn auth_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
- let mut parser = tuple((
- tag_no_case(b"AUTH"),
- tab,
- u64,
- tab,
- mechanism,
- tab,
- service,
- map(opt(preceded(tab, separated_list0(tab, auth_option))), |o| {
- o.unwrap_or(vec![])
- }),
- ));
- let (input, (_, _, id, _, mech, _, service, options)) = parser(input)?;
- Ok((
- input,
- ClientCommand::Auth {
- id,
- mech,
- service,
- options,
- },
- ))
-}
-
-fn is_base64_core(c: u8) -> bool {
- c >= 0x30 && c <= 0x39 // 0-9
- || c >= 0x41 && c <= 0x5a // A-Z
- || c >= 0x61 && c <= 0x7a // a-z
- || c == 0x2b // +
- || c == 0x2f // /
-}
-
-fn is_base64_pad(c: u8) -> bool {
- c == 0x3d // =
-}
-
-fn base64(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
- let (input, (b64, _)) = tuple((take_while1(is_base64_core), take_while(is_base64_pad)))(input)?;
-
- let data = base64::engine::general_purpose::STANDARD_NO_PAD
- .decode(b64)
- .map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))?;
-
- Ok((input, data))
-}
-
-/// @FIXME Dovecot does not say if base64 content must be padded or not
-fn cont_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
- let mut parser = tuple((tag_no_case(b"CONT"), tab, u64, tab, base64));
-
- let (input, (_, _, id, _, data)) = parser(input)?;
- Ok((input, ClientCommand::Cont { id, data }))
-}
-
-fn client_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
- alt((version_command, cpid_command, auth_command, cont_command))(input)
-}
-
-/*
-fn server_command(buf: &u8) -> IResult<&u8, ServerCommand> {
- unimplemented!();
-}
-*/
-
-// -----------------------------------------------------------------
-//
-// SASL DECODING
-//
-// -----------------------------------------------------------------
-
-fn not_null(c: u8) -> bool {
- c != 0x0
-}
-
-// impersonated user, login, password
-fn auth_plain<'a>(input: &'a [u8]) -> IResult<&'a [u8], (&'a [u8], &'a [u8], &'a [u8])> {
- map(
- tuple((
- take_while(not_null),
- take(1usize),
- take_while(not_null),
- take(1usize),
- rest,
- )),
- |(imp, _, user, _, pass)| (imp, user, pass),
- )(input)
-}
-
-// -----------------------------------------------------------------
-//
-// DOVECOT AUTH ENCODING
-//
-// ------------------------------------------------------------------
-use tokio_util::bytes::{BufMut, BytesMut};
-trait Encode {
- fn encode(&self, out: &mut BytesMut) -> Result<()>;
-}
-
-fn tab_enc(out: &mut BytesMut) {
- out.put(&[0x09][..])
-}
-
-fn lf_enc(out: &mut BytesMut) {
- out.put(&[0x0A][..])
-}
-
-impl Encode for Mechanism {
- fn encode(&self, out: &mut BytesMut) -> Result<()> {
- match self {
- Self::Plain => out.put(&b"PLAIN"[..]),
- Self::Login => out.put(&b"LOGIN"[..]),
- }
- Ok(())
- }
-}
-
-impl Encode for MechanismParameters {
- fn encode(&self, out: &mut BytesMut) -> Result<()> {
- match self {
- Self::Anonymous => out.put(&b"anonymous"[..]),
- Self::PlainText => out.put(&b"plaintext"[..]),
- Self::Dictionary => out.put(&b"dictionary"[..]),
- Self::Active => out.put(&b"active"[..]),
- Self::ForwardSecrecy => out.put(&b"forward-secrecy"[..]),
- Self::MutualAuth => out.put(&b"mutual-auth"[..]),
- Self::Private => out.put(&b"private"[..]),
- }
- Ok(())
- }
-}
-
-impl Encode for FailCode {
- fn encode(&self, out: &mut BytesMut) -> Result<()> {
- match self {
- Self::TempFail => out.put(&b"temp_fail"[..]),
- Self::AuthzFail => out.put(&b"authz_fail"[..]),
- Self::UserDisabled => out.put(&b"user_disabled"[..]),
- Self::PassExpired => out.put(&b"pass_expired"[..]),
- };
- Ok(())
- }
-}
-
-impl Encode for ServerCommand {
- fn encode(&self, out: &mut BytesMut) -> Result<()> {
- match self {
- Self::Version(Version { major, minor }) => {
- out.put(&b"VERSION"[..]);
- tab_enc(out);
- out.put(major.to_string().as_bytes());
- tab_enc(out);
- out.put(minor.to_string().as_bytes());
- lf_enc(out);
- }
- Self::Spid(pid) => {
- out.put(&b"SPID"[..]);
- tab_enc(out);
- out.put(pid.to_string().as_bytes());
- lf_enc(out);
- }
- Self::Cuid(pid) => {
- out.put(&b"CUID"[..]);
- tab_enc(out);
- out.put(pid.to_string().as_bytes());
- lf_enc(out);
- }
- Self::Cookie(cval) => {
- out.put(&b"COOKIE"[..]);
- tab_enc(out);
- out.put(hex::encode(cval).as_bytes());
- lf_enc(out);
- }
- Self::Mech { kind, parameters } => {
- out.put(&b"MECH"[..]);
- tab_enc(out);
- kind.encode(out)?;
- for p in parameters.iter() {
- tab_enc(out);
- p.encode(out)?;
- }
- lf_enc(out);
- }
- Self::Done => {
- out.put(&b"DONE"[..]);
- lf_enc(out);
- }
- Self::Cont { id, data } => {
- out.put(&b"CONT"[..]);
- tab_enc(out);
- out.put(id.to_string().as_bytes());
- tab_enc(out);
- if let Some(rdata) = data {
- let b64 = base64::engine::general_purpose::STANDARD.encode(rdata);
- out.put(b64.as_bytes());
- }
- lf_enc(out);
- }
- Self::Ok {
- id,
- user_id,
- extra_parameters,
- } => {
- out.put(&b"OK"[..]);
- tab_enc(out);
- out.put(id.to_string().as_bytes());
- if let Some(user) = user_id {
- tab_enc(out);
- out.put(&b"user="[..]);
- out.put(user.as_bytes());
- }
- for p in extra_parameters.iter() {
- tab_enc(out);
- out.put(&p[..]);
- }
- lf_enc(out);
- }
- Self::Fail {
- id,
- user_id,
- code,
- extra_parameters,
- } => {
- out.put(&b"FAIL"[..]);
- tab_enc(out);
- out.put(id.to_string().as_bytes());
- if let Some(user) = user_id {
- tab_enc(out);
- out.put(&b"user="[..]);
- out.put(user.as_bytes());
- }
- if let Some(code_val) = code {
- tab_enc(out);
- out.put(&b"code="[..]);
- code_val.encode(out)?;
- }
- for p in extra_parameters.iter() {
- tab_enc(out);
- out.put(&p[..]);
- }
- lf_enc(out);
- }
- }
- Ok(())
- }
-}
diff --git a/src/k2v_util.rs b/src/k2v_util.rs
deleted file mode 100644
index 3cd969b..0000000
--- a/src/k2v_util.rs
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
-use anyhow::Result;
-// ---- UTIL: function to wait for a value to have changed in K2V ----
-
-pub async fn k2v_wait_value_changed(
- k2v: &storage::RowStore,
- key: &storage::RowRef,
-) -> Result<CausalValue> {
- loop {
- if let Some(ct) = prev_ct {
- match k2v.poll_item(pk, sk, ct.clone(), None).await? {
- None => continue,
- Some(cv) => return Ok(cv),
- }
- } else {
- match k2v.read_item(pk, sk).await {
- Err(k2v_client::Error::NotFound) => {
- k2v.insert_item(pk, sk, vec![0u8], None).await?;
- }
- Err(e) => return Err(e.into()),
- Ok(cv) => return Ok(cv),
- }
- }
- }
-}
-*/
diff --git a/tests/behavior.rs b/tests/behavior.rs
deleted file mode 100644
index 13baf0e..0000000
--- a/tests/behavior.rs
+++ /dev/null
@@ -1,357 +0,0 @@
-use anyhow::Context;
-
-mod common;
-use crate::common::constants::*;
-use crate::common::fragments::*;
-
-fn main() {
- rfc3501_imap4rev1_base();
- rfc6851_imapext_move();
- rfc4551_imapext_condstore();
- rfc2177_imapext_idle();
- rfc5161_imapext_enable(); // 1
- rfc3691_imapext_unselect(); // 2
- rfc7888_imapext_literal(); // 3
- rfc4315_imapext_uidplus(); // 4
- rfc5819_imapext_liststatus(); // 5
- println!("✅ SUCCESS 🌟🚀🥳🙏🥹");
-}
-
-fn rfc3501_imap4rev1_base() {
- println!("🧪 rfc3501_imap4rev1_base");
- common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| {
- connect(imap_socket).context("server says hello")?;
- capability(imap_socket, Extension::None).context("check server capabilities")?;
- login(imap_socket, Account::Alice).context("login test")?;
- create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?;
- let select_res =
- select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?;
- assert!(select_res.contains("* 0 EXISTS"));
-
- check(imap_socket).context("check must run")?;
- status(imap_socket, Mailbox::Archive, StatusKind::UidNext)
- .context("status of archive from inbox")?;
- lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
- lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?;
- noop_exists(imap_socket, 1).context("noop loop must detect a new email")?;
-
- let srv_msg = fetch(
- imap_socket,
- Selection::FirstId,
- FetchKind::Rfc822,
- FetchMod::None,
- )
- .context("fetch rfc822 message, should be our first message")?;
- let orig_email = std::str::from_utf8(EMAIL1)?;
- assert!(srv_msg.contains(orig_email));
-
- copy(imap_socket, Selection::FirstId, Mailbox::Archive)
- .context("copy message to the archive mailbox")?;
- append(imap_socket, Email::Basic).context("insert email in INBOX")?;
- noop_exists(imap_socket, 2).context("noop loop must detect a new email")?;
- search(imap_socket, SearchKind::Text("OoOoO")).expect("search should return something");
- store(
- imap_socket,
- Selection::FirstId,
- Flag::Deleted,
- StoreAction::AddFlags,
- StoreMod::None,
- )
- .context("should add delete flag to the email")?;
- expunge(imap_socket).context("expunge emails")?;
- rename_mailbox(imap_socket, Mailbox::Archive, Mailbox::Drafts)
- .context("Archive mailbox is renamed Drafts")?;
- delete_mailbox(imap_socket, Mailbox::Drafts).context("Drafts mailbox is deleted")?;
- Ok(())
- })
- .expect("test fully run");
-}
-
-fn rfc3691_imapext_unselect() {
- println!("🧪 rfc3691_imapext_unselect");
- common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| {
- connect(imap_socket).context("server says hello")?;
-
- lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
- lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?;
-
- capability(imap_socket, Extension::Unselect).context("check server capabilities")?;
- login(imap_socket, Account::Alice).context("login test")?;
- let select_res =
- select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?;
- assert!(select_res.contains("* 0 EXISTS"));
-
- noop_exists(imap_socket, 1).context("noop loop must detect a new email")?;
- store(
- imap_socket,
- Selection::FirstId,
- Flag::Deleted,
- StoreAction::AddFlags,
- StoreMod::None,
- )
- .context("add delete flags to the email")?;
- unselect(imap_socket)
- .context("unselect inbox while preserving email with the \\Delete flag")?;
- let select_res =
- select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox again")?;
- assert!(select_res.contains("* 1 EXISTS"));
-
- let srv_msg = fetch(
- imap_socket,
- Selection::FirstId,
- FetchKind::Rfc822,
- FetchMod::None,
- )
- .context("message is still present")?;
- let orig_email = std::str::from_utf8(EMAIL2)?;
- assert!(srv_msg.contains(orig_email));
-
- close(imap_socket).context("close inbox and expunge message")?;
- let select_res = select(imap_socket, Mailbox::Inbox, SelectMod::None)
- .context("select inbox again and check it's empty")?;
- assert!(select_res.contains("* 0 EXISTS"));
-
- Ok(())
- })
- .expect("test fully run");
-}
-
-fn rfc5161_imapext_enable() {
- println!("🧪 rfc5161_imapext_enable");
- common::aerogramme_provider_daemon_dev(|imap_socket, _lmtp_socket| {
- connect(imap_socket).context("server says hello")?;
- login(imap_socket, Account::Alice).context("login test")?;
- enable(imap_socket, Enable::Utf8Accept, Some(Enable::Utf8Accept))?;
- enable(imap_socket, Enable::Utf8Accept, None)?;
- logout(imap_socket)?;
-
- Ok(())
- })
- .expect("test fully run");
-}
-
-fn rfc6851_imapext_move() {
- println!("🧪 rfc6851_imapext_move");
- common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| {
- connect(imap_socket).context("server says hello")?;
-
- capability(imap_socket, Extension::Move).context("check server capabilities")?;
- login(imap_socket, Account::Alice).context("login test")?;
- create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?;
- let select_res =
- select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?;
- assert!(select_res.contains("* 0 EXISTS"));
-
- lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
- lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?;
-
- noop_exists(imap_socket, 1).context("noop loop must detect a new email")?;
- r#move(imap_socket, Selection::FirstId, Mailbox::Archive)
- .context("message from inbox moved to archive")?;
-
- unselect(imap_socket)
- .context("unselect inbox while preserving email with the \\Delete flag")?;
- let select_res =
- select(imap_socket, Mailbox::Archive, SelectMod::None).context("select archive")?;
- assert!(select_res.contains("* 1 EXISTS"));
-
- let srv_msg = fetch(
- imap_socket,
- Selection::FirstId,
- FetchKind::Rfc822,
- FetchMod::None,
- )
- .context("check mail exists")?;
- let orig_email = std::str::from_utf8(EMAIL2)?;
- assert!(srv_msg.contains(orig_email));
-
- logout(imap_socket).context("must quit")?;
-
- Ok(())
- })
- .expect("test fully run");
-}
-
-fn rfc7888_imapext_literal() {
- println!("🧪 rfc7888_imapext_literal");
- common::aerogramme_provider_daemon_dev(|imap_socket, _lmtp_socket| {
- connect(imap_socket).context("server says hello")?;
-
- capability(imap_socket, Extension::LiteralPlus).context("check server capabilities")?;
- login_with_literal(imap_socket, Account::Alice).context("use literal to connect Alice")?;
-
- Ok(())
- })
- .expect("test fully run");
-}
-
-fn rfc4551_imapext_condstore() {
- println!("🧪 rfc4551_imapext_condstore");
- common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| {
- // Setup the test
- connect(imap_socket).context("server says hello")?;
-
- // RFC 3.1.1 Advertising Support for CONDSTORE
- capability(imap_socket, Extension::Condstore).context("check server capabilities")?;
- login(imap_socket, Account::Alice).context("login test")?;
-
- // RFC 3.1.8. CONDSTORE Parameter to SELECT and EXAMINE
- let select_res =
- select(imap_socket, Mailbox::Inbox, SelectMod::Condstore).context("select inbox")?;
- // RFC 3.1.2 New OK Untagged Responses for SELECT and EXAMINE
- assert!(select_res.contains("[HIGHESTMODSEQ 1]"));
-
- // RFC 3.1.3. STORE and UID STORE Commands
- lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
- lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?;
- lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?;
- noop_exists(imap_socket, 2).context("noop loop must detect a new email")?;
- let store_res = store(
- imap_socket,
- Selection::All,
- Flag::Important,
- StoreAction::AddFlags,
- StoreMod::UnchangedSince(1),
- )?;
- assert!(store_res.contains("[MODIFIED 2]"));
- assert!(store_res.contains("* 1 FETCH (FLAGS (\\Important) MODSEQ (3))"));
- assert!(!store_res.contains("* 2 FETCH"));
- assert_eq!(store_res.lines().count(), 2);
-
- // RFC 3.1.4. FETCH and UID FETCH Commands
- let fetch_res = fetch(
- imap_socket,
- Selection::All,
- FetchKind::Rfc822Size,
- FetchMod::ChangedSince(2),
- )?;
- assert!(fetch_res.contains("* 1 FETCH (RFC822.SIZE 81 MODSEQ (3))"));
- assert!(!fetch_res.contains("* 2 FETCH"));
- assert_eq!(store_res.lines().count(), 2);
-
- // RFC 3.1.5. MODSEQ Search Criterion in SEARCH
- let search_res = search(imap_socket, SearchKind::ModSeq(3))?;
- // RFC 3.1.6. Modified SEARCH Untagged Response
- assert!(search_res.contains("* SEARCH 1 (MODSEQ 3)"));
-
- // RFC 3.1.7 HIGHESTMODSEQ Status Data Items
- let status_res = status(imap_socket, Mailbox::Inbox, StatusKind::HighestModSeq)?;
- assert!(status_res.contains("HIGHESTMODSEQ 3"));
-
- Ok(())
- })
- .expect("test fully run");
-}
-
-fn rfc2177_imapext_idle() {
- println!("🧪 rfc2177_imapext_idle");
- common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| {
- // Test setup, check capability
- connect(imap_socket).context("server says hello")?;
- capability(imap_socket, Extension::Idle).context("check server capabilities")?;
- login(imap_socket, Account::Alice).context("login test")?;
- select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?;
-
- // Check that new messages from LMTP are correctly detected during idling
- start_idle(imap_socket).context("can't start idling")?;
- lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
- lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?;
- let srv_msg = stop_idle(imap_socket).context("stop idling")?;
- assert!(srv_msg.contains("* 1 EXISTS"));
-
- Ok(())
- })
- .expect("test fully run");
-}
-
-fn rfc4315_imapext_uidplus() {
- println!("🧪 rfc4315_imapext_uidplus");
- common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| {
- // Test setup, check capability, insert 2 emails
- connect(imap_socket).context("server says hello")?;
- capability(imap_socket, Extension::UidPlus).context("check server capabilities")?;
- login(imap_socket, Account::Alice).context("login test")?;
- select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?;
- lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
- lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?;
- lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?;
- noop_exists(imap_socket, 2).context("noop loop must detect a new email")?;
-
- // Check UID EXPUNGE seqset
- store(
- imap_socket,
- Selection::All,
- Flag::Deleted,
- StoreAction::AddFlags,
- StoreMod::None,
- )?;
- let res = uid_expunge(imap_socket, Selection::FirstId)?;
- assert_eq!(res.lines().count(), 2);
- assert!(res.contains("* 1 EXPUNGE"));
-
- // APPENDUID check UID + UID VALIDITY
- // Note: 4 and not 3, as we update the UID counter when we delete an email
- // it's part of our UID proof
- let res = append(imap_socket, Email::Multipart)?;
- assert!(res.contains("[APPENDUID 1 4]"));
-
- // COPYUID, check
- create_mailbox(imap_socket, Mailbox::Archive).context("created mailbox archive")?;
- let res = copy(imap_socket, Selection::FirstId, Mailbox::Archive)?;
- assert!(res.contains("[COPYUID 1 2 1]"));
-
- // MOVEUID, check
- let res = r#move(imap_socket, Selection::FirstId, Mailbox::Archive)?;
- assert!(res.contains("[COPYUID 1 2 2]"));
-
- Ok(())
- })
- .expect("test fully run");
-}
-
-///
-/// Example
-///
-/// ```text
-/// 30 list "" "*" RETURN (STATUS (MESSAGES UNSEEN))
-/// * LIST (\Subscribed) "." INBOX
-/// * STATUS INBOX (MESSAGES 2 UNSEEN 1)
-/// 30 OK LIST completed
-/// ```
-fn rfc5819_imapext_liststatus() {
- println!("🧪 rfc5819_imapext_liststatus");
- common::aerogramme_provider_daemon_dev(|imap_socket, lmtp_socket| {
- // Test setup, check capability, add 2 emails, read 1
- connect(imap_socket).context("server says hello")?;
- capability(imap_socket, Extension::ListStatus).context("check server capabilities")?;
- login(imap_socket, Account::Alice).context("login test")?;
- select(imap_socket, Mailbox::Inbox, SelectMod::None).context("select inbox")?;
- lmtp_handshake(lmtp_socket).context("handshake lmtp done")?;
- lmtp_deliver_email(lmtp_socket, Email::Basic).context("mail delivered successfully")?;
- lmtp_deliver_email(lmtp_socket, Email::Multipart).context("mail delivered successfully")?;
- noop_exists(imap_socket, 2).context("noop loop must detect a new email")?;
- fetch(
- imap_socket,
- Selection::FirstId,
- FetchKind::Rfc822,
- FetchMod::None,
- )
- .context("read one message")?;
- close(imap_socket).context("close inbox")?;
-
- // Test return status MESSAGES UNSEEN
- let ret = list(
- imap_socket,
- MbxSelect::All,
- ListReturn::StatusMessagesUnseen,
- )?;
- assert!(ret.contains("* STATUS INBOX (MESSAGES 2 UNSEEN 1)"));
-
- // Test that without RETURN, no status is sent
- let ret = list(imap_socket, MbxSelect::All, ListReturn::None)?;
- assert!(!ret.contains("* STATUS"));
-
- Ok(())
- })
- .expect("test fully run");
-}
diff --git a/tests/common/constants.rs b/tests/common/constants.rs
deleted file mode 100644
index c11a04d..0000000
--- a/tests/common/constants.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-use std::time;
-
-pub static SMALL_DELAY: time::Duration = time::Duration::from_millis(200);
-
-pub static EMAIL1: &[u8] = b"Date: Sat, 8 Jul 2023 07:14:29 +0200\r
-From: Bob Robert <bob@example.tld>\r
-To: Alice Malice <alice@example.tld>\r
-CC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>\r
-Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\r
- =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\r
-X-Unknown: something something\r
-Bad entry\r
- on multiple lines\r
-Message-ID: <NTAxNzA2AC47634Y366BAMTY4ODc5MzQyODY0ODY5@www.grrrndzero.org>\r
-MIME-Version: 1.0\r
-Content-Type: multipart/alternative;\r
- boundary=\"b1_e376dc71bafc953c0b0fdeb9983a9956\"\r
-Content-Transfer-Encoding: 7bit\r
-\r
-This is a multi-part message in MIME format.\r
-\r
---b1_e376dc71bafc953c0b0fdeb9983a9956\r
-Content-Type: text/plain; charset=utf-8\r
-Content-Transfer-Encoding: quoted-printable\r
-\r
-GZ\r
-OoOoO\r
-oOoOoOoOo\r
-oOoOoOoOoOoOoOoOo\r
-oOoOoOoOoOoOoOoOoOoOoOo\r
-oOoOoOoOoOoOoOoOoOoOoOoOoOoOo\r
-OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO\r
-\r
---b1_e376dc71bafc953c0b0fdeb9983a9956\r
-Content-Type: text/html; charset=us-ascii\r
-\r
-<div style=\"text-align: center;\"><strong>GZ</strong><br />\r
-OoOoO<br />\r
-oOoOoOoOo<br />\r
-oOoOoOoOoOoOoOoOo<br />\r
-oOoOoOoOoOoOoOoOoOoOoOo<br />\r
-oOoOoOoOoOoOoOoOoOoOoOoOoOoOo<br />\r
-OoOoOoOoOoOoOoOoOoOoOoOoOoOoOoOoO<br />\r
-</div>\r
-\r
---b1_e376dc71bafc953c0b0fdeb9983a9956--\r
-";
-
-pub static EMAIL2: &[u8] = b"From: alice@example.com\r
-To: alice@example.tld\r
-Subject: Test\r
-\r
-Hello world!\r
-";