mirror of
https://github.com/lennart-k/rustical.git
synced 2026-01-30 15:18:22 +00:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
276e65d41a | ||
|
|
7c3e9ecbc1 | ||
|
|
53f81a9433 | ||
|
|
55eabfde4a | ||
|
|
5d9d6e3fdf | ||
|
|
1567bc64ef | ||
|
|
44ae995f29 | ||
|
|
9c1cd24d32 | ||
|
|
ff0c5697cf | ||
|
|
6ccb5a67e4 | ||
|
|
da718dd290 | ||
|
|
4112347e24 | ||
|
|
f4fd1cdd21 | ||
|
|
5f0ca67e54 | ||
|
|
3aef9abe48 | ||
|
|
9784f2b53f | ||
|
|
271fdfd686 | ||
|
|
4fabf74333 | ||
|
|
7b154adec3 | ||
|
|
951a1e4bdc | ||
|
|
8c44733d0a | ||
|
|
829f7b727f | ||
|
|
037e6f5c92 | ||
|
|
311ceb6bc5 | ||
|
|
1174af3a4b | ||
|
|
845b3e61e3 | ||
|
|
e5d6541ffb | ||
|
|
b632ff6fe8 | ||
|
|
1ee873ac93 | ||
|
|
bf5bdb96bc | ||
|
|
47c2a55941 | ||
|
|
bfcd94f096 | ||
|
|
8cbb72719d | ||
|
|
ff0246c4fc | ||
|
|
15124a2fd5 | ||
|
|
5c6f63a5f3 | ||
|
|
17ba8faef2 | ||
|
|
578ddde36d | ||
|
|
9c3972e21c | ||
|
|
816c26565a | ||
|
|
4de0f9f665 | ||
|
|
cf31a51965 | ||
|
|
9fc099f6f4 | ||
|
|
498be172c9 | ||
|
|
b8c395e746 | ||
|
|
4b8a8c61f2 | ||
|
|
f778c470d0 | ||
|
|
d44a172261 | ||
|
|
e0ba34baea | ||
|
|
a7893ddbda | ||
|
|
ed7becffc2 |
19
.github/workflows/cicd.yml
vendored
19
.github/workflows/cicd.yml
vendored
@@ -5,6 +5,7 @@ permissions:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
RUST_VERSION: "1.92"
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -12,7 +13,9 @@ jobs:
|
|||||||
name: Check
|
name: Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: rustup update
|
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- run: cargo check
|
- run: cargo check
|
||||||
@@ -21,7 +24,9 @@ jobs:
|
|||||||
name: Test Suite
|
name: Test Suite
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: rustup update
|
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- run: cargo test --all-features --verbose --workspace
|
- run: cargo test --all-features --verbose --workspace
|
||||||
@@ -30,7 +35,9 @@ jobs:
|
|||||||
name: Test Coverage
|
name: Test Coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: rustup update
|
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
- name: Install tarpaulin
|
- name: Install tarpaulin
|
||||||
run: cargo install cargo-tarpaulin
|
run: cargo install cargo-tarpaulin
|
||||||
|
|
||||||
@@ -44,8 +51,10 @@ jobs:
|
|||||||
name: Lints
|
name: Lints
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: rustup update
|
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
- run: rustup component add rustfmt clippy
|
with:
|
||||||
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
6
.github/workflows/docker-publish.yml
vendored
6
.github/workflows/docker-publish.yml
vendored
@@ -26,12 +26,12 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/setup-buildx-action
|
# https://github.com/docker/setup-buildx-action
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
# https://github.com/docker/login-action
|
# https://github.com/docker/login-action
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
# https://github.com/docker/build-push-action
|
# https://github.com/docker/build-push-action
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/arm64,linux/amd64
|
platforms: linux/arm64,linux/amd64
|
||||||
|
|||||||
344
Cargo.lock
generated
344
Cargo.lock
generated
@@ -129,11 +129,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "askama"
|
name = "askama"
|
||||||
version = "0.14.0"
|
version = "0.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"
|
checksum = "bb7125972258312e79827b60c9eb93938334100245081cf701a2dee981b17427"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama_derive",
|
"askama_macros",
|
||||||
"itoa",
|
"itoa",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -142,9 +142,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "askama_derive"
|
name = "askama_derive"
|
||||||
version = "0.14.0"
|
version = "0.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"
|
checksum = "8ba5e7259a1580c61571e3116ebaaa01e3c001b2132b17c4cc5c70780ca3e994"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama_parser",
|
"askama_parser",
|
||||||
"basic-toml",
|
"basic-toml",
|
||||||
@@ -154,26 +154,36 @@ dependencies = [
|
|||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_macros"
|
||||||
|
version = "0.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "236ce20b77cb13506eaf5024899f4af6e12e8825f390bd943c4c37fd8f322e46"
|
||||||
|
dependencies = [
|
||||||
|
"askama_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "askama_parser"
|
name = "askama_parser"
|
||||||
version = "0.14.0"
|
version = "0.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"
|
checksum = "f3c63392767bb2df6aa65a6e1e3b80fd89bb7af6d58359b924c0695620f1512e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
|
"unicode-ident",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "askama_web"
|
name = "askama_web"
|
||||||
version = "0.14.7"
|
version = "0.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e1acadd534892f9ef8c3809b47997e3cd857fad735edceff77a88be1c8236920"
|
checksum = "c0d6576f8e59513752a3e2673ca602fb403be7d0d0aacba5cd8b219838ab58fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
"askama_web_derive",
|
"askama_web_derive",
|
||||||
@@ -184,13 +194,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "askama_web_derive"
|
name = "askama_web_derive"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34921de3d57974069bad483fdfe0ec65d88c4ff892edd1ab4d8b03be0dda1b9b"
|
checksum = "9767c17d33a63daf6da5872ffaf2ab0c289cd73ce7ed4f41d5ddf9149c004873"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -325,7 +335,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -467,9 +477,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64ct"
|
name = "base64ct"
|
||||||
version = "1.8.1"
|
version = "1.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a"
|
checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "basic-toml"
|
name = "basic-toml"
|
||||||
@@ -563,9 +573,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.51"
|
version = "1.2.52"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
|
checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@@ -613,7 +623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"
|
checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"phf",
|
"phf 0.12.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -645,9 +655,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.53"
|
version = "4.5.54"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
|
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -655,9 +665,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.53"
|
version = "4.5.54"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
|
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -674,7 +684,7 @@ dependencies = [
|
|||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -888,7 +898,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -922,7 +932,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"strsim",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -935,7 +945,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"strsim",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -946,7 +956,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core 0.21.3",
|
"darling_core 0.21.3",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -957,7 +967,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core 0.23.0",
|
"darling_core 0.23.0",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1000,7 +1010,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1024,7 +1034,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1231,9 +1241,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
|
checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flume"
|
name = "flume"
|
||||||
@@ -1361,7 +1371,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1474,9 +1484,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.12"
|
version = "0.4.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
|
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1484,7 +1494,7 @@ dependencies = [
|
|||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"http",
|
"http",
|
||||||
"indexmap 2.12.1",
|
"indexmap 2.13.0",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -1761,7 +1771,23 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ical"
|
name = "ical"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
source = "git+https://github.com/lennart-k/ical-rs#5cce57a90a60a28845b1da5df34643663ec63da1"
|
source = "git+https://github.com/lennart-k/ical-rs?branch=dev#b1edcdf2bb7db5a302a5df3650218a9a16aefe0c"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"chrono-tz",
|
||||||
|
"derive_more",
|
||||||
|
"itertools 0.14.0",
|
||||||
|
"lazy_static",
|
||||||
|
"phf 0.13.1",
|
||||||
|
"regex",
|
||||||
|
"rrule",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ical"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "git+https://github.com/lennart-k/ical-rs#dcd3b106758a054f46a5172103abb17972ad032d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
@@ -1894,9 +1920,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.12.1"
|
version = "2.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
@@ -1912,9 +1938,9 @@ checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "insta"
|
name = "insta"
|
||||||
version = "1.45.0"
|
version = "1.46.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b76866be74d68b1595eb8060cb9191dca9c021db2316558e52ddc5d55d41b66c"
|
checksum = "1b66886d14d18d420ab5052cbff544fc5d34d0b2cdd35eb5976aaa10a4a472e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console",
|
"console",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -1931,9 +1957,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iri-string"
|
name = "iri-string"
|
||||||
version = "0.7.9"
|
version = "0.7.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
|
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2008,9 +2034,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.178"
|
version = "0.2.180"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
|
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libm"
|
name = "libm"
|
||||||
@@ -2324,7 +2350,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2559,7 +2585,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"proc-macro2-diagnostics",
|
"proc-macro2-diagnostics",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2583,7 +2609,18 @@ version = "0.12.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
|
checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_shared",
|
"phf_shared 0.12.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||||
|
dependencies = [
|
||||||
|
"phf_macros",
|
||||||
|
"phf_shared 0.13.1",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2592,8 +2629,8 @@ version = "0.12.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "efbdcb6f01d193b17f0b9c3360fa7e0e620991b193ff08702f78b3ce365d7e61"
|
checksum = "efbdcb6f01d193b17f0b9c3360fa7e0e620991b193ff08702f78b3ce365d7e61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_generator",
|
"phf_generator 0.12.1",
|
||||||
"phf_shared",
|
"phf_shared 0.12.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2603,7 +2640,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b"
|
checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"phf_shared",
|
"phf_shared 0.12.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"phf_shared 0.13.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_macros"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator 0.13.1",
|
||||||
|
"phf_shared 0.13.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2615,6 +2675,15 @@ dependencies = [
|
|||||||
"siphasher",
|
"siphasher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.10"
|
version = "1.1.10"
|
||||||
@@ -2632,7 +2701,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2771,9 +2840,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.104"
|
version = "1.0.105"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
|
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -2786,7 +2855,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
"version_check",
|
"version_check",
|
||||||
"yansi",
|
"yansi",
|
||||||
]
|
]
|
||||||
@@ -2811,7 +2880,7 @@ dependencies = [
|
|||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2880,9 +2949,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.42"
|
version = "1.0.43"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@@ -3007,7 +3076,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3138,9 +3207,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.9"
|
version = "0.9.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
|
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"const-oid",
|
"const-oid",
|
||||||
"digest",
|
"digest",
|
||||||
@@ -3181,7 +3250,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"relative-path",
|
"relative-path",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3193,7 +3262,7 @@ checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3226,7 +3295,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rust-embed-utils",
|
"rust-embed-utils",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3263,7 +3332,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical"
|
name = "rustical"
|
||||||
version = "0.11.7"
|
version = "0.11.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -3274,6 +3343,7 @@ dependencies = [
|
|||||||
"figment",
|
"figment",
|
||||||
"headers",
|
"headers",
|
||||||
"http",
|
"http",
|
||||||
|
"ical 0.11.0 (git+https://github.com/lennart-k/ical-rs?branch=dev)",
|
||||||
"insta",
|
"insta",
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
"opentelemetry-otlp",
|
"opentelemetry-otlp",
|
||||||
@@ -3296,7 +3366,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml 0.9.10+spec-1.1.0",
|
"toml 0.9.11+spec-1.1.0",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-sessions",
|
"tower-sessions",
|
||||||
@@ -3308,7 +3378,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_caldav"
|
name = "rustical_caldav"
|
||||||
version = "0.11.7"
|
version = "0.11.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3321,7 +3391,7 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"headers",
|
"headers",
|
||||||
"http",
|
"http",
|
||||||
"ical",
|
"ical 0.11.0 (git+https://github.com/lennart-k/ical-rs)",
|
||||||
"insta",
|
"insta",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
@@ -3350,7 +3420,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_carddav"
|
name = "rustical_carddav"
|
||||||
version = "0.11.7"
|
version = "0.11.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3360,10 +3430,11 @@ dependencies = [
|
|||||||
"derive_more",
|
"derive_more",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"ical",
|
"ical 0.11.0 (git+https://github.com/lennart-k/ical-rs)",
|
||||||
"insta",
|
"insta",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
|
"rstest",
|
||||||
"rustical_dav",
|
"rustical_dav",
|
||||||
"rustical_dav_push",
|
"rustical_dav_push",
|
||||||
"rustical_ical",
|
"rustical_ical",
|
||||||
@@ -3383,7 +3454,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_dav"
|
name = "rustical_dav"
|
||||||
version = "0.11.7"
|
version = "0.11.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3392,7 +3463,7 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"headers",
|
"headers",
|
||||||
"http",
|
"http",
|
||||||
"ical",
|
"ical 0.11.0 (git+https://github.com/lennart-k/ical-rs)",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"log",
|
"log",
|
||||||
"matchit 0.9.1",
|
"matchit 0.9.1",
|
||||||
@@ -3409,7 +3480,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_dav_push"
|
name = "rustical_dav_push"
|
||||||
version = "0.11.7"
|
version = "0.11.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3434,7 +3505,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_frontend"
|
name = "rustical_frontend"
|
||||||
version = "0.11.7"
|
version = "0.11.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
"askama_web",
|
"askama_web",
|
||||||
@@ -3470,24 +3541,26 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_ical"
|
name = "rustical_ical"
|
||||||
version = "0.11.7"
|
version = "0.11.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"ical",
|
"ical 0.11.0 (git+https://github.com/lennart-k/ical-rs)",
|
||||||
"regex",
|
"regex",
|
||||||
"rrule",
|
"rrule",
|
||||||
|
"rstest",
|
||||||
"rustical_xml",
|
"rustical_xml",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"similar-asserts",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_oidc"
|
name = "rustical_oidc"
|
||||||
version = "0.11.7"
|
version = "0.11.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3503,7 +3576,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_store"
|
name = "rustical_store"
|
||||||
version = "0.11.7"
|
version = "0.11.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3515,7 +3588,7 @@ dependencies = [
|
|||||||
"futures-core",
|
"futures-core",
|
||||||
"headers",
|
"headers",
|
||||||
"http",
|
"http",
|
||||||
"ical",
|
"ical 0.11.0 (git+https://github.com/lennart-k/ical-rs)",
|
||||||
"regex",
|
"regex",
|
||||||
"rrule",
|
"rrule",
|
||||||
"rstest",
|
"rstest",
|
||||||
@@ -3536,7 +3609,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_store_sqlite"
|
name = "rustical_store_sqlite"
|
||||||
version = "0.11.7"
|
version = "0.11.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -3559,7 +3632,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_xml"
|
name = "rustical_xml"
|
||||||
version = "0.11.7"
|
version = "0.11.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
@@ -3581,9 +3654,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.35"
|
version = "0.23.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
|
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ring",
|
"ring",
|
||||||
@@ -3722,14 +3795,14 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.148"
|
version = "1.0.149"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -3798,7 +3871,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"hex",
|
"hex",
|
||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"indexmap 2.12.1",
|
"indexmap 2.13.0",
|
||||||
"schemars 0.9.0",
|
"schemars 0.9.0",
|
||||||
"schemars 1.2.0",
|
"schemars 1.2.0",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
@@ -3816,7 +3889,7 @@ dependencies = [
|
|||||||
"darling 0.21.3",
|
"darling 0.21.3",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3978,7 +4051,7 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"hashbrown 0.15.5",
|
"hashbrown 0.15.5",
|
||||||
"hashlink",
|
"hashlink",
|
||||||
"indexmap 2.12.1",
|
"indexmap 2.13.0",
|
||||||
"log",
|
"log",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -4005,7 +4078,7 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"sqlx-macros-core",
|
"sqlx-macros-core",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4028,7 +4101,7 @@ dependencies = [
|
|||||||
"sqlx-mysql",
|
"sqlx-mysql",
|
||||||
"sqlx-postgres",
|
"sqlx-postgres",
|
||||||
"sqlx-sqlite",
|
"sqlx-sqlite",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
@@ -4180,7 +4253,7 @@ dependencies = [
|
|||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4202,9 +4275,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.111"
|
version = "2.0.114"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -4228,7 +4301,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4270,7 +4343,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4281,7 +4354,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4361,9 +4434,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.48.0"
|
version = "1.49.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -4385,7 +4458,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4400,9 +4473,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-stream"
|
name = "tokio-stream"
|
||||||
version = "0.1.17"
|
version = "0.1.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
|
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -4411,9 +4484,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.17"
|
version = "0.7.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
|
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -4436,11 +4509,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.9.10+spec-1.1.0"
|
version = "0.9.11+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48"
|
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.12.1",
|
"indexmap 2.13.0",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_spanned 1.0.4",
|
"serde_spanned 1.0.4",
|
||||||
"toml_datetime 0.7.5+spec-1.1.0",
|
"toml_datetime 0.7.5+spec-1.1.0",
|
||||||
@@ -4473,7 +4546,7 @@ version = "0.22.27"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.12.1",
|
"indexmap 2.13.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned 0.6.9",
|
"serde_spanned 0.6.9",
|
||||||
"toml_datetime 0.6.11",
|
"toml_datetime 0.6.11",
|
||||||
@@ -4487,7 +4560,7 @@ version = "0.23.10+spec-1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
|
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.12.1",
|
"indexmap 2.13.0",
|
||||||
"toml_datetime 0.7.5+spec-1.1.0",
|
"toml_datetime 0.7.5+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow",
|
"winnow",
|
||||||
@@ -4559,7 +4632,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"indexmap 2.12.1",
|
"indexmap 2.13.0",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"slab",
|
"slab",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
@@ -4697,7 +4770,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4781,9 +4854,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
version = "2.8.1"
|
version = "2.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
@@ -4832,14 +4905,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.7"
|
version = "2.5.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
|
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4897,7 +4971,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "f6728de8767c8dea44f41b88115a205ed23adc3302e1b4342be59d922934dae5"
|
checksum = "f6728de8767c8dea44f41b88115a205ed23adc3302e1b4342be59d922934dae5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glob",
|
"glob",
|
||||||
"phf",
|
"phf 0.12.1",
|
||||||
"phf_codegen",
|
"phf_codegen",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4986,7 +5060,7 @@ dependencies = [
|
|||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5021,9 +5095,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "1.0.4"
|
version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
|
checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
@@ -5090,7 +5164,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5101,7 +5175,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5382,14 +5456,14 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xml_derive"
|
name = "xml_derive"
|
||||||
version = "0.11.7"
|
version = "0.11.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling 0.23.0",
|
"darling 0.23.0",
|
||||||
"heck",
|
"heck",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5417,28 +5491,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.31"
|
version = "0.8.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
|
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.31"
|
version = "0.8.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
|
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5458,7 +5532,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5498,11 +5572,11 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.111",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.1"
|
version = "1.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f5858cd3a46fff31e77adea2935e357e3a2538d870741617bfb7c943e218fee6"
|
checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"
|
||||||
|
|||||||
12
Cargo.toml
12
Cargo.toml
@@ -2,8 +2,8 @@
|
|||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.11.7"
|
version = "0.11.11"
|
||||||
rust-version = "1.91"
|
rust-version = "1.92"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A CalDAV server"
|
description = "A CalDAV server"
|
||||||
documentation = "https://lennart-k.github.io/rustical/"
|
documentation = "https://lennart-k.github.io/rustical/"
|
||||||
@@ -89,8 +89,8 @@ derive_more = { version = "2.1", features = [
|
|||||||
"constructor",
|
"constructor",
|
||||||
"display",
|
"display",
|
||||||
] }
|
] }
|
||||||
askama = { version = "0.14", features = ["serde_json"] }
|
askama = { version = "0.15", features = ["serde_json"] }
|
||||||
askama_web = { version = "0.14", features = ["axum-0.8"] }
|
askama_web = { version = "0.15", features = ["axum-0.8"] }
|
||||||
sqlx = { version = "0.8", default-features = false, features = [
|
sqlx = { version = "0.8", default-features = false, features = [
|
||||||
"sqlx-sqlite",
|
"sqlx-sqlite",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -201,3 +201,7 @@ tower-http.workspace = true
|
|||||||
axum-extra.workspace = true
|
axum-extra.workspace = true
|
||||||
headers.workspace = true
|
headers.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
|
# TODO: Remove in next major release
|
||||||
|
ical_dev = { package = "ical", git = "https://github.com/lennart-k/ical-rs", branch = "dev", features = [
|
||||||
|
"chrono-tz",
|
||||||
|
] }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM --platform=$BUILDPLATFORM rust:1.91-alpine AS chef
|
FROM --platform=$BUILDPLATFORM rust:1.92-alpine AS chef
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ARG BUILDPLATFORM
|
ARG BUILDPLATFORM
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
[](https://raw.githubusercontent.com/lennart-k/rustical/main/LICENSE)
|
||||||
|
[](https://coveralls.io/github/lennart-k/rustical?branch=main)
|
||||||
|
|
||||||
# RustiCal
|
# RustiCal
|
||||||
|
|
||||||
a CalDAV/CardDAV server
|
a CalDAV/CardDAV server
|
||||||
|
|||||||
@@ -90,14 +90,14 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
let calendar = IcalParser::new(tz.as_bytes())
|
let calendar = IcalParser::new(tz.as_bytes())
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?
|
.ok_or_else(|| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?
|
||||||
.map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?;
|
.map_err(|_| rustical_dav::Error::BadRequest("Error parsing timezone".to_owned()))?;
|
||||||
|
|
||||||
let timezone = calendar.timezones.first().ok_or_else(|| {
|
let timezone = calendar.timezones.first().ok_or_else(|| {
|
||||||
rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
|
rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
|
||||||
})?;
|
})?;
|
||||||
let timezone: chrono_tz::Tz = timezone
|
let timezone: chrono_tz::Tz = timezone.try_into().map_err(|_| {
|
||||||
.try_into()
|
rustical_dav::Error::BadRequest("Cannot translate VTIMEZONE into IANA TZID".to_owned())
|
||||||
.map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?;
|
})?;
|
||||||
|
|
||||||
Some(timezone.name().to_owned())
|
Some(timezone.name().to_owned())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ impl CompFilterable for CalendarObjectComponent {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use chrono::{TimeZone, Utc};
|
use chrono::{TimeZone, Utc};
|
||||||
use rustical_dav::xml::{NegateCondition, TextCollation, TextMatchElement};
|
use rustical_dav::xml::{MatchType, NegateCondition, TextCollation, TextMatchElement};
|
||||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||||
|
|
||||||
use crate::calendar::methods::report::calendar_query::{
|
use crate::calendar::methods::report::calendar_query::{
|
||||||
@@ -217,6 +217,7 @@ END:VCALENDAR";
|
|||||||
name: "VERSION".to_string(),
|
name: "VERSION".to_string(),
|
||||||
time_range: None,
|
time_range: None,
|
||||||
text_match: Some(TextMatchElement {
|
text_match: Some(TextMatchElement {
|
||||||
|
match_type: MatchType::Contains,
|
||||||
needle: "2.0".to_string(),
|
needle: "2.0".to_string(),
|
||||||
collation: TextCollation::default(),
|
collation: TextCollation::default(),
|
||||||
negate_condition: NegateCondition::default(),
|
negate_condition: NegateCondition::default(),
|
||||||
@@ -240,6 +241,7 @@ END:VCALENDAR";
|
|||||||
name: "SUMMARY".to_string(),
|
name: "SUMMARY".to_string(),
|
||||||
time_range: None,
|
time_range: None,
|
||||||
text_match: Some(TextMatchElement {
|
text_match: Some(TextMatchElement {
|
||||||
|
match_type: MatchType::Contains,
|
||||||
collation: TextCollation::default(),
|
collation: TextCollation::default(),
|
||||||
negate_condition: NegateCondition(false),
|
negate_condition: NegateCondition(false),
|
||||||
needle: "weekly".to_string(),
|
needle: "weekly".to_string(),
|
||||||
@@ -327,6 +329,7 @@ END:VCALENDAR";
|
|||||||
name: "TZID".to_string(),
|
name: "TZID".to_string(),
|
||||||
time_range: None,
|
time_range: None,
|
||||||
text_match: Some(TextMatchElement {
|
text_match: Some(TextMatchElement {
|
||||||
|
match_type: MatchType::Contains,
|
||||||
collation: TextCollation::AsciiCasemap,
|
collation: TextCollation::AsciiCasemap,
|
||||||
negate_condition: NegateCondition::default(),
|
negate_condition: NegateCondition::default(),
|
||||||
needle: "Europe/Berlin".to_string(),
|
needle: "Europe/Berlin".to_string(),
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use super::comp_filter::{CompFilterElement, CompFilterable};
|
use super::comp_filter::{CompFilterElement, CompFilterable};
|
||||||
use crate::calendar_object::CalendarObjectPropWrapperName;
|
use crate::calendar_object::CalendarObjectPropWrapperName;
|
||||||
|
use ical::property::Property;
|
||||||
use rustical_dav::xml::{PropfindType, TextMatchElement};
|
use rustical_dav::xml::{PropfindType, TextMatchElement};
|
||||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||||
use rustical_store::calendar_store::CalendarQuery;
|
use rustical_store::calendar_store::CalendarQuery;
|
||||||
use rustical_xml::XmlDeserialize;
|
use rustical_xml::{XmlDeserialize, XmlRootTag};
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -27,7 +28,25 @@ pub struct ParamFilterElement {
|
|||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
impl ParamFilterElement {
|
||||||
|
#[must_use]
|
||||||
|
pub fn match_property(&self, prop: &Property) -> bool {
|
||||||
|
let Some(param) = prop.get_param(&self.name) else {
|
||||||
|
return self.is_not_defined.is_some();
|
||||||
|
};
|
||||||
|
if self.is_not_defined.is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(text_match) = self.text_match.as_ref() else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
text_match.match_text(param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)]
|
||||||
|
#[xml(root = "filter", ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7
|
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7
|
||||||
pub struct FilterElement {
|
pub struct FilterElement {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ use rustical_store::CalendarStore;
|
|||||||
mod comp_filter;
|
mod comp_filter;
|
||||||
mod elements;
|
mod elements;
|
||||||
mod prop_filter;
|
mod prop_filter;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub use comp_filter::{CompFilterElement, CompFilterable};
|
pub use comp_filter::{CompFilterElement, CompFilterable};
|
||||||
pub use elements::*;
|
pub use elements::*;
|
||||||
@@ -27,7 +29,7 @@ pub async fn get_objects_calendar_query<C: CalendarStore>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod xml_tests {
|
||||||
use super::{
|
use super::{
|
||||||
CalendarQueryRequest, FilterElement, ParamFilterElement, comp_filter::CompFilterElement,
|
CalendarQueryRequest, FilterElement, ParamFilterElement, comp_filter::CompFilterElement,
|
||||||
prop_filter::PropFilterElement,
|
prop_filter::PropFilterElement,
|
||||||
@@ -36,7 +38,9 @@ mod tests {
|
|||||||
calendar::methods::report::ReportRequest,
|
calendar::methods::report::ReportRequest,
|
||||||
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
|
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
|
||||||
};
|
};
|
||||||
use rustical_dav::xml::{NegateCondition, PropElement, TextCollation, TextMatchElement};
|
use rustical_dav::xml::{
|
||||||
|
MatchType, NegateCondition, PropElement, TextCollation, TextMatchElement,
|
||||||
|
};
|
||||||
use rustical_xml::XmlDocument;
|
use rustical_xml::XmlDocument;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -49,16 +53,16 @@ mod tests {
|
|||||||
<C:calendar-data/>
|
<C:calendar-data/>
|
||||||
</D:prop>
|
</D:prop>
|
||||||
<C:filter>
|
<C:filter>
|
||||||
<C:comp-filter name="VCALENDAR">
|
<C:comp-filter name="VCALENDAR">
|
||||||
<C:comp-filter name="VEVENT">
|
<C:comp-filter name="VEVENT">
|
||||||
<C:prop-filter name="ATTENDEE">
|
<C:prop-filter name="ATTENDEE">
|
||||||
<C:text-match collation="i;ascii-casemap">mailto:lisa@example.com</C:text-match>
|
<C:text-match collation="i;ascii-casemap">mailto:lisa@example.com</C:text-match>
|
||||||
<C:param-filter name="PARTSTAT">
|
<C:param-filter name="PARTSTAT">
|
||||||
<C:text-match collation="i;ascii-casemap">NEEDS-ACTION</C:text-match>
|
<C:text-match collation="i;ascii-casemap">NEEDS-ACTION</C:text-match>
|
||||||
</C:param-filter>
|
</C:param-filter>
|
||||||
</C:prop-filter>
|
</C:prop-filter>
|
||||||
|
</C:comp-filter>
|
||||||
</C:comp-filter>
|
</C:comp-filter>
|
||||||
</C:comp-filter>
|
|
||||||
</C:filter>
|
</C:filter>
|
||||||
</C:calendar-query>
|
</C:calendar-query>
|
||||||
"#;
|
"#;
|
||||||
@@ -93,6 +97,7 @@ mod tests {
|
|||||||
prop_filter: vec![PropFilterElement {
|
prop_filter: vec![PropFilterElement {
|
||||||
name: "ATTENDEE".to_owned(),
|
name: "ATTENDEE".to_owned(),
|
||||||
text_match: Some(TextMatchElement {
|
text_match: Some(TextMatchElement {
|
||||||
|
match_type: MatchType::Contains,
|
||||||
collation: TextCollation::AsciiCasemap,
|
collation: TextCollation::AsciiCasemap,
|
||||||
negate_condition: NegateCondition(false),
|
negate_condition: NegateCondition(false),
|
||||||
needle: "mailto:lisa@example.com".to_string()
|
needle: "mailto:lisa@example.com".to_string()
|
||||||
@@ -102,6 +107,7 @@ mod tests {
|
|||||||
is_not_defined: None,
|
is_not_defined: None,
|
||||||
name: "PARTSTAT".to_owned(),
|
name: "PARTSTAT".to_owned(),
|
||||||
text_match: Some(TextMatchElement {
|
text_match: Some(TextMatchElement {
|
||||||
|
match_type: MatchType::Contains,
|
||||||
collation: TextCollation::AsciiCasemap,
|
collation: TextCollation::AsciiCasemap,
|
||||||
negate_condition: NegateCondition(false),
|
negate_condition: NegateCondition(false),
|
||||||
needle: "NEEDS-ACTION".to_string()
|
needle: "NEEDS-ACTION".to_string()
|
||||||
|
|||||||
@@ -30,18 +30,8 @@ pub struct PropFilterElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PropFilterElement {
|
impl PropFilterElement {
|
||||||
pub fn match_component(&self, comp: &impl PropFilterable) -> bool {
|
#[must_use]
|
||||||
let property = comp.get_property(&self.name);
|
pub fn match_property(&self, property: &Property) -> bool {
|
||||||
let property = match (self.is_not_defined.is_some(), property) {
|
|
||||||
// We are the component that's not supposed to be defined
|
|
||||||
(true, Some(_))
|
|
||||||
// We don't match
|
|
||||||
| (false, None) => return false,
|
|
||||||
// We shall not be and indeed we aren't
|
|
||||||
(true, None) => return true,
|
|
||||||
(false, Some(property)) => property
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(TimeRangeElement { start, end }) = &self.time_range {
|
if let Some(TimeRangeElement { start, end }) = &self.time_range {
|
||||||
// TODO: Respect timezones
|
// TODO: Respect timezones
|
||||||
let Ok(timestamp) = CalDateTime::parse_prop(property, &HashMap::default()) else {
|
let Ok(timestamp) = CalDateTime::parse_prop(property, &HashMap::default()) else {
|
||||||
@@ -67,58 +57,75 @@ impl PropFilterElement {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: param-filter
|
if !self
|
||||||
|
.param_filter
|
||||||
|
.iter()
|
||||||
|
.all(|param_filter| param_filter.match_property(property))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn match_component(&self, comp: &impl PropFilterable) -> bool {
|
||||||
|
let properties = comp.get_named_properties(&self.name);
|
||||||
|
if self.is_not_defined.is_some() {
|
||||||
|
return properties.is_empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The filter matches when one property instance matches
|
||||||
|
// Example where this matters: We have multiple attendees and want to match one
|
||||||
|
properties.iter().any(|prop| self.match_property(prop))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait PropFilterable {
|
pub trait PropFilterable {
|
||||||
fn get_property(&self, name: &str) -> Option<&Property>;
|
fn get_named_properties(&self, name: &str) -> Vec<&Property>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropFilterable for CalendarObject {
|
impl PropFilterable for CalendarObject {
|
||||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||||
Self::get_property(self, name)
|
Self::get_named_properties(self, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropFilterable for IcalEvent {
|
impl PropFilterable for IcalEvent {
|
||||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||||
Component::get_property(self, name)
|
Component::get_named_properties(self, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropFilterable for IcalTodo {
|
impl PropFilterable for IcalTodo {
|
||||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||||
Component::get_property(self, name)
|
Component::get_named_properties(self, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropFilterable for IcalJournal {
|
impl PropFilterable for IcalJournal {
|
||||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||||
Component::get_property(self, name)
|
Component::get_named_properties(self, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropFilterable for IcalCalendar {
|
impl PropFilterable for IcalCalendar {
|
||||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||||
Component::get_property(self, name)
|
Component::get_named_properties(self, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropFilterable for IcalTimeZone {
|
impl PropFilterable for IcalTimeZone {
|
||||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||||
Component::get_property(self, name)
|
Component::get_named_properties(self, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropFilterable for CalendarObjectComponent {
|
impl PropFilterable for CalendarObjectComponent {
|
||||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||||
match self {
|
match self {
|
||||||
Self::Event(event, _) => PropFilterable::get_property(&event.event, name),
|
Self::Event(event, _) => PropFilterable::get_named_properties(&event.event, name),
|
||||||
Self::Todo(todo, _) => PropFilterable::get_property(todo, name),
|
Self::Todo(todo, _) => PropFilterable::get_named_properties(todo, name),
|
||||||
Self::Journal(journal, _) => PropFilterable::get_property(journal, name),
|
Self::Journal(journal, _) => PropFilterable::get_named_properties(journal, name),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
use super::FilterElement;
|
||||||
|
use rstest::rstest;
|
||||||
|
use rustical_ical::CalendarObject;
|
||||||
|
use rustical_xml::XmlDocument;
|
||||||
|
|
||||||
|
const ICS_1: &str = r"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:20060206T001220Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060104T100000
|
||||||
|
DURATION:PT1H
|
||||||
|
LAST-MODIFIED:20060206T001330Z
|
||||||
|
ORGANIZER:mailto:cyrus@example.com
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:TENTATIVE
|
||||||
|
SUMMARY:Event #3
|
||||||
|
UID:DC6C50A017428C5216A2F1CD@example.com
|
||||||
|
X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
";
|
||||||
|
|
||||||
|
const FILTER_1: &str = r#"
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<C:filter xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<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>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const FILTER_2: &str = r#"
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<C:filter xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<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">ACCEPTED</C:text-match>
|
||||||
|
</C:param-filter>
|
||||||
|
</C:prop-filter>
|
||||||
|
</C:comp-filter>
|
||||||
|
</C:comp-filter>
|
||||||
|
</C:filter>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(ICS_1, FILTER_1, true)]
|
||||||
|
#[case(ICS_1, FILTER_2, false)]
|
||||||
|
fn yeet(#[case] ics: &str, #[case] filter: &str, #[case] matches: bool) {
|
||||||
|
let obj = CalendarObject::from_ics(ics.to_owned(), None).unwrap();
|
||||||
|
let filter = FilterElement::parse_str(filter).unwrap();
|
||||||
|
assert_eq!(matches, filter.matches(&obj));
|
||||||
|
}
|
||||||
@@ -174,7 +174,7 @@ mod tests {
|
|||||||
prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![
|
prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![
|
||||||
CalendarObjectPropWrapperName::CalendarObject(CalendarObjectPropName::Getetag),
|
CalendarObjectPropWrapperName::CalendarObject(CalendarObjectPropName::Getetag),
|
||||||
CalendarObjectPropWrapperName::CalendarObject(CalendarObjectPropName::CalendarData(
|
CalendarObjectPropWrapperName::CalendarObject(CalendarObjectPropName::CalendarData(
|
||||||
CalendarData { comp: None, expand: Some(ExpandElement {
|
CalendarData { comp: None, prop: None, expand: Some(ExpandElement {
|
||||||
start: <UtcDateTime as ValueDeserialize>::deserialize("20250426T220000Z").unwrap(),
|
start: <UtcDateTime as ValueDeserialize>::deserialize("20250426T220000Z").unwrap(),
|
||||||
end: <UtcDateTime as ValueDeserialize>::deserialize("20250503T220000Z").unwrap(),
|
end: <UtcDateTime as ValueDeserialize>::deserialize("20250503T220000Z").unwrap(),
|
||||||
}), limit_recurrence_set: None, limit_freebusy_set: None }
|
}), limit_recurrence_set: None, limit_freebusy_set: None }
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ impl Default for SupportedCollationSet {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(vec![
|
Self(vec![
|
||||||
SupportedCollation(TextCollation::AsciiCasemap),
|
SupportedCollation(TextCollation::AsciiCasemap),
|
||||||
|
SupportedCollation(TextCollation::UnicodeCasemap),
|
||||||
SupportedCollation(TextCollation::Octet),
|
SupportedCollation(TextCollation::Octet),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use rustical_store::auth::Principal;
|
|||||||
use rustical_xml::{EnumVariants, PropName};
|
use rustical_xml::{EnumVariants, PropName};
|
||||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
|
||||||
#[xml(unit_variants_ident = "CalendarPropName")]
|
#[xml(unit_variants_ident = "CalendarPropName")]
|
||||||
@@ -41,7 +42,7 @@ pub enum CalendarProp {
|
|||||||
SupportedCalendarData(SupportedCalendarData),
|
SupportedCalendarData(SupportedCalendarData),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
|
||||||
SupportedCollationSet(SupportedCollationSet),
|
SupportedCollationSet(SupportedCollationSet),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
MaxResourceSize(i64),
|
MaxResourceSize(i64),
|
||||||
#[xml(skip_deserializing)]
|
#[xml(skip_deserializing)]
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
@@ -72,8 +73,8 @@ pub struct CalendarResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceName for CalendarResource {
|
impl ResourceName for CalendarResource {
|
||||||
fn get_name(&self) -> String {
|
fn get_name(&self) -> Cow<'_, str> {
|
||||||
self.cal.id.clone()
|
Cow::from(&self.cal.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
---
|
||||||
|
source: crates/caldav/src/calendar/tests.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<response xmlns="DAV:">
|
||||||
|
<href>/caldav/principal/user/calendar/</href>
|
||||||
|
<propstat>
|
||||||
|
<prop xmlns="DAV:">
|
||||||
|
<calendar-timezone xmlns="urn:ietf:params:xml:ns:caldav">BEGIN:VCALENDAR
|
||||||
|
PRODID:-//github.com/lennart-k/vzic-rs//RustiCal Calendar server//EN
|
||||||
|
VERSION:2.0
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:Europe/Berlin
|
||||||
|
LAST-MODIFIED:20250723T190331Z
|
||||||
|
X-LIC-LOCATION:Europe/Berlin
|
||||||
|
X-PROLEPTIC-TZNAME:LMT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+005328
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:18930401T000000
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:CEST
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
DTSTART:19160430T230000
|
||||||
|
RDATE:19400401T020000
|
||||||
|
RDATE:19430329T020000
|
||||||
|
RDATE:19460414T020000
|
||||||
|
RDATE:19470406T030000
|
||||||
|
RDATE:19480418T020000
|
||||||
|
RDATE:19490410T020000
|
||||||
|
RDATE:19800406T020000
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:19161001T010000
|
||||||
|
RDATE:19421102T030000
|
||||||
|
RDATE:19431004T030000
|
||||||
|
RDATE:19441002T030000
|
||||||
|
RDATE:19451118T030000
|
||||||
|
RDATE:19461007T030000
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:CEST
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
DTSTART:19170416T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3MO;UNTIL=19180415T010000Z
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:19170917T030000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3MO;UNTIL=19180916T010000Z
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:CEST
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
DTSTART:19440403T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1MO;UNTIL=19450402T010000Z
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:CEMT
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0300
|
||||||
|
DTSTART:19450524T020000
|
||||||
|
RDATE:19470511T030000
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:CEST
|
||||||
|
TZOFFSETFROM:+0300
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
DTSTART:19450924T030000
|
||||||
|
RDATE:19470629T030000
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:19460101T000000
|
||||||
|
RDATE:19800101T000000
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:19471005T030000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19491002T010000Z
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:19800928T030000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19950924T010000Z
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:CEST
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
DTSTART:19810329T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:19961027T030000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
|
END:VCALENDAR
|
||||||
|
</calendar-timezone>
|
||||||
|
<timezone-service-set xmlns="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<href xmlns="DAV:">https://www.iana.org/time-zones</href>
|
||||||
|
</timezone-service-set>
|
||||||
|
<calendar-timezone-id xmlns="urn:ietf:params:xml:ns:caldav">Europe/Berlin</calendar-timezone-id>
|
||||||
|
<calendar-order xmlns="http://apple.com/ns/ical/">0</calendar-order>
|
||||||
|
<supported-calendar-component-set xmlns="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<comp xmlns="urn:ietf:params:xml:ns:caldav" name="VEVENT"/>
|
||||||
|
<comp xmlns="urn:ietf:params:xml:ns:caldav" name="VTODO"/>
|
||||||
|
</supported-calendar-component-set>
|
||||||
|
<supported-calendar-data xmlns="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<calendar-data xmlns="urn:ietf:params:xml:ns:caldav" content-type="text/calendar" version="2.0"/>
|
||||||
|
</supported-calendar-data>
|
||||||
|
<supported-collation-set xmlns="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<supported-collation xmlns="urn:ietf:params:xml:ns:caldav">i;ascii-casemap</supported-collation>
|
||||||
|
<supported-collation xmlns="urn:ietf:params:xml:ns:caldav">i;unicode-casemap</supported-collation>
|
||||||
|
<supported-collation xmlns="urn:ietf:params:xml:ns:caldav">i;octet</supported-collation>
|
||||||
|
</supported-collation-set>
|
||||||
|
<max-resource-size xmlns="urn:ietf:params:xml:ns:caldav">10000000</max-resource-size>
|
||||||
|
<supported-report-set xmlns="DAV:">
|
||||||
|
<supported-report xmlns="DAV:">
|
||||||
|
<report xmlns="DAV:">
|
||||||
|
<calendar-query xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
</report>
|
||||||
|
</supported-report>
|
||||||
|
<supported-report xmlns="DAV:">
|
||||||
|
<report xmlns="DAV:">
|
||||||
|
<calendar-multiget xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
</report>
|
||||||
|
</supported-report>
|
||||||
|
<supported-report xmlns="DAV:">
|
||||||
|
<report xmlns="DAV:">
|
||||||
|
<sync-collection xmlns="DAV:"/>
|
||||||
|
</report>
|
||||||
|
</supported-report>
|
||||||
|
</supported-report-set>
|
||||||
|
<min-date-time xmlns="urn:ietf:params:xml:ns:caldav">-2621430101T000000Z</min-date-time>
|
||||||
|
<max-date-time xmlns="urn:ietf:params:xml:ns:caldav">+2621421231T235959Z</max-date-time>
|
||||||
|
<sync-token xmlns="DAV:">github.com/lennart-k/rustical/ns/12</sync-token>
|
||||||
|
<getctag xmlns="http://calendarserver.org/ns/">github.com/lennart-k/rustical/ns/12</getctag>
|
||||||
|
<transports xmlns="https://bitfire.at/webdav-push">
|
||||||
|
<web-push xmlns="https://bitfire.at/webdav-push"/>
|
||||||
|
</transports>
|
||||||
|
<topic xmlns="https://bitfire.at/webdav-push">b28b41e9-8801-4fc5-ae29-8efb5fadeb36</topic>
|
||||||
|
<supported-triggers xmlns="https://bitfire.at/webdav-push">
|
||||||
|
<content-update xmlns="https://bitfire.at/webdav-push">
|
||||||
|
<depth xmlns="DAV:">1</depth>
|
||||||
|
</content-update>
|
||||||
|
<property-update xmlns="https://bitfire.at/webdav-push">
|
||||||
|
<depth xmlns="DAV:">1</depth>
|
||||||
|
</property-update>
|
||||||
|
</supported-triggers>
|
||||||
|
<resourcetype xmlns="DAV:">
|
||||||
|
<collection xmlns="DAV:"/>
|
||||||
|
<calendar xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
</resourcetype>
|
||||||
|
<displayname xmlns="DAV:">Calendar</displayname>
|
||||||
|
<current-user-principal xmlns="DAV:">
|
||||||
|
<href xmlns="DAV:">/caldav/principal/user/</href>
|
||||||
|
</current-user-principal>
|
||||||
|
<current-user-privilege-set xmlns="DAV:">
|
||||||
|
<privilege>
|
||||||
|
<read/>
|
||||||
|
</privilege>
|
||||||
|
<privilege>
|
||||||
|
<write-properties/>
|
||||||
|
</privilege>
|
||||||
|
<privilege>
|
||||||
|
<read-acl/>
|
||||||
|
</privilege>
|
||||||
|
<privilege>
|
||||||
|
<read-current-user-privilege-set/>
|
||||||
|
</privilege>
|
||||||
|
</current-user-privilege-set>
|
||||||
|
<owner xmlns="DAV:">
|
||||||
|
<href xmlns="DAV:">/caldav/principal/user/</href>
|
||||||
|
</owner>
|
||||||
|
</prop>
|
||||||
|
<status xmlns="DAV:">HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
source: crates/caldav/src/calendar/tests.rs
|
||||||
|
expression: output
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<response xmlns="DAV:">
|
||||||
|
<href>/caldav/principal/user/calendar/</href>
|
||||||
|
<propstat>
|
||||||
|
<prop xmlns="DAV:">
|
||||||
|
<calendar-color xmlns="http://apple.com/ns/ical/"/>
|
||||||
|
<calendar-description xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<calendar-timezone xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<timezone-service-set xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<calendar-timezone-id xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<calendar-order xmlns="http://apple.com/ns/ical/"/>
|
||||||
|
<supported-calendar-component-set xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<supported-calendar-data xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<supported-collation-set xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<max-resource-size xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<supported-report-set xmlns="DAV:"/>
|
||||||
|
<source xmlns="http://calendarserver.org/ns/"/>
|
||||||
|
<min-date-time xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<max-date-time xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||||
|
<sync-token xmlns="DAV:"/>
|
||||||
|
<getctag xmlns="http://calendarserver.org/ns/"/>
|
||||||
|
<transports xmlns="https://bitfire.at/webdav-push"/>
|
||||||
|
<topic xmlns="https://bitfire.at/webdav-push"/>
|
||||||
|
<supported-triggers xmlns="https://bitfire.at/webdav-push"/>
|
||||||
|
<resourcetype xmlns="DAV:"/>
|
||||||
|
<displayname xmlns="DAV:"/>
|
||||||
|
<current-user-principal xmlns="DAV:"/>
|
||||||
|
<current-user-privilege-set xmlns="DAV:"/>
|
||||||
|
<owner xmlns="DAV:"/>
|
||||||
|
</prop>
|
||||||
|
<status xmlns="DAV:">HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
|
||||||
<href>/caldav/principal/user/calendar/</href>
|
|
||||||
<propstat>
|
|
||||||
<prop>
|
|
||||||
<calendar-color xmlns="http://apple.com/ns/ical/"/>
|
|
||||||
<calendar-description xmlns="urn:ietf:params:xml:ns:caldav"/>
|
|
||||||
<calendar-timezone xmlns="urn:ietf:params:xml:ns:caldav"/>
|
|
||||||
<timezone-service-set xmlns="urn:ietf:params:xml:ns:caldav"/>
|
|
||||||
<calendar-timezone-id xmlns="urn:ietf:params:xml:ns:caldav"/>
|
|
||||||
<calendar-order xmlns="http://apple.com/ns/ical/"/>
|
|
||||||
<supported-calendar-component-set xmlns="urn:ietf:params:xml:ns:caldav"/>
|
|
||||||
<supported-calendar-data xmlns="urn:ietf:params:xml:ns:caldav"/>
|
|
||||||
<supported-collation-set xmlns="urn:ietf:params:xml:ns:caldav"/>
|
|
||||||
<max-resource-size xmlns="DAV:"/>
|
|
||||||
<supported-report-set xmlns="DAV:"/>
|
|
||||||
<source xmlns="http://calendarserver.org/ns/"/>
|
|
||||||
<min-date-time xmlns="urn:ietf:params:xml:ns:caldav"/>
|
|
||||||
<max-date-time xmlns="urn:ietf:params:xml:ns:caldav"/>
|
|
||||||
<sync-token xmlns="DAV:"/>
|
|
||||||
<getctag xmlns="http://calendarserver.org/ns/"/>
|
|
||||||
<transports xmlns="https://bitfire.at/webdav-push"/>
|
|
||||||
<topic xmlns="https://bitfire.at/webdav-push"/>
|
|
||||||
<supported-triggers xmlns="https://bitfire.at/webdav-push"/>
|
|
||||||
<resourcetype xmlns="DAV:"/>
|
|
||||||
<displayname xmlns="DAV:"/>
|
|
||||||
<current-user-principal xmlns="DAV:"/>
|
|
||||||
<current-user-privilege-set xmlns="DAV:"/>
|
|
||||||
<owner xmlns="DAV:"/>
|
|
||||||
</prop>
|
|
||||||
<status>HTTP/1.1 200 OK</status>
|
|
||||||
</propstat>
|
|
||||||
</response>
|
|
||||||
|
|
||||||
|
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
|
||||||
<href>/caldav/principal/user/calendar/</href>
|
|
||||||
<propstat>
|
|
||||||
<prop>
|
|
||||||
<CAL:calendar-timezone>BEGIN:VCALENDAR
|
|
||||||
PRODID:-//github.com/lennart-k/vzic-rs//RustiCal Calendar server//EN
|
|
||||||
VERSION:2.0
|
|
||||||
BEGIN:VTIMEZONE
|
|
||||||
TZID:Europe/Berlin
|
|
||||||
LAST-MODIFIED:20250723T190331Z
|
|
||||||
X-LIC-LOCATION:Europe/Berlin
|
|
||||||
X-PROLEPTIC-TZNAME:LMT
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+005328
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:18930401T000000
|
|
||||||
END:STANDARD
|
|
||||||
BEGIN:DAYLIGHT
|
|
||||||
TZNAME:CEST
|
|
||||||
TZOFFSETFROM:+0100
|
|
||||||
TZOFFSETTO:+0200
|
|
||||||
DTSTART:19160430T230000
|
|
||||||
RDATE:19400401T020000
|
|
||||||
RDATE:19430329T020000
|
|
||||||
RDATE:19460414T020000
|
|
||||||
RDATE:19470406T030000
|
|
||||||
RDATE:19480418T020000
|
|
||||||
RDATE:19490410T020000
|
|
||||||
RDATE:19800406T020000
|
|
||||||
END:DAYLIGHT
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+0200
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:19161001T010000
|
|
||||||
RDATE:19421102T030000
|
|
||||||
RDATE:19431004T030000
|
|
||||||
RDATE:19441002T030000
|
|
||||||
RDATE:19451118T030000
|
|
||||||
RDATE:19461007T030000
|
|
||||||
END:STANDARD
|
|
||||||
BEGIN:DAYLIGHT
|
|
||||||
TZNAME:CEST
|
|
||||||
TZOFFSETFROM:+0100
|
|
||||||
TZOFFSETTO:+0200
|
|
||||||
DTSTART:19170416T020000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3MO;UNTIL=19180415T010000Z
|
|
||||||
END:DAYLIGHT
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+0200
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:19170917T030000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3MO;UNTIL=19180916T010000Z
|
|
||||||
END:STANDARD
|
|
||||||
BEGIN:DAYLIGHT
|
|
||||||
TZNAME:CEST
|
|
||||||
TZOFFSETFROM:+0100
|
|
||||||
TZOFFSETTO:+0200
|
|
||||||
DTSTART:19440403T020000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1MO;UNTIL=19450402T010000Z
|
|
||||||
END:DAYLIGHT
|
|
||||||
BEGIN:DAYLIGHT
|
|
||||||
TZNAME:CEMT
|
|
||||||
TZOFFSETFROM:+0200
|
|
||||||
TZOFFSETTO:+0300
|
|
||||||
DTSTART:19450524T020000
|
|
||||||
RDATE:19470511T030000
|
|
||||||
END:DAYLIGHT
|
|
||||||
BEGIN:DAYLIGHT
|
|
||||||
TZNAME:CEST
|
|
||||||
TZOFFSETFROM:+0300
|
|
||||||
TZOFFSETTO:+0200
|
|
||||||
DTSTART:19450924T030000
|
|
||||||
RDATE:19470629T030000
|
|
||||||
END:DAYLIGHT
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+0100
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:19460101T000000
|
|
||||||
RDATE:19800101T000000
|
|
||||||
END:STANDARD
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+0200
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:19471005T030000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19491002T010000Z
|
|
||||||
END:STANDARD
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+0200
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:19800928T030000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19950924T010000Z
|
|
||||||
END:STANDARD
|
|
||||||
BEGIN:DAYLIGHT
|
|
||||||
TZNAME:CEST
|
|
||||||
TZOFFSETFROM:+0100
|
|
||||||
TZOFFSETTO:+0200
|
|
||||||
DTSTART:19810329T020000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
|
||||||
END:DAYLIGHT
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+0200
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:19961027T030000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
|
||||||
END:STANDARD
|
|
||||||
END:VTIMEZONE
|
|
||||||
END:VCALENDAR
|
|
||||||
</CAL:calendar-timezone>
|
|
||||||
<CAL:timezone-service-set>
|
|
||||||
<href>https://www.iana.org/time-zones</href>
|
|
||||||
</CAL:timezone-service-set>
|
|
||||||
<CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
|
|
||||||
<calendar-order xmlns="http://apple.com/ns/ical/">0</calendar-order>
|
|
||||||
<CAL:supported-calendar-component-set>
|
|
||||||
<CAL:comp name="VEVENT"/>
|
|
||||||
<CAL:comp name="VTODO"/>
|
|
||||||
</CAL:supported-calendar-component-set>
|
|
||||||
<CAL:supported-calendar-data>
|
|
||||||
<CAL:calendar-data content-type="text/calendar" version="2.0"/>
|
|
||||||
</CAL:supported-calendar-data>
|
|
||||||
<CAL:supported-collation-set>
|
|
||||||
<CAL:supported-collation>i;ascii-casemap</CAL:supported-collation>
|
|
||||||
<CAL:supported-collation>i;octet</CAL:supported-collation>
|
|
||||||
</CAL:supported-collation-set>
|
|
||||||
<max-resource-size>10000000</max-resource-size>
|
|
||||||
<supported-report-set>
|
|
||||||
<supported-report>
|
|
||||||
<report>
|
|
||||||
<CAL:calendar-query/>
|
|
||||||
</report>
|
|
||||||
</supported-report>
|
|
||||||
<supported-report>
|
|
||||||
<report>
|
|
||||||
<CAL:calendar-multiget/>
|
|
||||||
</report>
|
|
||||||
</supported-report>
|
|
||||||
<supported-report>
|
|
||||||
<report>
|
|
||||||
<sync-collection/>
|
|
||||||
</report>
|
|
||||||
</supported-report>
|
|
||||||
</supported-report-set>
|
|
||||||
<CAL:min-date-time>-2621430101T000000Z</CAL:min-date-time>
|
|
||||||
<CAL:max-date-time>+2621421231T235959Z</CAL:max-date-time>
|
|
||||||
<sync-token>github.com/lennart-k/rustical/ns/12</sync-token>
|
|
||||||
<CS:getctag>github.com/lennart-k/rustical/ns/12</CS:getctag>
|
|
||||||
<PUSH:transports>
|
|
||||||
<PUSH:web-push/>
|
|
||||||
</PUSH:transports>
|
|
||||||
<PUSH:topic>b28b41e9-8801-4fc5-ae29-8efb5fadeb36</PUSH:topic>
|
|
||||||
<PUSH:supported-triggers>
|
|
||||||
<PUSH:content-update>
|
|
||||||
<depth>1</depth>
|
|
||||||
</PUSH:content-update>
|
|
||||||
<PUSH:property-update>
|
|
||||||
<depth>1</depth>
|
|
||||||
</PUSH:property-update>
|
|
||||||
</PUSH:supported-triggers>
|
|
||||||
<resourcetype>
|
|
||||||
<collection/>
|
|
||||||
<CAL:calendar/>
|
|
||||||
</resourcetype>
|
|
||||||
<displayname>Calendar</displayname>
|
|
||||||
<current-user-principal>
|
|
||||||
<href>/caldav/principal/user/</href>
|
|
||||||
</current-user-principal>
|
|
||||||
<current-user-privilege-set>
|
|
||||||
<privilege>
|
|
||||||
<read/>
|
|
||||||
</privilege>
|
|
||||||
<privilege>
|
|
||||||
<write-properties/>
|
|
||||||
</privilege>
|
|
||||||
<privilege>
|
|
||||||
<read-acl/>
|
|
||||||
</privilege>
|
|
||||||
<privilege>
|
|
||||||
<read-current-user-privilege-set/>
|
|
||||||
</privilege>
|
|
||||||
</current-user-privilege-set>
|
|
||||||
<owner>
|
|
||||||
<href>/caldav/principal/user/</href>
|
|
||||||
</owner>
|
|
||||||
</prop>
|
|
||||||
<status>HTTP/1.1 200 OK</status>
|
|
||||||
</propstat>
|
|
||||||
</response>
|
|
||||||
@@ -14,14 +14,9 @@ async fn test_propfind() {
|
|||||||
from_str(include_str!("./test_files/propfind.principals.json")).unwrap();
|
from_str(include_str!("./test_files/propfind.principals.json")).unwrap();
|
||||||
let resources: Vec<CalendarResource> =
|
let resources: Vec<CalendarResource> =
|
||||||
from_str(include_str!("./test_files/propfind.resources.json")).unwrap();
|
from_str(include_str!("./test_files/propfind.resources.json")).unwrap();
|
||||||
let outputs: Vec<_> = include_str!("./test_files/propfind.outputs")
|
|
||||||
.trim()
|
|
||||||
.split("\n\n")
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for principal in principals {
|
for principal in principals {
|
||||||
for ((request, resource), &expected_output) in requests.iter().zip(&resources).zip(&outputs)
|
for (request, resource) in requests.iter().zip(&resources) {
|
||||||
{
|
|
||||||
let propfind = CalendarResource::parse_propfind(request).unwrap();
|
let propfind = CalendarResource::parse_propfind(request).unwrap();
|
||||||
|
|
||||||
let response = resource
|
let response = resource
|
||||||
@@ -33,13 +28,12 @@ async fn test_propfind() {
|
|||||||
&principal,
|
&principal,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let expected_output = expected_output.trim();
|
|
||||||
let output = response
|
let output = response
|
||||||
.serialize_to_string()
|
.serialize_to_string()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.trim()
|
.trim()
|
||||||
.replace("\r\n", "\n");
|
.replace("\r\n", "\n");
|
||||||
similar_asserts::assert_eq!(expected_output, output);
|
insta::assert_snapshot!(output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use axum::extract::{Path, State};
|
|||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum_extra::TypedHeader;
|
use axum_extra::TypedHeader;
|
||||||
use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
||||||
use http::{HeaderMap, Method, StatusCode};
|
use http::{HeaderMap, HeaderValue, Method, StatusCode};
|
||||||
use rustical_ical::CalendarObject;
|
use rustical_ical::CalendarObject;
|
||||||
use rustical_store::CalendarStore;
|
use rustical_store::CalendarStore;
|
||||||
use rustical_store::auth::Principal;
|
use rustical_store::auth::Principal;
|
||||||
@@ -73,7 +73,23 @@ pub async fn put_event<C: CalendarStore>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match {
|
let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match {
|
||||||
if_none_match == IfNoneMatch::any()
|
// TODO: Put into transaction?
|
||||||
|
let existing = match cal_store
|
||||||
|
.get_object(&principal, &calendar_id, &object_id, false)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(existing) => Some(existing),
|
||||||
|
Err(rustical_store::Error::NotFound) => None,
|
||||||
|
Err(err) => Err(err)?,
|
||||||
|
};
|
||||||
|
existing.is_none_or(|existing| {
|
||||||
|
if_none_match.precondition_passes(
|
||||||
|
&existing
|
||||||
|
.get_etag()
|
||||||
|
.parse()
|
||||||
|
.expect("We only generate valid ETags"),
|
||||||
|
)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
};
|
};
|
||||||
@@ -82,9 +98,15 @@ pub async fn put_event<C: CalendarStore>(
|
|||||||
debug!("invalid calendar data:\n{body}");
|
debug!("invalid calendar data:\n{body}");
|
||||||
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
|
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
|
||||||
};
|
};
|
||||||
|
let etag = object.get_etag();
|
||||||
cal_store
|
cal_store
|
||||||
.put_object(principal, calendar_id, object, overwrite)
|
.put_object(principal, calendar_id, object, overwrite)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(StatusCode::CREATED.into_response())
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
"ETag",
|
||||||
|
HeaderValue::from_str(&etag).expect("Contains no invalid characters"),
|
||||||
|
);
|
||||||
|
Ok((StatusCode::CREATED, headers).into_response())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use rustical_dav::extensions::CommonPropertiesProp;
|
use rustical_dav::extensions::CommonPropertiesProp;
|
||||||
use rustical_ical::UtcDateTime;
|
use rustical_ical::UtcDateTime;
|
||||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{EnumVariants, PropName, Unparsed, XmlDeserialize, XmlSerialize};
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
|
||||||
#[xml(unit_variants_ident = "CalendarObjectPropName")]
|
#[xml(unit_variants_ident = "CalendarObjectPropName")]
|
||||||
@@ -35,7 +35,9 @@ pub struct ExpandElement {
|
|||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Default, Eq, Hash)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Default, Eq, Hash)]
|
||||||
pub struct CalendarData {
|
pub struct CalendarData {
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
pub(crate) comp: Option<()>,
|
pub(crate) comp: Option<Unparsed>,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
|
pub(crate) prop: Option<Unparsed>,
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
pub(crate) expand: Option<ExpandElement>,
|
pub(crate) expand: Option<ExpandElement>,
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use super::prop::{
|
use super::prop::{
|
||||||
CalendarData, CalendarObjectProp, CalendarObjectPropName, CalendarObjectPropWrapper,
|
CalendarData, CalendarObjectProp, CalendarObjectPropName, CalendarObjectPropWrapper,
|
||||||
CalendarObjectPropWrapperName,
|
CalendarObjectPropWrapperName,
|
||||||
@@ -20,8 +22,8 @@ pub struct CalendarObjectResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceName for CalendarObjectResource {
|
impl ResourceName for CalendarObjectResource {
|
||||||
fn get_name(&self) -> String {
|
fn get_name(&self) -> Cow<'_, str> {
|
||||||
format!("{}.ics", self.object.get_id())
|
Cow::from(format!("{}.ics", self.object.get_id()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use rustical_dav::xml::{
|
|||||||
GroupMemberSet, GroupMembership, Resourcetype, ResourcetypeInner, SupportedReportSet,
|
GroupMemberSet, GroupMembership, Resourcetype, ResourcetypeInner, SupportedReportSet,
|
||||||
};
|
};
|
||||||
use rustical_store::auth::Principal;
|
use rustical_store::auth::Principal;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
mod service;
|
mod service;
|
||||||
pub use service::*;
|
pub use service::*;
|
||||||
@@ -23,8 +24,8 @@ pub struct PrincipalResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceName for PrincipalResource {
|
impl ResourceName for PrincipalResource {
|
||||||
fn get_name(&self) -> String {
|
fn get_name(&self) -> Cow<'_, str> {
|
||||||
self.principal.id.clone()
|
Cow::from(&self.principal.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,51 +3,51 @@ source: crates/caldav/src/principal/tests.rs
|
|||||||
expression: response.serialize_to_string().unwrap()
|
expression: response.serialize_to_string().unwrap()
|
||||||
---
|
---
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<response xmlns="DAV:">
|
||||||
<href>/caldav/principal/user/</href>
|
<href>/caldav/principal/user/</href>
|
||||||
<propstat>
|
<propstat>
|
||||||
<prop>
|
<prop xmlns="DAV:">
|
||||||
<CAL:calendar-user-type>INDIVIDUAL</CAL:calendar-user-type>
|
<calendar-user-type xmlns="urn:ietf:params:xml:ns:caldav">INDIVIDUAL</calendar-user-type>
|
||||||
<CAL:calendar-user-address-set>
|
<calendar-user-address-set xmlns="urn:ietf:params:xml:ns:caldav">
|
||||||
<href>/caldav/principal/user/</href>
|
<href xmlns="DAV:">/caldav/principal/user/</href>
|
||||||
</CAL:calendar-user-address-set>
|
</calendar-user-address-set>
|
||||||
<principal-URL>
|
<principal-URL xmlns="DAV:">
|
||||||
<href>/caldav/principal/user/</href>
|
<href xmlns="DAV:">/caldav/principal/user/</href>
|
||||||
</principal-URL>
|
</principal-URL>
|
||||||
<group-membership>
|
<group-membership xmlns="DAV:">
|
||||||
<href>/caldav/principal/group/</href>
|
<href xmlns="DAV:">/caldav/principal/group/</href>
|
||||||
</group-membership>
|
</group-membership>
|
||||||
<group-member-set>
|
<group-member-set xmlns="DAV:">
|
||||||
</group-member-set>
|
</group-member-set>
|
||||||
<alternate-URI-set/>
|
<alternate-URI-set xmlns="DAV:"/>
|
||||||
<supported-report-set>
|
<supported-report-set xmlns="DAV:">
|
||||||
<supported-report>
|
<supported-report xmlns="DAV:">
|
||||||
<report>
|
<report xmlns="DAV:">
|
||||||
<principal-match/>
|
<principal-match xmlns="DAV:"/>
|
||||||
</report>
|
</report>
|
||||||
</supported-report>
|
</supported-report>
|
||||||
</supported-report-set>
|
</supported-report-set>
|
||||||
<CAL:calendar-home-set>
|
<calendar-home-set xmlns="urn:ietf:params:xml:ns:caldav">
|
||||||
<href>/caldav/principal/group/</href>
|
<href xmlns="DAV:">/caldav/principal/group/</href>
|
||||||
<href>/caldav/principal/user/</href>
|
<href xmlns="DAV:">/caldav/principal/user/</href>
|
||||||
</CAL:calendar-home-set>
|
</calendar-home-set>
|
||||||
<resourcetype>
|
<resourcetype xmlns="DAV:">
|
||||||
<collection/>
|
<collection xmlns="DAV:"/>
|
||||||
<principal/>
|
<principal xmlns="DAV:"/>
|
||||||
</resourcetype>
|
</resourcetype>
|
||||||
<displayname>user</displayname>
|
<displayname xmlns="DAV:">user</displayname>
|
||||||
<current-user-principal>
|
<current-user-principal xmlns="DAV:">
|
||||||
<href>/caldav/principal/user/</href>
|
<href xmlns="DAV:">/caldav/principal/user/</href>
|
||||||
</current-user-principal>
|
</current-user-principal>
|
||||||
<current-user-privilege-set>
|
<current-user-privilege-set xmlns="DAV:">
|
||||||
<privilege>
|
<privilege>
|
||||||
<all/>
|
<all/>
|
||||||
</privilege>
|
</privilege>
|
||||||
</current-user-privilege-set>
|
</current-user-privilege-set>
|
||||||
<owner>
|
<owner xmlns="DAV:">
|
||||||
<href>/caldav/principal/user/</href>
|
<href xmlns="DAV:">/caldav/principal/user/</href>
|
||||||
</owner>
|
</owner>
|
||||||
</prop>
|
</prop>
|
||||||
<status>HTTP/1.1 200 OK</status>
|
<status xmlns="DAV:">HTTP/1.1 200 OK</status>
|
||||||
</propstat>
|
</propstat>
|
||||||
</response>
|
</response>
|
||||||
|
|||||||
@@ -5,32 +5,27 @@ use crate::{
|
|||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
use rustical_dav::resource::{Resource, ResourceService};
|
use rustical_dav::resource::{Resource, ResourceService};
|
||||||
use rustical_store::auth::{Principal, PrincipalType::Individual};
|
use rustical_store::auth::{Principal, PrincipalType::Individual};
|
||||||
use rustical_store_sqlite::{
|
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||||
SqliteStore,
|
|
||||||
calendar_store::SqliteCalendarStore,
|
|
||||||
principal_store::SqlitePrincipalStore,
|
|
||||||
tests::{get_test_calendar_store, get_test_principal_store, get_test_subscription_store},
|
|
||||||
};
|
|
||||||
use rustical_xml::XmlSerializeRoot;
|
use rustical_xml::XmlSerializeRoot;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_principal_resource(
|
async fn test_principal_resource(
|
||||||
#[from(get_test_calendar_store)]
|
|
||||||
#[future]
|
#[future]
|
||||||
cal_store: SqliteCalendarStore,
|
#[from(test_store_context)]
|
||||||
#[from(get_test_principal_store)]
|
context: TestStoreContext,
|
||||||
#[future]
|
|
||||||
auth_provider: SqlitePrincipalStore,
|
|
||||||
#[from(get_test_subscription_store)]
|
|
||||||
#[future]
|
|
||||||
sub_store: SqliteStore,
|
|
||||||
) {
|
) {
|
||||||
|
let TestStoreContext {
|
||||||
|
cal_store,
|
||||||
|
sub_store,
|
||||||
|
principal_store: auth_provider,
|
||||||
|
..
|
||||||
|
} = context.await;
|
||||||
let service = PrincipalResourceService {
|
let service = PrincipalResourceService {
|
||||||
cal_store: Arc::new(cal_store.await),
|
cal_store: Arc::new(cal_store),
|
||||||
sub_store: Arc::new(sub_store.await),
|
sub_store: Arc::new(sub_store),
|
||||||
auth_provider: Arc::new(auth_provider.await),
|
auth_provider: Arc::new(auth_provider),
|
||||||
simplified_home_set: false,
|
simplified_home_set: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ percent-encoding.workspace = true
|
|||||||
ical.workspace = true
|
ical.workspace = true
|
||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
strum_macros.workspace = true
|
strum_macros.workspace = true
|
||||||
|
rstest.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta.workspace = true
|
insta.workspace = true
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use axum::extract::{Path, State};
|
|||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum_extra::TypedHeader;
|
use axum_extra::TypedHeader;
|
||||||
use axum_extra::headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
use axum_extra::headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
||||||
|
use http::HeaderValue;
|
||||||
use http::Method;
|
use http::Method;
|
||||||
use http::{HeaderMap, StatusCode};
|
use http::{HeaderMap, StatusCode};
|
||||||
use rustical_dav::privileges::UserPrivilege;
|
use rustical_dav::privileges::UserPrivilege;
|
||||||
@@ -81,15 +82,37 @@ pub async fn put_object<AS: AddressbookStore>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match {
|
let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match {
|
||||||
if_none_match == IfNoneMatch::any()
|
// TODO: Put into transaction?
|
||||||
|
let existing = match addr_store
|
||||||
|
.get_object(&principal, &addressbook_id, &object_id, false)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(existing) => Some(existing),
|
||||||
|
Err(rustical_store::Error::NotFound) => None,
|
||||||
|
Err(err) => Err(err)?,
|
||||||
|
};
|
||||||
|
existing.is_none_or(|existing| {
|
||||||
|
if_none_match.precondition_passes(
|
||||||
|
&existing
|
||||||
|
.get_etag()
|
||||||
|
.parse()
|
||||||
|
.expect("We only generate valid ETags"),
|
||||||
|
)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
};
|
};
|
||||||
|
|
||||||
let object = AddressObject::from_vcf(object_id, body)?;
|
let object = AddressObject::from_vcf(object_id, body)?;
|
||||||
|
let etag = object.get_etag();
|
||||||
addr_store
|
addr_store
|
||||||
.put_object(principal, addressbook_id, object, overwrite)
|
.put_object(principal, addressbook_id, object, overwrite)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(StatusCode::CREATED.into_response())
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
"ETag",
|
||||||
|
HeaderValue::from_str(&etag).expect("Contains no invalid characters"),
|
||||||
|
);
|
||||||
|
Ok((StatusCode::CREATED, headers).into_response())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Error,
|
Error,
|
||||||
address_object::{
|
address_object::{
|
||||||
@@ -22,8 +24,8 @@ pub struct AddressObjectResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceName for AddressObjectResource {
|
impl ResourceName for AddressObjectResource {
|
||||||
fn get_name(&self) -> String {
|
fn get_name(&self) -> Cow<'_, str> {
|
||||||
format!("{}.vcf", self.object.get_id())
|
Cow::from(format!("{}.vcf", self.object.get_id()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ use crate::{
|
|||||||
address_object::AddressObjectPropWrapperName,
|
address_object::AddressObjectPropWrapperName,
|
||||||
addressbook::methods::report::addressbook_query::PropFilterElement,
|
addressbook::methods::report::addressbook_query::PropFilterElement,
|
||||||
};
|
};
|
||||||
|
use derive_more::{From, Into};
|
||||||
|
use ical::property::Property;
|
||||||
use rustical_dav::xml::{PropfindType, TextMatchElement};
|
use rustical_dav::xml::{PropfindType, TextMatchElement};
|
||||||
use rustical_ical::{AddressObject, UtcDateTime};
|
use rustical_ical::{AddressObject, UtcDateTime};
|
||||||
use rustical_xml::XmlDeserialize;
|
use rustical_xml::{ValueDeserialize, XmlDeserialize, XmlRootTag};
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -28,18 +30,51 @@ pub struct ParamFilterElement {
|
|||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
impl ParamFilterElement {
|
||||||
#[allow(dead_code)]
|
#[must_use]
|
||||||
|
pub fn match_property(&self, prop: &Property) -> bool {
|
||||||
|
let Some(param) = prop.get_param(&self.name) else {
|
||||||
|
return self.is_not_defined.is_some();
|
||||||
|
};
|
||||||
|
if self.is_not_defined.is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(text_match) = self.text_match.as_ref() else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
text_match.match_text(param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Default, From, Into)]
|
||||||
|
pub struct Allof(pub bool);
|
||||||
|
|
||||||
|
impl ValueDeserialize for Allof {
|
||||||
|
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
||||||
|
Ok(Self(match val {
|
||||||
|
"allof" => true,
|
||||||
|
"anyof" => false,
|
||||||
|
_ => {
|
||||||
|
return Err(rustical_xml::XmlError::InvalidVariant(format!(
|
||||||
|
"Invalid test parameter: {val}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// <!ELEMENT filter (prop-filter*)>
|
// <!ELEMENT filter (prop-filter*)>
|
||||||
// <!ATTLIST filter test (anyof | allof) "anyof">
|
// <!ATTLIST filter test (anyof | allof) "anyof">
|
||||||
// <!-- test value:
|
// <!-- test value:
|
||||||
// anyof logical OR for prop-filter matches
|
// anyof logical OR for prop-filter matches
|
||||||
// allof logical AND for prop-filter matches -->
|
// allof logical AND for prop-filter matches -->
|
||||||
|
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[xml(root = "filter", ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct FilterElement {
|
pub struct FilterElement {
|
||||||
#[xml(ty = "attr")]
|
#[xml(ty = "attr", default = "Default::default")]
|
||||||
pub anyof: Option<String>,
|
pub test: Allof,
|
||||||
#[xml(ty = "attr")]
|
|
||||||
pub allof: Option<String>,
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", flatten)]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", flatten)]
|
||||||
pub(crate) prop_filter: Vec<PropFilterElement>,
|
pub(crate) prop_filter: Vec<PropFilterElement>,
|
||||||
}
|
}
|
||||||
@@ -47,11 +82,12 @@ pub struct FilterElement {
|
|||||||
impl FilterElement {
|
impl FilterElement {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn matches(&self, addr_object: &AddressObject) -> bool {
|
pub fn matches(&self, addr_object: &AddressObject) -> bool {
|
||||||
let allof = match (self.allof.is_some(), self.anyof.is_some()) {
|
if self.prop_filter.is_empty() {
|
||||||
(true, false) => true,
|
// Filter empty
|
||||||
(false, _) => false,
|
return true;
|
||||||
(true, true) => panic!("wat"),
|
}
|
||||||
};
|
|
||||||
|
let Allof(allof) = self.test;
|
||||||
let mut results = self
|
let mut results = self
|
||||||
.prop_filter
|
.prop_filter
|
||||||
.iter()
|
.iter()
|
||||||
@@ -74,4 +110,30 @@ pub struct AddressbookQueryRequest {
|
|||||||
pub prop: PropfindType<AddressObjectPropWrapperName>,
|
pub prop: PropfindType<AddressObjectPropWrapperName>,
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
pub(crate) filter: FilterElement,
|
pub(crate) filter: FilterElement,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
|
pub(crate) limit: Option<LimitElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc5323#section-5.17
|
||||||
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct LimitElement {
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
|
pub nresults: NresultsElement,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u64> for LimitElement {
|
||||||
|
fn from(value: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
nresults: NresultsElement(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LimitElement> for u64 {
|
||||||
|
fn from(value: LimitElement) -> Self {
|
||||||
|
value.nresults.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct NresultsElement(#[xml(ty = "text")] pub u64);
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ pub use prop_filter::{PropFilterElement, PropFilterable};
|
|||||||
use rustical_ical::AddressObject;
|
use rustical_ical::AddressObject;
|
||||||
use rustical_store::AddressbookStore;
|
use rustical_store::AddressbookStore;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
pub async fn get_objects_addressbook_query<AS: AddressbookStore>(
|
pub async fn get_objects_addressbook_query<AS: AddressbookStore>(
|
||||||
addr_query: &AddressbookQueryRequest,
|
addr_query: &AddressbookQueryRequest,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::ParamFilterElement;
|
use super::{Allof, ParamFilterElement};
|
||||||
use ical::{parser::Component, property::Property};
|
use ical::{parser::Component, property::Property};
|
||||||
use rustical_dav::xml::TextMatchElement;
|
use rustical_dav::xml::TextMatchElement;
|
||||||
use rustical_ical::AddressObject;
|
use rustical_ical::AddressObject;
|
||||||
@@ -22,54 +22,56 @@ pub struct PropFilterElement {
|
|||||||
pub(crate) text_match: Vec<TextMatchElement>,
|
pub(crate) text_match: Vec<TextMatchElement>,
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", flatten)]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", flatten)]
|
||||||
pub(crate) param_filter: Vec<ParamFilterElement>,
|
pub(crate) param_filter: Vec<ParamFilterElement>,
|
||||||
|
#[xml(ty = "attr", default = "Default::default")]
|
||||||
|
pub test: Allof,
|
||||||
|
|
||||||
#[xml(ty = "attr")]
|
#[xml(ty = "attr")]
|
||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
|
|
||||||
#[xml(ty = "attr")]
|
|
||||||
pub anyof: Option<String>,
|
|
||||||
#[xml(ty = "attr")]
|
|
||||||
pub allof: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropFilterElement {
|
impl PropFilterElement {
|
||||||
|
#[must_use]
|
||||||
|
pub fn match_property(&self, property: &Property) -> bool {
|
||||||
|
if self.param_filter.is_empty() && self.text_match.is_empty() {
|
||||||
|
// Filter empty
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let Allof(allof) = self.test;
|
||||||
|
let text_matches = self
|
||||||
|
.text_match
|
||||||
|
.iter()
|
||||||
|
.map(|text_match| text_match.match_property(property));
|
||||||
|
|
||||||
|
let param_matches = self
|
||||||
|
.param_filter
|
||||||
|
.iter()
|
||||||
|
.map(|param_filter| param_filter.match_property(property));
|
||||||
|
let mut matches = text_matches.chain(param_matches);
|
||||||
|
|
||||||
|
if allof {
|
||||||
|
matches.all(|a| a)
|
||||||
|
} else {
|
||||||
|
matches.any(|a| a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn match_component(&self, comp: &impl PropFilterable) -> bool {
|
pub fn match_component(&self, comp: &impl PropFilterable) -> bool {
|
||||||
let property = comp.get_property(&self.name);
|
let properties = comp.get_named_properties(&self.name);
|
||||||
let _property = match (self.is_not_defined.is_some(), property) {
|
if self.is_not_defined.is_some() {
|
||||||
// We are the component that's not supposed to be defined
|
return properties.is_empty();
|
||||||
(true, Some(_))
|
}
|
||||||
// We don't match
|
|
||||||
| (false, None) => return false,
|
|
||||||
// We shall not be and indeed we aren't
|
|
||||||
(true, None) => return true,
|
|
||||||
(false, Some(property)) => property
|
|
||||||
};
|
|
||||||
|
|
||||||
let _allof = match (self.allof.is_some(), self.anyof.is_some()) {
|
// The filter matches when one property instance matches
|
||||||
(true, false) => true,
|
properties.iter().any(|prop| self.match_property(prop))
|
||||||
(false, _) => false,
|
|
||||||
(true, true) => panic!("wat"),
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: IMPLEMENT
|
|
||||||
// if let Some(text_match) = &self.text_match
|
|
||||||
// && !text_match.match_property(property)
|
|
||||||
// {
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO: param-filter
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait PropFilterable {
|
pub trait PropFilterable {
|
||||||
fn get_property(&self, name: &str) -> Option<&Property>;
|
fn get_named_properties(&self, name: &str) -> Vec<&Property>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropFilterable for AddressObject {
|
impl PropFilterable for AddressObject {
|
||||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||||
self.get_vcard().get_property(name)
|
self.get_vcard().get_named_properties(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
use super::FilterElement;
|
||||||
|
use rstest::rstest;
|
||||||
|
use rustical_ical::AddressObject;
|
||||||
|
use rustical_xml::XmlDocument;
|
||||||
|
|
||||||
|
const VCF_1: &str = r"BEGIN:VCARD
|
||||||
|
VERSION:4.0
|
||||||
|
FN:Simon Perreault
|
||||||
|
N:Perreault;Simon;;;ing. jr,M.Sc.
|
||||||
|
BDAY:--0203
|
||||||
|
GENDER:M
|
||||||
|
EMAIL;TYPE=work:simon.perreault@viagenie.ca
|
||||||
|
END:VCARD";
|
||||||
|
|
||||||
|
const VCF_2: &str = r"BEGIN:VCARD
|
||||||
|
VERSION:4.0
|
||||||
|
N:Gump;Forrest;;Mr.;
|
||||||
|
FN:Forrest Gump
|
||||||
|
ORG:Bubba Gump Shrimp Co.
|
||||||
|
TITLE:Shrimp Man
|
||||||
|
PHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif
|
||||||
|
TEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212
|
||||||
|
TEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212
|
||||||
|
ADR;TYPE=WORK;PREF=1;LABEL=100 Waters Edge\\nBaytown\\, LA 30314\\nUnited S
|
||||||
|
tates of America:;;100 Waters Edge;Baytown;LA;30314;United States of Ameri
|
||||||
|
ca
|
||||||
|
ADR;TYPE=HOME;LABEL=42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States
|
||||||
|
of America:;;42 Plantation St.;Baytown;LA;30314;United States of America
|
||||||
|
EMAIL:forrestgump@example.com
|
||||||
|
REV:20080424T195243Z
|
||||||
|
x-qq:21588891
|
||||||
|
UID:890a9da4-bb6d-4afb-9f32-b5eff6494a53
|
||||||
|
END:VCARD
|
||||||
|
";
|
||||||
|
|
||||||
|
const FILTER_1: &str = r#"
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<C:filter xmlns:C="urn:ietf:params:xml:ns:carddav">
|
||||||
|
<C:prop-filter name="EMAIL" test="allof">
|
||||||
|
<C:text-match collation="i;ascii-casemap">simon.perreault@viagenie.ca</C:text-match>
|
||||||
|
<C:param-filter name="TYPE">
|
||||||
|
<C:text-match match-type="equals" collation="i;unicode-casemap">WORK</C:text-match>
|
||||||
|
</C:param-filter>
|
||||||
|
</C:prop-filter>
|
||||||
|
</C:filter>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const FILTER_2: &str = r#"
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<C:filter xmlns:C="urn:ietf:params:xml:ns:carddav">
|
||||||
|
<C:prop-filter name="EMAIL" test="anyof">
|
||||||
|
<C:text-match collation="i;ascii-casemap">forrestgump@example.com</C:text-match>
|
||||||
|
<C:param-filter name="TYPE">
|
||||||
|
<C:text-match match-type="equals" collation="i;ascii-casemap">WORK</C:text-match>
|
||||||
|
</C:param-filter>
|
||||||
|
</C:prop-filter>
|
||||||
|
</C:filter>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(VCF_1, FILTER_1, true)]
|
||||||
|
#[case(VCF_2, FILTER_1, false)]
|
||||||
|
#[case(VCF_1, FILTER_2, true)]
|
||||||
|
#[case(VCF_2, FILTER_2, true)]
|
||||||
|
fn test_filter(#[case] vcf: &str, #[case] filter: &str, #[case] matches: bool) {
|
||||||
|
dbg!(vcf);
|
||||||
|
let obj = AddressObject::from_vcf(String::new(), vcf.to_owned()).unwrap();
|
||||||
|
let filter = FilterElement::parse_str(filter).unwrap();
|
||||||
|
assert_eq!(matches, filter.matches(&obj));
|
||||||
|
}
|
||||||
@@ -158,7 +158,9 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
address_object::AddressObjectPropName,
|
address_object::AddressObjectPropName,
|
||||||
addressbook::methods::report::addressbook_query::{FilterElement, PropFilterElement},
|
addressbook::methods::report::addressbook_query::{
|
||||||
|
Allof, FilterElement, LimitElement, NresultsElement, PropFilterElement,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use rustical_dav::xml::{PropElement, sync_collection::SyncLevel};
|
use rustical_dav::xml::{PropElement, sync_collection::SyncLevel};
|
||||||
|
|
||||||
@@ -235,6 +237,9 @@ mod tests {
|
|||||||
<card:filter>
|
<card:filter>
|
||||||
<card:prop-filter name="FN"/>
|
<card:prop-filter name="FN"/>
|
||||||
</card:filter>
|
</card:filter>
|
||||||
|
<card:limit>
|
||||||
|
<card:nresults>100</card:nresults>
|
||||||
|
</card:limit>
|
||||||
</card:addressbook-query>
|
</card:addressbook-query>
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
@@ -250,17 +255,18 @@ mod tests {
|
|||||||
vec![]
|
vec![]
|
||||||
)),
|
)),
|
||||||
filter: FilterElement {
|
filter: FilterElement {
|
||||||
anyof: None,
|
test: Allof::default(),
|
||||||
allof: None,
|
|
||||||
prop_filter: vec![PropFilterElement {
|
prop_filter: vec![PropFilterElement {
|
||||||
name: "FN".to_owned(),
|
name: "FN".to_owned(),
|
||||||
is_not_defined: None,
|
is_not_defined: None,
|
||||||
text_match: vec![],
|
text_match: vec![],
|
||||||
param_filter: vec![],
|
param_filter: vec![],
|
||||||
allof: None,
|
test: Allof::default()
|
||||||
anyof: None
|
}],
|
||||||
}]
|
},
|
||||||
}
|
limit: Some(LimitElement {
|
||||||
|
nresults: NresultsElement(100)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
use derive_more::{From, Into};
|
||||||
use rustical_dav::{
|
use rustical_dav::{
|
||||||
extensions::{CommonPropertiesProp, SyncTokenExtensionProp},
|
extensions::{CommonPropertiesProp, SyncTokenExtensionProp},
|
||||||
xml::SupportedReportSet,
|
xml::{SupportedReportSet, TextCollation},
|
||||||
};
|
};
|
||||||
use rustical_dav_push::DavPushExtensionProp;
|
use rustical_dav_push::DavPushExtensionProp;
|
||||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||||
@@ -14,9 +15,11 @@ pub enum AddressbookProp {
|
|||||||
AddressbookDescription(Option<String>),
|
AddressbookDescription(Option<String>),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", skip_deserializing)]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", skip_deserializing)]
|
||||||
SupportedAddressData(SupportedAddressData),
|
SupportedAddressData(SupportedAddressData),
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", skip_deserializing)]
|
||||||
|
SupportedCollationSet(SupportedCollationSet),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
|
||||||
SupportedReportSet(SupportedReportSet<ReportMethod>),
|
SupportedReportSet(SupportedReportSet<ReportMethod>),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
MaxResourceSize(i64),
|
MaxResourceSize(i64),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +63,29 @@ impl Default for SupportedAddressData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)]
|
||||||
|
pub struct SupportedCollation(#[xml(ty = "text")] pub TextCollation);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq)]
|
||||||
|
pub struct SupportedCollationSet(
|
||||||
|
#[xml(
|
||||||
|
ns = "rustical_dav::namespace::NS_CARDDAV",
|
||||||
|
flatten,
|
||||||
|
rename = "supported-collation"
|
||||||
|
)]
|
||||||
|
pub Vec<SupportedCollation>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Default for SupportedCollationSet {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(vec![
|
||||||
|
SupportedCollation(TextCollation::AsciiCasemap),
|
||||||
|
SupportedCollation(TextCollation::UnicodeCasemap),
|
||||||
|
SupportedCollation(TextCollation::Octet),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, XmlSerialize, PartialEq, Eq, VariantArray)]
|
#[derive(Debug, Clone, XmlSerialize, PartialEq, Eq, VariantArray)]
|
||||||
pub enum ReportMethod {
|
pub enum ReportMethod {
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use super::prop::SupportedAddressData;
|
|||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::addressbook::prop::{
|
use crate::addressbook::prop::{
|
||||||
AddressbookProp, AddressbookPropName, AddressbookPropWrapper, AddressbookPropWrapperName,
|
AddressbookProp, AddressbookPropName, AddressbookPropWrapper, AddressbookPropWrapperName,
|
||||||
|
SupportedCollationSet,
|
||||||
};
|
};
|
||||||
use derive_more::derive::{From, Into};
|
use derive_more::derive::{From, Into};
|
||||||
use rustical_dav::extensions::{CommonPropertiesExtension, SyncTokenExtension};
|
use rustical_dav::extensions::{CommonPropertiesExtension, SyncTokenExtension};
|
||||||
@@ -11,13 +12,14 @@ use rustical_dav::xml::{Resourcetype, ResourcetypeInner, SupportedReportSet};
|
|||||||
use rustical_dav_push::DavPushExtension;
|
use rustical_dav_push::DavPushExtension;
|
||||||
use rustical_store::Addressbook;
|
use rustical_store::Addressbook;
|
||||||
use rustical_store::auth::Principal;
|
use rustical_store::auth::Principal;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
#[derive(Clone, Debug, From, Into)]
|
#[derive(Clone, Debug, From, Into)]
|
||||||
pub struct AddressbookResource(pub(crate) Addressbook);
|
pub struct AddressbookResource(pub(crate) Addressbook);
|
||||||
|
|
||||||
impl ResourceName for AddressbookResource {
|
impl ResourceName for AddressbookResource {
|
||||||
fn get_name(&self) -> String {
|
fn get_name(&self) -> Cow<'_, str> {
|
||||||
self.0.id.clone()
|
Cow::from(&self.0.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +63,9 @@ impl Resource for AddressbookResource {
|
|||||||
AddressbookPropName::MaxResourceSize => {
|
AddressbookPropName::MaxResourceSize => {
|
||||||
AddressbookProp::MaxResourceSize(10_000_000)
|
AddressbookProp::MaxResourceSize(10_000_000)
|
||||||
}
|
}
|
||||||
|
AddressbookPropName::SupportedCollationSet => {
|
||||||
|
AddressbookProp::SupportedCollationSet(SupportedCollationSet::default())
|
||||||
|
}
|
||||||
AddressbookPropName::SupportedReportSet => {
|
AddressbookPropName::SupportedReportSet => {
|
||||||
AddressbookProp::SupportedReportSet(SupportedReportSet::all())
|
AddressbookProp::SupportedReportSet(SupportedReportSet::all())
|
||||||
}
|
}
|
||||||
@@ -93,6 +98,7 @@ impl Resource for AddressbookResource {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
AddressbookProp::MaxResourceSize(_)
|
AddressbookProp::MaxResourceSize(_)
|
||||||
|
| AddressbookProp::SupportedCollationSet(_)
|
||||||
| AddressbookProp::SupportedReportSet(_)
|
| AddressbookProp::SupportedReportSet(_)
|
||||||
| AddressbookProp::SupportedAddressData(_) => {
|
| AddressbookProp::SupportedAddressData(_) => {
|
||||||
Err(rustical_dav::Error::PropReadOnly)
|
Err(rustical_dav::Error::PropReadOnly)
|
||||||
@@ -115,6 +121,7 @@ impl Resource for AddressbookResource {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
AddressbookPropName::MaxResourceSize
|
AddressbookPropName::MaxResourceSize
|
||||||
|
| AddressbookPropName::SupportedCollationSet
|
||||||
| AddressbookPropName::SupportedReportSet
|
| AddressbookPropName::SupportedReportSet
|
||||||
| AddressbookPropName::SupportedAddressData => {
|
| AddressbookPropName::SupportedAddressData => {
|
||||||
Err(rustical_dav::Error::PropReadOnly)
|
Err(rustical_dav::Error::PropReadOnly)
|
||||||
|
|||||||
@@ -31,6 +31,23 @@ ResponseElement {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Addressbook(
|
||||||
|
SupportedCollationSet(
|
||||||
|
SupportedCollationSet(
|
||||||
|
[
|
||||||
|
SupportedCollation(
|
||||||
|
AsciiCasemap,
|
||||||
|
),
|
||||||
|
SupportedCollation(
|
||||||
|
UnicodeCasemap,
|
||||||
|
),
|
||||||
|
SupportedCollation(
|
||||||
|
Octet,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Addressbook(
|
Addressbook(
|
||||||
SupportedReportSet(
|
SupportedReportSet(
|
||||||
SupportedReportSet {
|
SupportedReportSet {
|
||||||
|
|||||||
@@ -3,57 +3,62 @@ source: crates/carddav/src/addressbook/tests.rs
|
|||||||
expression: response.serialize_to_string().unwrap()
|
expression: response.serialize_to_string().unwrap()
|
||||||
---
|
---
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<response xmlns="DAV:">
|
||||||
<href>/carddav/principal/user/yeet/</href>
|
<href>/carddav/principal/user/yeet/</href>
|
||||||
<propstat>
|
<propstat>
|
||||||
<prop>
|
<prop xmlns="DAV:">
|
||||||
<CARD:supported-address-data>
|
<supported-address-data xmlns="urn:ietf:params:xml:ns:carddav">
|
||||||
<CARD:address-data-type content-type="text/vcard" version="3.0"/>
|
<address-data-type xmlns="urn:ietf:params:xml:ns:carddav" content-type="text/vcard" version="3.0"/>
|
||||||
<CARD:address-data-type content-type="text/vcard" version="4.0"/>
|
<address-data-type xmlns="urn:ietf:params:xml:ns:carddav" content-type="text/vcard" version="4.0"/>
|
||||||
</CARD:supported-address-data>
|
</supported-address-data>
|
||||||
<supported-report-set>
|
<supported-collation-set xmlns="urn:ietf:params:xml:ns:carddav">
|
||||||
<supported-report>
|
<supported-collation xmlns="urn:ietf:params:xml:ns:carddav">i;ascii-casemap</supported-collation>
|
||||||
<report>
|
<supported-collation xmlns="urn:ietf:params:xml:ns:carddav">i;unicode-casemap</supported-collation>
|
||||||
<CARD:addressbook-multiget/>
|
<supported-collation xmlns="urn:ietf:params:xml:ns:carddav">i;octet</supported-collation>
|
||||||
|
</supported-collation-set>
|
||||||
|
<supported-report-set xmlns="DAV:">
|
||||||
|
<supported-report xmlns="DAV:">
|
||||||
|
<report xmlns="DAV:">
|
||||||
|
<addressbook-multiget xmlns="urn:ietf:params:xml:ns:carddav"/>
|
||||||
</report>
|
</report>
|
||||||
</supported-report>
|
</supported-report>
|
||||||
<supported-report>
|
<supported-report xmlns="DAV:">
|
||||||
<report>
|
<report xmlns="DAV:">
|
||||||
<sync-collection/>
|
<sync-collection xmlns="DAV:"/>
|
||||||
</report>
|
</report>
|
||||||
</supported-report>
|
</supported-report>
|
||||||
</supported-report-set>
|
</supported-report-set>
|
||||||
<max-resource-size>10000000</max-resource-size>
|
<max-resource-size xmlns="urn:ietf:params:xml:ns:carddav">10000000</max-resource-size>
|
||||||
<sync-token>github.com/lennart-k/rustical/ns/0</sync-token>
|
<sync-token xmlns="DAV:">github.com/lennart-k/rustical/ns/0</sync-token>
|
||||||
<CS:getctag>github.com/lennart-k/rustical/ns/0</CS:getctag>
|
<getctag xmlns="http://calendarserver.org/ns/">github.com/lennart-k/rustical/ns/0</getctag>
|
||||||
<PUSH:transports>
|
<transports xmlns="https://bitfire.at/webdav-push">
|
||||||
<PUSH:web-push/>
|
<web-push xmlns="https://bitfire.at/webdav-push"/>
|
||||||
</PUSH:transports>
|
</transports>
|
||||||
<PUSH:topic>asdasd</PUSH:topic>
|
<topic xmlns="https://bitfire.at/webdav-push">asdasd</topic>
|
||||||
<PUSH:supported-triggers>
|
<supported-triggers xmlns="https://bitfire.at/webdav-push">
|
||||||
<PUSH:content-update>
|
<content-update xmlns="https://bitfire.at/webdav-push">
|
||||||
<depth>1</depth>
|
<depth xmlns="DAV:">1</depth>
|
||||||
</PUSH:content-update>
|
</content-update>
|
||||||
<PUSH:property-update>
|
<property-update xmlns="https://bitfire.at/webdav-push">
|
||||||
<depth>1</depth>
|
<depth xmlns="DAV:">1</depth>
|
||||||
</PUSH:property-update>
|
</property-update>
|
||||||
</PUSH:supported-triggers>
|
</supported-triggers>
|
||||||
<resourcetype>
|
<resourcetype xmlns="DAV:">
|
||||||
<collection/>
|
<collection xmlns="DAV:"/>
|
||||||
<CARD:addressbook/>
|
<addressbook xmlns="urn:ietf:params:xml:ns:carddav"/>
|
||||||
</resourcetype>
|
</resourcetype>
|
||||||
<current-user-principal>
|
<current-user-principal xmlns="DAV:">
|
||||||
<href>/carddav/principal/user/</href>
|
<href xmlns="DAV:">/carddav/principal/user/</href>
|
||||||
</current-user-principal>
|
</current-user-principal>
|
||||||
<current-user-privilege-set>
|
<current-user-privilege-set xmlns="DAV:">
|
||||||
<privilege>
|
<privilege>
|
||||||
<all/>
|
<all/>
|
||||||
</privilege>
|
</privilege>
|
||||||
</current-user-privilege-set>
|
</current-user-privilege-set>
|
||||||
<owner>
|
<owner xmlns="DAV:">
|
||||||
<href>/carddav/principal/user/</href>
|
<href xmlns="DAV:">/carddav/principal/user/</href>
|
||||||
</owner>
|
</owner>
|
||||||
</prop>
|
</prop>
|
||||||
<status>HTTP/1.1 200 OK</status>
|
<status xmlns="DAV:">HTTP/1.1 200 OK</status>
|
||||||
</propstat>
|
</propstat>
|
||||||
</response>
|
</response>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use rustical_dav::extensions::CommonPropertiesExtension;
|
use rustical_dav::extensions::CommonPropertiesExtension;
|
||||||
use rustical_dav::privileges::UserPrivilegeSet;
|
use rustical_dav::privileges::UserPrivilegeSet;
|
||||||
@@ -21,8 +23,8 @@ pub struct PrincipalResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceName for PrincipalResource {
|
impl ResourceName for PrincipalResource {
|
||||||
fn get_name(&self) -> String {
|
fn get_name(&self) -> Cow<'_, str> {
|
||||||
self.principal.id.clone()
|
Cow::from(&self.principal.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,43 +3,43 @@ source: crates/carddav/src/principal/tests.rs
|
|||||||
expression: response.serialize_to_string().unwrap()
|
expression: response.serialize_to_string().unwrap()
|
||||||
---
|
---
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<response xmlns="DAV:">
|
||||||
<href>/carddav/principal/user/</href>
|
<href>/carddav/principal/user/</href>
|
||||||
<propstat>
|
<propstat>
|
||||||
<prop>
|
<prop xmlns="DAV:">
|
||||||
<principal-URL>
|
<principal-URL xmlns="DAV:">
|
||||||
<href>/carddav/principal/user/</href>
|
<href xmlns="DAV:">/carddav/principal/user/</href>
|
||||||
</principal-URL>
|
</principal-URL>
|
||||||
<group-membership>
|
<group-membership xmlns="DAV:">
|
||||||
<href>/carddav/principal/group/</href>
|
<href xmlns="DAV:">/carddav/principal/group/</href>
|
||||||
</group-membership>
|
</group-membership>
|
||||||
<group-member-set>
|
<group-member-set xmlns="DAV:">
|
||||||
</group-member-set>
|
</group-member-set>
|
||||||
<alternate-URI-set/>
|
<alternate-URI-set xmlns="DAV:"/>
|
||||||
<principal-collection-set>
|
<principal-collection-set xmlns="DAV:">
|
||||||
<href>/carddav/principal/</href>
|
<href xmlns="DAV:">/carddav/principal/</href>
|
||||||
</principal-collection-set>
|
</principal-collection-set>
|
||||||
<CARD:addressbook-home-set>
|
<addressbook-home-set xmlns="urn:ietf:params:xml:ns:carddav">
|
||||||
<href>/carddav/principal/group/</href>
|
<href xmlns="DAV:">/carddav/principal/group/</href>
|
||||||
<href>/carddav/principal/user/</href>
|
<href xmlns="DAV:">/carddav/principal/user/</href>
|
||||||
</CARD:addressbook-home-set>
|
</addressbook-home-set>
|
||||||
<resourcetype>
|
<resourcetype xmlns="DAV:">
|
||||||
<collection/>
|
<collection xmlns="DAV:"/>
|
||||||
<principal/>
|
<principal xmlns="DAV:"/>
|
||||||
</resourcetype>
|
</resourcetype>
|
||||||
<displayname>user</displayname>
|
<displayname xmlns="DAV:">user</displayname>
|
||||||
<current-user-principal>
|
<current-user-principal xmlns="DAV:">
|
||||||
<href>/carddav/principal/user/</href>
|
<href xmlns="DAV:">/carddav/principal/user/</href>
|
||||||
</current-user-principal>
|
</current-user-principal>
|
||||||
<current-user-privilege-set>
|
<current-user-privilege-set xmlns="DAV:">
|
||||||
<privilege>
|
<privilege>
|
||||||
<all/>
|
<all/>
|
||||||
</privilege>
|
</privilege>
|
||||||
</current-user-privilege-set>
|
</current-user-privilege-set>
|
||||||
<owner>
|
<owner xmlns="DAV:">
|
||||||
<href>/carddav/principal/user/</href>
|
<href xmlns="DAV:">/carddav/principal/user/</href>
|
||||||
</owner>
|
</owner>
|
||||||
</prop>
|
</prop>
|
||||||
<status>HTTP/1.1 200 OK</status>
|
<status xmlns="DAV:">HTTP/1.1 200 OK</status>
|
||||||
</propstat>
|
</propstat>
|
||||||
</response>
|
</response>
|
||||||
|
|||||||
@@ -52,12 +52,11 @@ pub async fn route_delete<R: ResourceService>(
|
|||||||
return Err(Error::Unauthorized.into());
|
return Err(Error::Unauthorized.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(if_match) = if_match {
|
if let Some(if_match) = if_match
|
||||||
dbg!(&if_match);
|
&& !resource.satisfies_if_match(&if_match)
|
||||||
if !resource.satisfies_if_match(&if_match) {
|
{
|
||||||
// Precondition failed
|
// Precondition failed
|
||||||
return Err(crate::Error::PreconditionFailed.into());
|
return Err(crate::Error::PreconditionFailed.into());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if let Some(if_none_match) = if_none_match
|
if let Some(if_none_match) = if_none_match
|
||||||
&& resource.satisfies_if_none_match(&if_none_match)
|
&& resource.satisfies_if_none_match(&if_none_match)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub use resource_service::ResourceService;
|
|||||||
use rustical_xml::{
|
use rustical_xml::{
|
||||||
EnumVariants, NamespaceOwned, PropName, XmlDeserialize, XmlDocument, XmlSerialize,
|
EnumVariants, NamespaceOwned, PropName, XmlDeserialize, XmlDocument, XmlSerialize,
|
||||||
};
|
};
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
mod axum_methods;
|
mod axum_methods;
|
||||||
@@ -30,7 +31,7 @@ pub trait ResourcePropName: FromStr {}
|
|||||||
impl<T: FromStr> ResourcePropName for T {}
|
impl<T: FromStr> ResourcePropName for T {}
|
||||||
|
|
||||||
pub trait ResourceName {
|
pub trait ResourceName {
|
||||||
fn get_name(&self) -> String;
|
fn get_name(&self) -> Cow<'_, str>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Resource: Clone + Send + 'static {
|
pub trait Resource: Clone + Send + 'static {
|
||||||
|
|||||||
@@ -42,13 +42,6 @@ pub enum PropstatWrapper<T: XmlSerialize> {
|
|||||||
// responsedescription?) >
|
// responsedescription?) >
|
||||||
#[derive(XmlSerialize, XmlRootTag, Debug)]
|
#[derive(XmlSerialize, XmlRootTag, Debug)]
|
||||||
#[xml(ns = "crate::namespace::NS_DAV", root = "response")]
|
#[xml(ns = "crate::namespace::NS_DAV", root = "response")]
|
||||||
#[xml(ns_prefix(
|
|
||||||
crate::namespace::NS_DAV = "",
|
|
||||||
crate::namespace::NS_CARDDAV = "CARD",
|
|
||||||
crate::namespace::NS_CALDAV = "CAL",
|
|
||||||
crate::namespace::NS_CALENDARSERVER = "CS",
|
|
||||||
crate::namespace::NS_DAVPUSH = "PUSH"
|
|
||||||
))]
|
|
||||||
pub struct ResponseElement<PropstatType: XmlSerialize> {
|
pub struct ResponseElement<PropstatType: XmlSerialize> {
|
||||||
pub href: String,
|
pub href: String,
|
||||||
#[xml(serialize_with = "xml_serialize_optional_status")]
|
#[xml(serialize_with = "xml_serialize_optional_status")]
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
use ical::property::Property;
|
use ical::property::Property;
|
||||||
use rustical_xml::{ValueDeserialize, XmlDeserialize};
|
use rustical_xml::{ValueDeserialize, XmlDeserialize};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||||
pub enum TextCollation {
|
pub enum TextCollation {
|
||||||
#[default]
|
#[default]
|
||||||
AsciiCasemap,
|
AsciiCasemap,
|
||||||
|
UnicodeCasemap,
|
||||||
Octet,
|
Octet,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextCollation {
|
impl TextCollation {
|
||||||
// Check whether a haystack contains a needle respecting the collation
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn match_text(&self, needle: &str, haystack: &str) -> bool {
|
pub fn normalise<'a>(&self, value: &'a str) -> Cow<'a, str> {
|
||||||
match self {
|
match self {
|
||||||
// https://datatracker.ietf.org/doc/html/rfc4790#section-9.2
|
// https://datatracker.ietf.org/doc/html/rfc4790#section-9.2
|
||||||
Self::AsciiCasemap => haystack
|
Self::AsciiCasemap => Cow::from(value.to_ascii_uppercase()),
|
||||||
.to_ascii_uppercase()
|
Self::UnicodeCasemap => Cow::from(value.to_uppercase()),
|
||||||
.contains(&needle.to_ascii_uppercase()),
|
Self::Octet => Cow::from(value),
|
||||||
Self::Octet => haystack.contains(needle),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,7 @@ impl AsRef<str> for TextCollation {
|
|||||||
fn as_ref(&self) -> &str {
|
fn as_ref(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Self::AsciiCasemap => "i;ascii-casemap",
|
Self::AsciiCasemap => "i;ascii-casemap",
|
||||||
|
Self::UnicodeCasemap => "i;unicode-casemap",
|
||||||
Self::Octet => "i;octet",
|
Self::Octet => "i;octet",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,6 +36,7 @@ impl ValueDeserialize for TextCollation {
|
|||||||
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
||||||
match val {
|
match val {
|
||||||
"i;ascii-casemap" => Ok(Self::AsciiCasemap),
|
"i;ascii-casemap" => Ok(Self::AsciiCasemap),
|
||||||
|
"i;unicode-casemap" => Ok(Self::UnicodeCasemap),
|
||||||
"i;octet" => Ok(Self::Octet),
|
"i;octet" => Ok(Self::Octet),
|
||||||
_ => Err(rustical_xml::XmlError::InvalidVariant(format!(
|
_ => Err(rustical_xml::XmlError::InvalidVariant(format!(
|
||||||
"Invalid collation: {val}"
|
"Invalid collation: {val}"
|
||||||
@@ -58,6 +60,46 @@ impl ValueDeserialize for NegateCondition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||||
|
pub enum MatchType {
|
||||||
|
Equals,
|
||||||
|
#[default]
|
||||||
|
Contains,
|
||||||
|
StartsWith,
|
||||||
|
EndsWith,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MatchType {
|
||||||
|
#[must_use]
|
||||||
|
pub fn match_text(&self, collation: &TextCollation, needle: &str, haystack: &str) -> bool {
|
||||||
|
let haystack = collation.normalise(haystack);
|
||||||
|
let needle = collation.normalise(needle);
|
||||||
|
|
||||||
|
match &self {
|
||||||
|
Self::Equals => haystack == needle,
|
||||||
|
Self::Contains => haystack.contains(needle.as_ref()),
|
||||||
|
Self::StartsWith => haystack.starts_with(needle.as_ref()),
|
||||||
|
Self::EndsWith => haystack.ends_with(needle.as_ref()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueDeserialize for MatchType {
|
||||||
|
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
|
||||||
|
Ok(match val {
|
||||||
|
"equals" => Self::Equals,
|
||||||
|
"contains" => Self::Contains,
|
||||||
|
"starts-with" => Self::StartsWith,
|
||||||
|
"ends-with" => Self::EndsWith,
|
||||||
|
_ => {
|
||||||
|
return Err(rustical_xml::XmlError::InvalidVariant(format!(
|
||||||
|
"Invalid match-type parameter: {val}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct TextMatchElement {
|
pub struct TextMatchElement {
|
||||||
@@ -65,39 +107,48 @@ pub struct TextMatchElement {
|
|||||||
pub collation: TextCollation,
|
pub collation: TextCollation,
|
||||||
#[xml(ty = "attr", default = "Default::default")]
|
#[xml(ty = "attr", default = "Default::default")]
|
||||||
pub negate_condition: NegateCondition,
|
pub negate_condition: NegateCondition,
|
||||||
|
#[xml(ty = "attr", default = "Default::default")]
|
||||||
|
pub match_type: MatchType,
|
||||||
#[xml(ty = "text")]
|
#[xml(ty = "text")]
|
||||||
pub needle: String,
|
pub needle: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextMatchElement {
|
impl TextMatchElement {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn match_property(&self, property: &Property) -> bool {
|
pub fn match_text(&self, haystack: &str) -> bool {
|
||||||
let Self {
|
let Self {
|
||||||
collation,
|
collation,
|
||||||
negate_condition,
|
negate_condition,
|
||||||
needle,
|
needle,
|
||||||
|
match_type,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let matches = property
|
let matches = match_type.match_text(collation, needle, haystack);
|
||||||
.value
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|haystack| collation.match_text(needle, haystack));
|
|
||||||
|
|
||||||
// XOR
|
// XOR
|
||||||
negate_condition.0 ^ matches
|
negate_condition.0 ^ matches
|
||||||
}
|
}
|
||||||
|
#[must_use]
|
||||||
|
pub fn match_property(&self, property: &Property) -> bool {
|
||||||
|
let text = property.value.as_deref().unwrap_or("");
|
||||||
|
self.match_text(text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::TextCollation;
|
use super::TextCollation;
|
||||||
|
use crate::xml::MatchType;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_collation() {
|
fn test_collation() {
|
||||||
assert!(TextCollation::AsciiCasemap.match_text("GrüN", "grün"));
|
assert!(!MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrÜN", "grünsd"));
|
||||||
assert!(!TextCollation::AsciiCasemap.match_text("GrÜN", "grün"));
|
assert!(MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrüN", "grün"));
|
||||||
assert!(!TextCollation::Octet.match_text("GrÜN", "grün"));
|
assert!(!MatchType::Contains.match_text(&TextCollation::Octet, "GrüN", "grün"));
|
||||||
assert!(TextCollation::Octet.match_text("hallo", "hallo"));
|
assert!(MatchType::Contains.match_text(&TextCollation::UnicodeCasemap, "GrÜN", "grün"));
|
||||||
assert!(TextCollation::AsciiCasemap.match_text("HaLlo", "hAllo"));
|
assert!(MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrüN", "grün"));
|
||||||
|
assert!(MatchType::Contains.match_text(&TextCollation::AsciiCasemap, "GrüN", "grün"));
|
||||||
|
assert!(MatchType::StartsWith.match_text(&TextCollation::Octet, "hello", "hello you"));
|
||||||
|
assert!(MatchType::EndsWith.match_text(&TextCollation::Octet, "mama", "joe mama"));
|
||||||
|
assert!(MatchType::Equals.match_text(&TextCollation::UnicodeCasemap, "GrÜN", "grün"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ use axum::{
|
|||||||
extract::Path,
|
extract::Path,
|
||||||
response::{Html, IntoResponse, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::{TypedHeader, extract::Host};
|
use axum_extra::TypedHeader;
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use headers::UserAgent;
|
use headers::{Host, UserAgent};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use rustical_store::auth::{AuthenticationProvider, Principal};
|
use rustical_store::auth::{AuthenticationProvider, Principal};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -21,7 +21,7 @@ use tracing::instrument;
|
|||||||
pub async fn post_nextcloud_login(
|
pub async fn post_nextcloud_login(
|
||||||
Extension(state): Extension<Arc<NextcloudFlows>>,
|
Extension(state): Extension<Arc<NextcloudFlows>>,
|
||||||
TypedHeader(user_agent): TypedHeader<UserAgent>,
|
TypedHeader(user_agent): TypedHeader<UserAgent>,
|
||||||
Host(host): Host,
|
TypedHeader(host): TypedHeader<Host>,
|
||||||
) -> Json<NextcloudLoginResponse> {
|
) -> Json<NextcloudLoginResponse> {
|
||||||
let flow_id = uuid::Uuid::new_v4().to_string();
|
let flow_id = uuid::Uuid::new_v4().to_string();
|
||||||
let token = uuid::Uuid::new_v4().to_string();
|
let token = uuid::Uuid::new_v4().to_string();
|
||||||
@@ -150,7 +150,7 @@ pub async fn post_nextcloud_flow(
|
|||||||
user: Principal,
|
user: Principal,
|
||||||
Extension(state): Extension<Arc<NextcloudFlows>>,
|
Extension(state): Extension<Arc<NextcloudFlows>>,
|
||||||
Path(flow_id): Path<String>,
|
Path(flow_id): Path<String>,
|
||||||
Host(host): Host,
|
TypedHeader(host): TypedHeader<Host>,
|
||||||
Form(form): Form<NextcloudAuthorizeForm>,
|
Form(form): Form<NextcloudAuthorizeForm>,
|
||||||
) -> Result<Response, rustical_store::Error> {
|
) -> Result<Response, rustical_store::Error> {
|
||||||
if let Some(flow) = state.flows.write().await.get_mut(&flow_id) {
|
if let Some(flow) = state.flows.write().await.get_mut(&flow_id) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use axum::{
|
|||||||
extract::Path,
|
extract::Path,
|
||||||
response::{IntoResponse, Redirect, Response},
|
response::{IntoResponse, Redirect, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::Host;
|
use axum_extra::TypedHeader;
|
||||||
use headers::{ContentType, HeaderMapExt};
|
use headers::{ContentType, HeaderMapExt, Host};
|
||||||
use http::{HeaderValue, StatusCode, header};
|
use http::{HeaderValue, StatusCode, header};
|
||||||
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||||
use rand::{Rng, distr::Alphanumeric};
|
use rand::{Rng, distr::Alphanumeric};
|
||||||
@@ -50,7 +50,7 @@ pub async fn route_post_app_token<AP: AuthenticationProvider>(
|
|||||||
user: Principal,
|
user: Principal,
|
||||||
Extension(auth_provider): Extension<Arc<AP>>,
|
Extension(auth_provider): Extension<Arc<AP>>,
|
||||||
Path(user_id): Path<String>,
|
Path(user_id): Path<String>,
|
||||||
Host(hostname): Host,
|
TypedHeader(host): TypedHeader<Host>,
|
||||||
Form(PostAppTokenForm { apple, name }): Form<PostAppTokenForm>,
|
Form(PostAppTokenForm { apple, name }): Form<PostAppTokenForm>,
|
||||||
) -> Result<Response, rustical_store::Error> {
|
) -> Result<Response, rustical_store::Error> {
|
||||||
assert!(!name.is_empty());
|
assert!(!name.is_empty());
|
||||||
@@ -66,10 +66,10 @@ pub async fn route_post_app_token<AP: AuthenticationProvider>(
|
|||||||
if apple {
|
if apple {
|
||||||
let profile = AppleConfig {
|
let profile = AppleConfig {
|
||||||
token_name: name,
|
token_name: name,
|
||||||
account_description: format!("{}@{}", &user.id, &hostname),
|
account_description: format!("{}@{}", &user.id, &host),
|
||||||
hostname: hostname.clone(),
|
hostname: host.to_string(),
|
||||||
caldav_principal_url: format!("https://{hostname}/caldav-compat/principal/{user_id}"),
|
caldav_principal_url: format!("https://{host}/caldav-compat/principal/{user_id}"),
|
||||||
carddav_principal_url: format!("https://{hostname}/carddav/principal/{user_id}"),
|
carddav_principal_url: format!("https://{host}/carddav/principal/{user_id}"),
|
||||||
user: user.id.clone(),
|
user: user.id.clone(),
|
||||||
token,
|
token,
|
||||||
caldav_profile_uuid: Uuid::new_v4(),
|
caldav_profile_uuid: Uuid::new_v4(),
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::{FrontendConfig, OidcConfig, pages::DefaultLayoutData};
|
use crate::{FrontendConfig, OidcConfig, pages::DefaultLayoutData};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use askama_web::WebTemplate;
|
use askama_web::WebTemplate;
|
||||||
@@ -8,10 +6,12 @@ use axum::{
|
|||||||
extract::Query,
|
extract::Query,
|
||||||
response::{IntoResponse, Redirect, Response},
|
response::{IntoResponse, Redirect, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::Host;
|
use axum_extra::TypedHeader;
|
||||||
|
use headers::Host;
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use rustical_store::auth::AuthenticationProvider;
|
use rustical_store::auth::AuthenticationProvider;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::sync::Arc;
|
||||||
use tower_sessions::Session;
|
use tower_sessions::Session;
|
||||||
use tracing::{instrument, warn};
|
use tracing::{instrument, warn};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@@ -73,7 +73,7 @@ pub async fn route_post_login<AP: AuthenticationProvider>(
|
|||||||
Extension(auth_provider): Extension<Arc<AP>>,
|
Extension(auth_provider): Extension<Arc<AP>>,
|
||||||
Extension(config): Extension<FrontendConfig>,
|
Extension(config): Extension<FrontendConfig>,
|
||||||
session: Session,
|
session: Session,
|
||||||
Host(host): Host,
|
TypedHeader(host): TypedHeader<Host>,
|
||||||
Form(PostLoginForm {
|
Form(PostLoginForm {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::pages::user::{Section, UserPage};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use askama_web::WebTemplate;
|
use askama_web::WebTemplate;
|
||||||
use axum::{
|
use axum::{
|
||||||
@@ -7,13 +8,11 @@ use axum::{
|
|||||||
extract::Path,
|
extract::Path,
|
||||||
response::{IntoResponse, Redirect},
|
response::{IntoResponse, Redirect},
|
||||||
};
|
};
|
||||||
use axum_extra::{TypedHeader, extract::Host};
|
use axum_extra::TypedHeader;
|
||||||
use headers::UserAgent;
|
use headers::{Host, UserAgent};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use rustical_store::auth::{AppToken, AuthenticationProvider, Principal};
|
use rustical_store::auth::{AppToken, AuthenticationProvider, Principal};
|
||||||
|
|
||||||
use crate::pages::user::{Section, UserPage};
|
|
||||||
|
|
||||||
impl Section for ProfileSection {
|
impl Section for ProfileSection {
|
||||||
fn name() -> &'static str {
|
fn name() -> &'static str {
|
||||||
"profile"
|
"profile"
|
||||||
@@ -33,7 +32,7 @@ pub async fn route_user_named<AP: AuthenticationProvider>(
|
|||||||
Path(user_id): Path<String>,
|
Path(user_id): Path<String>,
|
||||||
Extension(auth_provider): Extension<Arc<AP>>,
|
Extension(auth_provider): Extension<Arc<AP>>,
|
||||||
TypedHeader(user_agent): TypedHeader<UserAgent>,
|
TypedHeader(user_agent): TypedHeader<UserAgent>,
|
||||||
Host(host): Host,
|
TypedHeader(host): TypedHeader<Host>,
|
||||||
user: Principal,
|
user: Principal,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if user_id != user.id {
|
if user_id != user.id {
|
||||||
@@ -41,7 +40,10 @@ pub async fn route_user_named<AP: AuthenticationProvider>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let is_apple = user_agent.as_str().contains("Apple") || user_agent.as_str().contains("Mac OS");
|
let is_apple = user_agent.as_str().contains("Apple") || user_agent.as_str().contains("Mac OS");
|
||||||
let davx5_hostname = user_agent.as_str().contains("Android").then_some(host);
|
let davx5_hostname = user_agent
|
||||||
|
.as_str()
|
||||||
|
.contains("Android")
|
||||||
|
.then_some(host.to_string());
|
||||||
|
|
||||||
UserPage {
|
UserPage {
|
||||||
section: ProfileSection {
|
section: ProfileSection {
|
||||||
|
|||||||
@@ -21,3 +21,5 @@ rrule.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
|
rstest.workspace = true
|
||||||
|
similar-asserts.workspace = true
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ impl EventObject {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut rrule_set = RRuleSet::new(dtstart);
|
let mut rrule_set = RRuleSet::new(dtstart);
|
||||||
|
// TODO: Make nice, this is just a bodge to get correct behaviour
|
||||||
|
let mut empty = true;
|
||||||
|
|
||||||
for prop in &self.event.properties {
|
for prop in &self.event.properties {
|
||||||
rrule_set = match prop.name.as_str() {
|
rrule_set = match prop.name.as_str() {
|
||||||
@@ -76,49 +78,63 @@ impl EventObject {
|
|||||||
})?)?
|
})?)?
|
||||||
.validate(dtstart)
|
.validate(dtstart)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
empty = false;
|
||||||
rrule_set.rrule(rrule)
|
rrule_set.rrule(rrule)
|
||||||
}
|
}
|
||||||
"RDATE" => {
|
"RDATE" => {
|
||||||
let rdate = CalDateTime::parse_prop(prop, &self.timezones)?.into();
|
let rdate = CalDateTime::parse_prop(prop, &self.timezones)?.into();
|
||||||
|
empty = false;
|
||||||
rrule_set.rdate(rdate)
|
rrule_set.rdate(rdate)
|
||||||
}
|
}
|
||||||
"EXDATE" => {
|
"EXDATE" => {
|
||||||
let exdate = CalDateTime::parse_prop(prop, &self.timezones)?.into();
|
let exdate = CalDateTime::parse_prop(prop, &self.timezones)?.into();
|
||||||
|
empty = false;
|
||||||
rrule_set.exdate(exdate)
|
rrule_set.exdate(exdate)
|
||||||
}
|
}
|
||||||
_ => rrule_set,
|
_ => rrule_set,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if empty {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Some(rrule_set))
|
Ok(Some(rrule_set))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
pub fn expand_recurrence(
|
pub fn expand_recurrence(
|
||||||
&self,
|
&self,
|
||||||
start: Option<DateTime<Utc>>,
|
start: Option<DateTime<Utc>>,
|
||||||
end: Option<DateTime<Utc>>,
|
end: Option<DateTime<Utc>>,
|
||||||
overrides: &[Self],
|
overrides: &[Self],
|
||||||
) -> Result<Vec<IcalEvent>, Error> {
|
) -> Result<Vec<IcalEvent>, Error> {
|
||||||
let Some(mut rrule_set) = self.recurrence_ruleset()? else {
|
let mut events = vec![];
|
||||||
return Ok(vec![self.event.clone()]);
|
let dtstart = self.get_dtstart()?.expect("We must have a DTSTART here");
|
||||||
};
|
let computed_duration = self
|
||||||
|
.get_dtend()?
|
||||||
|
.map(|dtend| dtend.as_datetime().into_owned() - dtstart.as_datetime().as_ref());
|
||||||
|
|
||||||
|
let Some(mut rrule_set) = self.recurrence_ruleset()? else {
|
||||||
|
// If ruleset empty simply return main event AND all overrides
|
||||||
|
return Ok(std::iter::once(self.clone())
|
||||||
|
.chain(overrides.iter().cloned())
|
||||||
|
.map(|event| event.event)
|
||||||
|
.collect());
|
||||||
|
};
|
||||||
if let Some(start) = start {
|
if let Some(start) = start {
|
||||||
rrule_set = rrule_set.after(start.with_timezone(&rrule::Tz::UTC));
|
rrule_set = rrule_set.after(start.with_timezone(&rrule::Tz::UTC));
|
||||||
}
|
}
|
||||||
if let Some(end) = end {
|
if let Some(end) = end {
|
||||||
rrule_set = rrule_set.before(end.with_timezone(&rrule::Tz::UTC));
|
rrule_set = rrule_set.before(end.with_timezone(&rrule::Tz::UTC));
|
||||||
}
|
}
|
||||||
let mut events = vec![];
|
|
||||||
let dates = rrule_set.all(2048).dates;
|
let dates = rrule_set.all(2048).dates;
|
||||||
let dtstart = self.get_dtstart()?.expect("We must have a DTSTART here");
|
|
||||||
let computed_duration = self
|
|
||||||
.get_dtend()?
|
|
||||||
.map(|dtend| dtend.as_datetime().into_owned() - dtstart.as_datetime().as_ref());
|
|
||||||
|
|
||||||
'recurrence: for date in dates {
|
'recurrence: for date in dates {
|
||||||
let date = CalDateTime::from(date);
|
let date = CalDateTime::from(date.to_utc());
|
||||||
let dateformat = if dtstart.is_date() {
|
let recurrence_id = if dtstart.is_date() {
|
||||||
date.format_date()
|
date.format_date()
|
||||||
} else {
|
} else {
|
||||||
date.format()
|
date.format()
|
||||||
@@ -131,7 +147,7 @@ impl EventObject {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("overrides have a recurrence id")
|
.expect("overrides have a recurrence id")
|
||||||
.value
|
.value
|
||||||
&& override_id == &dateformat
|
&& override_id == &recurrence_id
|
||||||
{
|
{
|
||||||
// We have an override for this occurence
|
// We have an override for this occurence
|
||||||
//
|
//
|
||||||
@@ -154,13 +170,13 @@ impl EventObject {
|
|||||||
|
|
||||||
ev.set_property(Property {
|
ev.set_property(Property {
|
||||||
name: "RECURRENCE-ID".to_string(),
|
name: "RECURRENCE-ID".to_string(),
|
||||||
value: Some(dateformat.clone()),
|
value: Some(recurrence_id.clone()),
|
||||||
params: vec![],
|
params: vec![],
|
||||||
});
|
});
|
||||||
ev.set_property(Property {
|
ev.set_property(Property {
|
||||||
name: "DTSTART".to_string(),
|
name: "DTSTART".to_string(),
|
||||||
value: Some(dateformat),
|
value: Some(recurrence_id),
|
||||||
params: dtstart_prop.params.clone(),
|
params: vec![],
|
||||||
});
|
});
|
||||||
if let Some(duration) = computed_duration {
|
if let Some(duration) = computed_duration {
|
||||||
let dtend = date + duration;
|
let dtend = date + duration;
|
||||||
@@ -183,10 +199,12 @@ impl EventObject {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::CalendarObject;
|
use crate::{CalDateTime, CalendarObject};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use ical::generator::Emitter;
|
use ical::generator::Emitter;
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
const ICS: &str = r"BEGIN:VCALENDAR
|
const ICS_1: &str = r"BEGIN:VCALENDAR
|
||||||
CALSCALE:GREGORIAN
|
CALSCALE:GREGORIAN
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
BEGIN:VTIMEZONE
|
BEGIN:VTIMEZONE
|
||||||
@@ -206,16 +224,16 @@ RRULE:FREQ=WEEKLY;COUNT=4;INTERVAL=2;BYDAY=TU,TH,SU
|
|||||||
END:VEVENT
|
END:VEVENT
|
||||||
END:VCALENDAR";
|
END:VCALENDAR";
|
||||||
|
|
||||||
const EXPANDED: [&str; 4] = [
|
const EXPANDED_1: &[&str] = &[
|
||||||
"BEGIN:VEVENT\r
|
"BEGIN:VEVENT\r
|
||||||
UID:318ec6503573d9576818daf93dac07317058d95c\r
|
UID:318ec6503573d9576818daf93dac07317058d95c\r
|
||||||
DTSTAMP:20250502T132758Z\r
|
DTSTAMP:20250502T132758Z\r
|
||||||
SEQUENCE:2\r
|
SEQUENCE:2\r
|
||||||
SUMMARY:weekly stuff\r
|
SUMMARY:weekly stuff\r
|
||||||
TRANSP:OPAQUE\r
|
TRANSP:OPAQUE\r
|
||||||
RECURRENCE-ID:20250506T090000\r
|
RECURRENCE-ID:20250506T070000Z\r
|
||||||
DTSTART;TZID=Europe/Berlin:20250506T090000\r
|
DTSTART:20250506T070000Z\r
|
||||||
DTEND;TZID=Europe/Berlin:20250506T092500\r
|
DTEND:20250506T072500Z\r
|
||||||
END:VEVENT\r\n",
|
END:VEVENT\r\n",
|
||||||
"BEGIN:VEVENT\r
|
"BEGIN:VEVENT\r
|
||||||
UID:318ec6503573d9576818daf93dac07317058d95c\r
|
UID:318ec6503573d9576818daf93dac07317058d95c\r
|
||||||
@@ -223,9 +241,9 @@ DTSTAMP:20250502T132758Z\r
|
|||||||
SEQUENCE:2\r
|
SEQUENCE:2\r
|
||||||
SUMMARY:weekly stuff\r
|
SUMMARY:weekly stuff\r
|
||||||
TRANSP:OPAQUE\r
|
TRANSP:OPAQUE\r
|
||||||
RECURRENCE-ID:20250508T090000\r
|
RECURRENCE-ID:20250508T070000Z\r
|
||||||
DTSTART;TZID=Europe/Berlin:20250508T090000\r
|
DTSTART:20250508T070000Z\r
|
||||||
DTEND;TZID=Europe/Berlin:20250508T092500\r
|
DTEND:20250508T072500Z\r
|
||||||
END:VEVENT\r\n",
|
END:VEVENT\r\n",
|
||||||
"BEGIN:VEVENT\r
|
"BEGIN:VEVENT\r
|
||||||
UID:318ec6503573d9576818daf93dac07317058d95c\r
|
UID:318ec6503573d9576818daf93dac07317058d95c\r
|
||||||
@@ -234,8 +252,8 @@ SEQUENCE:2\r
|
|||||||
SUMMARY:weekly stuff\r
|
SUMMARY:weekly stuff\r
|
||||||
TRANSP:OPAQUE\r
|
TRANSP:OPAQUE\r
|
||||||
RECURRENCE-ID:20250511T090000\r
|
RECURRENCE-ID:20250511T090000\r
|
||||||
DTSTART;TZID=Europe/Berlin:20250511T090000\r
|
DTSTART:20250511T070000Z\r
|
||||||
DTEND;TZID=Europe/Berlin:20250511T092500\r
|
DTEND:20250511T072500Z\r
|
||||||
END:VEVENT\r\n",
|
END:VEVENT\r\n",
|
||||||
"BEGIN:VEVENT\r
|
"BEGIN:VEVENT\r
|
||||||
UID:318ec6503573d9576818daf93dac07317058d95c\r
|
UID:318ec6503573d9576818daf93dac07317058d95c\r
|
||||||
@@ -244,25 +262,124 @@ SEQUENCE:2\r
|
|||||||
SUMMARY:weekly stuff\r
|
SUMMARY:weekly stuff\r
|
||||||
TRANSP:OPAQUE\r
|
TRANSP:OPAQUE\r
|
||||||
RECURRENCE-ID:20250520T090000\r
|
RECURRENCE-ID:20250520T090000\r
|
||||||
DTSTART;TZID=Europe/Berlin:20250520T090000\r
|
DTSTA:20250520T070000Z\r
|
||||||
DTEND;TZID=Europe/Berlin:20250520T092500\r
|
DTEND:20250520T072500Z\r
|
||||||
END:VEVENT\r\n",
|
END:VEVENT\r\n",
|
||||||
];
|
];
|
||||||
|
|
||||||
#[test]
|
const ICS_2: &str = r"BEGIN:VCALENDAR
|
||||||
fn test_expand_recurrence() {
|
CALSCALE:GREGORIAN
|
||||||
let event = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
|
VERSION:2.0
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:US/Eastern
|
||||||
|
END:VTIMEZONE
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060102T120000
|
||||||
|
DURATION:PT1H
|
||||||
|
RRULE:FREQ=DAILY;COUNT=5
|
||||||
|
SUMMARY:Event #2
|
||||||
|
UID:abcd2
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060104T140000
|
||||||
|
DURATION:PT1H
|
||||||
|
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
|
||||||
|
SUMMARY:Event #2 bis
|
||||||
|
UID:abcd2
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
";
|
||||||
|
|
||||||
|
const EXPANDED_2: &[&str] = &[
|
||||||
|
"BEGIN:VEVENT\r
|
||||||
|
DTSTAMP:20060206T001121Z\r
|
||||||
|
DURATION:PT1H\r
|
||||||
|
SUMMARY:Event #2\r
|
||||||
|
UID:abcd2\r
|
||||||
|
RECURRENCE-ID:20060103T170000\r
|
||||||
|
DTSTART:20060103T170000\r
|
||||||
|
END:VEVENT\r\n",
|
||||||
|
"BEGIN:VEVENT\r
|
||||||
|
DTSTAMP:20060206T001121Z\r
|
||||||
|
DURATION:PT1H\r
|
||||||
|
SUMMARY:Event #2 bis\r
|
||||||
|
UID:abcd2\r
|
||||||
|
RECURRENCE-ID:20060104T170000\r
|
||||||
|
DTSTART:20060104T190000\r
|
||||||
|
END:VEVENT\r
|
||||||
|
END:VCALENDAR\r\n",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ICS_3: &str = r"BEGIN:VCALENDAR
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
VERSION:2.0
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:US/Eastern
|
||||||
|
END:VTIMEZONE
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
|
||||||
|
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
|
||||||
|
DTSTAMP:20060206T001220Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060104T100000
|
||||||
|
DURATION:PT1H
|
||||||
|
LAST-MODIFIED:20060206T001330Z
|
||||||
|
ORGANIZER:mailto:cyrus@example.com
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:TENTATIVE
|
||||||
|
SUMMARY:Event #3
|
||||||
|
UID:abcd3
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
";
|
||||||
|
|
||||||
|
const EXPANDED_3: &[&str] = &["BEGIN:VEVENT
|
||||||
|
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
|
||||||
|
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
|
||||||
|
DTSTAMP:20060206T001220Z
|
||||||
|
DTSTART:20060104T150000
|
||||||
|
DURATION:PT1H
|
||||||
|
LAST-MODIFIED:20060206T001330Z
|
||||||
|
ORGANIZER:mailto:cyrus@example.com
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:TENTATIVE
|
||||||
|
SUMMARY:Event #3
|
||||||
|
UID:abcd3
|
||||||
|
X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
|
||||||
|
END:VEVENT"];
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(ICS_1, EXPANDED_1, None, None)]
|
||||||
|
// from https://datatracker.ietf.org/doc/html/rfc4791#section-7.8.3
|
||||||
|
#[case(ICS_2, EXPANDED_2,
|
||||||
|
Some(CalDateTime::parse("20060103T000000Z", Some(chrono_tz::US::Eastern)).unwrap().utc()),
|
||||||
|
Some(CalDateTime::parse("20060105T000000Z", Some(chrono_tz::US::Eastern)).unwrap().utc())
|
||||||
|
)]
|
||||||
|
#[case(ICS_3, EXPANDED_3,
|
||||||
|
Some(CalDateTime::parse("20060103T000000Z", Some(chrono_tz::US::Eastern)).unwrap().utc()),
|
||||||
|
Some(CalDateTime::parse("20060105T000000Z", Some(chrono_tz::US::Eastern)).unwrap().utc())
|
||||||
|
)]
|
||||||
|
fn test_expand_recurrence(
|
||||||
|
#[case] ics: &'static str,
|
||||||
|
#[case] expanded: &[&str],
|
||||||
|
#[case] from: Option<DateTime<Utc>>,
|
||||||
|
#[case] to: Option<DateTime<Utc>>,
|
||||||
|
) {
|
||||||
|
let event = CalendarObject::from_ics(ics.to_string(), None).unwrap();
|
||||||
let crate::CalendarObjectComponent::Event(event, overrides) = event.get_data() else {
|
let crate::CalendarObjectComponent::Event(event, overrides) = event.get_data() else {
|
||||||
panic!()
|
panic!()
|
||||||
};
|
};
|
||||||
|
|
||||||
let events: Vec<String> = event
|
let events: Vec<String> = event
|
||||||
.expand_recurrence(None, None, overrides)
|
.expand_recurrence(from, to, overrides)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|event| Emitter::generate(&event))
|
.map(|event| Emitter::generate(&event))
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(events.as_slice()[0], EXPANDED[0]);
|
assert_eq!(events.len(), expanded.len());
|
||||||
assert_eq!(events.as_slice(), &EXPANDED);
|
for (output, reference) in events.iter().zip(expanded) {
|
||||||
|
similar_asserts::assert_eq!(output, reference);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,10 +89,19 @@ impl From<&CalendarObjectComponent> for CalendarObjectType {
|
|||||||
|
|
||||||
impl CalendarObjectComponent {
|
impl CalendarObjectComponent {
|
||||||
fn from_events(mut events: Vec<EventObject>) -> Result<Self, Error> {
|
fn from_events(mut events: Vec<EventObject>) -> Result<Self, Error> {
|
||||||
let main_event = events
|
// A calendar object does not necessarily have to contain a main VOBJECT
|
||||||
|
if events.is_empty() {
|
||||||
|
return Err(Error::MissingCalendar);
|
||||||
|
}
|
||||||
|
#[allow(clippy::option_if_let_else)]
|
||||||
|
let main_event = if let Some(main) = events
|
||||||
.extract_if(.., |event| event.event.get_recurrence_id().is_none())
|
.extract_if(.., |event| event.event.get_recurrence_id().is_none())
|
||||||
.next()
|
.next()
|
||||||
.expect("there must be one main event");
|
{
|
||||||
|
main
|
||||||
|
} else {
|
||||||
|
events.remove(0)
|
||||||
|
};
|
||||||
let overrides = events;
|
let overrides = events;
|
||||||
for event in &overrides {
|
for event in &overrides {
|
||||||
if event.get_uid() != main_event.get_uid() {
|
if event.get_uid() != main_event.get_uid() {
|
||||||
@@ -109,10 +118,19 @@ impl CalendarObjectComponent {
|
|||||||
Ok(Self::Event(main_event, overrides))
|
Ok(Self::Event(main_event, overrides))
|
||||||
}
|
}
|
||||||
fn from_todos(mut todos: Vec<IcalTodo>) -> Result<Self, Error> {
|
fn from_todos(mut todos: Vec<IcalTodo>) -> Result<Self, Error> {
|
||||||
let main_todo = todos
|
// A calendar object does not necessarily have to contain a main VOBJECT
|
||||||
|
if todos.is_empty() {
|
||||||
|
return Err(Error::MissingCalendar);
|
||||||
|
}
|
||||||
|
#[allow(clippy::option_if_let_else)]
|
||||||
|
let main_todo = if let Some(main) = todos
|
||||||
.extract_if(.., |todo| todo.get_recurrence_id().is_none())
|
.extract_if(.., |todo| todo.get_recurrence_id().is_none())
|
||||||
.next()
|
.next()
|
||||||
.expect("there must be one main event");
|
{
|
||||||
|
main
|
||||||
|
} else {
|
||||||
|
todos.remove(0)
|
||||||
|
};
|
||||||
let overrides = todos;
|
let overrides = todos;
|
||||||
for todo in &overrides {
|
for todo in &overrides {
|
||||||
if todo.get_uid() != main_todo.get_uid() {
|
if todo.get_uid() != main_todo.get_uid() {
|
||||||
@@ -129,10 +147,19 @@ impl CalendarObjectComponent {
|
|||||||
Ok(Self::Todo(main_todo, overrides))
|
Ok(Self::Todo(main_todo, overrides))
|
||||||
}
|
}
|
||||||
fn from_journals(mut journals: Vec<IcalJournal>) -> Result<Self, Error> {
|
fn from_journals(mut journals: Vec<IcalJournal>) -> Result<Self, Error> {
|
||||||
let main_journal = journals
|
// A calendar object does not necessarily have to contain a main VOBJECT
|
||||||
|
if journals.is_empty() {
|
||||||
|
return Err(Error::MissingCalendar);
|
||||||
|
}
|
||||||
|
#[allow(clippy::option_if_let_else)]
|
||||||
|
let main_journal = if let Some(main) = journals
|
||||||
.extract_if(.., |journal| journal.get_recurrence_id().is_none())
|
.extract_if(.., |journal| journal.get_recurrence_id().is_none())
|
||||||
.next()
|
.next()
|
||||||
.expect("there must be one main event");
|
{
|
||||||
|
main
|
||||||
|
} else {
|
||||||
|
journals.remove(0)
|
||||||
|
};
|
||||||
let overrides = journals;
|
let overrides = journals;
|
||||||
for journal in &overrides {
|
for journal in &overrides {
|
||||||
if journal.get_uid() != main_journal.get_uid() {
|
if journal.get_uid() != main_journal.get_uid() {
|
||||||
@@ -328,4 +355,12 @@ impl CalendarObject {
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|property| property.name == name)
|
.find(|property| property.name == name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_named_properties(&self, name: &str) -> Vec<&Property> {
|
||||||
|
self.properties
|
||||||
|
.iter()
|
||||||
|
.filter(|property| property.name == name)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,6 +198,14 @@ impl CalDateTime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_timezone(&self, tz: &ICalTimezone) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::DateTime(datetime) => Self::DateTime(datetime.with_timezone(tz)),
|
||||||
|
Self::Date(date, _) => Self::Date(date.to_owned(), tz.to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse(value: &str, timezone: Option<Tz>) -> Result<Self, CalDateTimeError> {
|
pub fn parse(value: &str, timezone: Option<Tz>) -> Result<Self, CalDateTimeError> {
|
||||||
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATE_TIME) {
|
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATE_TIME) {
|
||||||
if let Some(timezone) = timezone {
|
if let Some(timezone) = timezone {
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ use axum::{
|
|||||||
extract::Query,
|
extract::Query,
|
||||||
response::{IntoResponse, Redirect, Response},
|
response::{IntoResponse, Redirect, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::Host;
|
use axum_extra::TypedHeader;
|
||||||
pub use config::OidcConfig;
|
pub use config::OidcConfig;
|
||||||
use config::UserIdClaim;
|
use config::UserIdClaim;
|
||||||
use error::OidcError;
|
use error::OidcError;
|
||||||
|
use headers::Host;
|
||||||
use openidconnect::{
|
use openidconnect::{
|
||||||
AuthenticationFlow, AuthorizationCode, CsrfToken, EndpointMaybeSet, EndpointNotSet,
|
AuthenticationFlow, AuthorizationCode, CsrfToken, EndpointMaybeSet, EndpointNotSet,
|
||||||
EndpointSet, IssuerUrl, Nonce, OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier,
|
EndpointSet, IssuerUrl, Nonce, OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier,
|
||||||
@@ -100,7 +101,7 @@ pub struct GetOidcForm {
|
|||||||
pub async fn route_post_oidc(
|
pub async fn route_post_oidc(
|
||||||
Extension(oidc_config): Extension<OidcConfig>,
|
Extension(oidc_config): Extension<OidcConfig>,
|
||||||
session: Session,
|
session: Session,
|
||||||
Host(host): Host,
|
TypedHeader(host): TypedHeader<Host>,
|
||||||
Form(GetOidcForm { redirect_uri }): Form<GetOidcForm>,
|
Form(GetOidcForm { redirect_uri }): Form<GetOidcForm>,
|
||||||
) -> Result<Response, OidcError> {
|
) -> Result<Response, OidcError> {
|
||||||
let callback_uri = format!("https://{host}/frontend/login/oidc/callback");
|
let callback_uri = format!("https://{host}/frontend/login/oidc/callback");
|
||||||
@@ -155,7 +156,7 @@ pub async fn route_get_oidc_callback<US: UserStore + Clone>(
|
|||||||
Extension(service_config): Extension<OidcServiceConfig>,
|
Extension(service_config): Extension<OidcServiceConfig>,
|
||||||
session: Session,
|
session: Session,
|
||||||
Query(AuthCallbackQuery { code, iss, state }): Query<AuthCallbackQuery>,
|
Query(AuthCallbackQuery { code, iss, state }): Query<AuthCallbackQuery>,
|
||||||
Host(host): Host,
|
TypedHeader(host): TypedHeader<Host>,
|
||||||
) -> Result<Response, OidcError> {
|
) -> Result<Response, OidcError> {
|
||||||
let callback_uri = format!("https://{host}/frontend/login/oidc/callback");
|
let callback_uri = format!("https://{host}/frontend/login/oidc/callback");
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
use rustical_ical::{CalendarObject, CalendarObjectType};
|
use rustical_ical::{CalendarObject, CalendarObjectType};
|
||||||
use rustical_store::{Calendar, CalendarMetadata, CalendarStore};
|
use rustical_store::{Calendar, CalendarMetadata, CalendarStore};
|
||||||
use rustical_store_sqlite::tests::get_test_calendar_store;
|
use rustical_store_sqlite::tests::test_store_context;
|
||||||
|
|
||||||
fn benchmark(c: &mut Criterion) {
|
fn benchmark(c: &mut Criterion) {
|
||||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||||
let cal_store = runtime.block_on(async {
|
let cal_store = runtime.block_on(async {
|
||||||
let cal_store = get_test_calendar_store().await;
|
let cal_store = test_store_context().await.cal_store;
|
||||||
|
|
||||||
cal_store
|
cal_store
|
||||||
.insert_calendar(Calendar {
|
.insert_calendar(Calendar {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ impl TryFrom<AddressObjectRow> for AddressObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Constructor)]
|
#[derive(Debug, Clone, Constructor)]
|
||||||
pub struct SqliteAddressbookStore {
|
pub struct SqliteAddressbookStore {
|
||||||
db: SqlitePool,
|
db: SqlitePool,
|
||||||
sender: Sender<CollectionOperation>,
|
sender: Sender<CollectionOperation>,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ impl From<CalendarRow> for Calendar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Constructor)]
|
#[derive(Debug, Clone, Constructor)]
|
||||||
pub struct SqliteCalendarStore {
|
pub struct SqliteCalendarStore {
|
||||||
db: SqlitePool,
|
db: SqlitePool,
|
||||||
sender: Sender<CollectionOperation>,
|
sender: Sender<CollectionOperation>,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub(crate) enum ChangeOperation {
|
|||||||
Delete,
|
Delete,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SqliteStore {
|
pub struct SqliteStore {
|
||||||
db: SqlitePool,
|
db: SqlitePool,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ impl TryFrom<PrincipalRow> for Principal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Constructor)]
|
#[derive(Debug, Clone, Constructor)]
|
||||||
pub struct SqlitePrincipalStore {
|
pub struct SqlitePrincipalStore {
|
||||||
db: SqlitePool,
|
db: SqlitePool,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{addressbook_store::SqliteAddressbookStore, tests::get_test_addressbook_store};
|
use crate::tests::{TestStoreContext, test_store_context};
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
use rustical_store::{Addressbook, AddressbookStore};
|
use rustical_store::{Addressbook, AddressbookStore};
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_addressbook_store(
|
async fn test_addressbook_store(
|
||||||
#[from(get_test_addressbook_store)]
|
#[from(test_store_context)]
|
||||||
#[future]
|
#[future]
|
||||||
addr_store: SqliteAddressbookStore,
|
context: TestStoreContext,
|
||||||
) {
|
) {
|
||||||
let addr_store = addr_store.await;
|
let addr_store = context.await.addr_store;
|
||||||
|
|
||||||
let cal = Addressbook {
|
let cal = Addressbook {
|
||||||
id: "addr".to_string(),
|
id: "addr".to_string(),
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{calendar_store::SqliteCalendarStore, tests::get_test_calendar_store};
|
use crate::tests::{TestStoreContext, test_store_context};
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
use rustical_store::{Calendar, CalendarMetadata, CalendarStore};
|
use rustical_store::{Calendar, CalendarMetadata, CalendarStore};
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_calendar_store(
|
async fn test_calendar_store(
|
||||||
#[from(get_test_calendar_store)]
|
|
||||||
#[future]
|
#[future]
|
||||||
cal_store: SqliteCalendarStore,
|
#[from(test_store_context)]
|
||||||
|
context: TestStoreContext,
|
||||||
) {
|
) {
|
||||||
let cal_store = cal_store.await;
|
let TestStoreContext { cal_store, .. } = context.await;
|
||||||
|
|
||||||
|
let cal_store = cal_store;
|
||||||
|
|
||||||
let cal = Calendar {
|
let cal = Calendar {
|
||||||
principal: "fake-user".to_string(),
|
principal: "fake-user".to_string(),
|
||||||
|
|||||||
@@ -2,64 +2,59 @@ use crate::{
|
|||||||
SqliteStore, addressbook_store::SqliteAddressbookStore, calendar_store::SqliteCalendarStore,
|
SqliteStore, addressbook_store::SqliteAddressbookStore, calendar_store::SqliteCalendarStore,
|
||||||
principal_store::SqlitePrincipalStore,
|
principal_store::SqlitePrincipalStore,
|
||||||
};
|
};
|
||||||
use rustical_store::{
|
use rstest::fixture;
|
||||||
Secret,
|
use rustical_store::auth::{AuthenticationProvider, Principal, PrincipalType};
|
||||||
auth::{AuthenticationProvider, Principal, PrincipalType},
|
|
||||||
};
|
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use tokio::sync::OnceCell;
|
|
||||||
|
|
||||||
static DB: OnceCell<SqlitePool> = OnceCell::const_new();
|
|
||||||
|
|
||||||
mod addressbook_store;
|
mod addressbook_store;
|
||||||
mod calendar_store;
|
mod calendar_store;
|
||||||
|
|
||||||
async fn get_test_db() -> SqlitePool {
|
async fn get_test_db() -> SqlitePool {
|
||||||
DB.get_or_init(async || {
|
let db = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
||||||
let db = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
sqlx::migrate!("./migrations").run(&db).await.unwrap();
|
||||||
sqlx::migrate!("./migrations").run(&db).await.unwrap();
|
|
||||||
|
|
||||||
// Populate with test data
|
// Populate with test data
|
||||||
let principal_store = SqlitePrincipalStore::new(db.clone());
|
let principal_store = SqlitePrincipalStore::new(db.clone());
|
||||||
principal_store
|
principal_store
|
||||||
.insert_principal(
|
.insert_principal(
|
||||||
Principal {
|
Principal {
|
||||||
id: "user".to_owned(),
|
id: "user".to_owned(),
|
||||||
displayname: None,
|
displayname: None,
|
||||||
memberships: vec![],
|
memberships: vec![],
|
||||||
password: None,
|
password: None,
|
||||||
principal_type: PrincipalType::Individual,
|
principal_type: PrincipalType::Individual,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
principal_store
|
principal_store
|
||||||
.add_app_token("user", "test".to_string(), "pass".to_string())
|
.add_app_token("user", "test".to_string(), "pass".to_string())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
db
|
db
|
||||||
})
|
|
||||||
.await
|
|
||||||
.clone()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest::fixture]
|
#[derive(Debug, Clone)]
|
||||||
pub async fn get_test_addressbook_store() -> SqliteAddressbookStore {
|
pub struct TestStoreContext {
|
||||||
let (send, _recv) = tokio::sync::mpsc::channel(1000);
|
pub db: SqlitePool,
|
||||||
SqliteAddressbookStore::new(get_test_db().await, send)
|
pub addr_store: SqliteAddressbookStore,
|
||||||
|
pub cal_store: SqliteCalendarStore,
|
||||||
|
pub principal_store: SqlitePrincipalStore,
|
||||||
|
pub sub_store: SqliteStore,
|
||||||
}
|
}
|
||||||
#[rstest::fixture]
|
|
||||||
pub async fn get_test_calendar_store() -> SqliteCalendarStore {
|
#[fixture]
|
||||||
let (send, _recv) = tokio::sync::mpsc::channel(1000);
|
pub async fn test_store_context() -> TestStoreContext {
|
||||||
SqliteCalendarStore::new(get_test_db().await, send)
|
let (send_addr, _recv) = tokio::sync::mpsc::channel(1);
|
||||||
}
|
let (send_cal, _recv) = tokio::sync::mpsc::channel(1);
|
||||||
#[rstest::fixture]
|
let db = get_test_db().await;
|
||||||
pub async fn get_test_subscription_store() -> SqliteStore {
|
TestStoreContext {
|
||||||
SqliteStore::new(get_test_db().await)
|
db: db.clone(),
|
||||||
}
|
addr_store: SqliteAddressbookStore::new(db.clone(), send_addr),
|
||||||
#[rstest::fixture]
|
cal_store: SqliteCalendarStore::new(db.clone(), send_cal),
|
||||||
pub async fn get_test_principal_store() -> SqlitePrincipalStore {
|
principal_store: SqlitePrincipalStore::new(db.clone()),
|
||||||
SqlitePrincipalStore::new(get_test_db().await)
|
sub_store: SqliteStore::new(db),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ use quick_xml::events::BytesStart;
|
|||||||
use crate::{XmlDeserialize, XmlError};
|
use crate::{XmlDeserialize, XmlError};
|
||||||
|
|
||||||
// TODO: actually implement
|
// TODO: actually implement
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct Unparsed(BytesStart<'static>);
|
pub struct Unparsed(String);
|
||||||
|
|
||||||
impl Unparsed {
|
impl Unparsed {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn tag_name(&self) -> String {
|
pub fn tag_name(&self) -> String {
|
||||||
// TODO: respect namespace?
|
// TODO: respect namespace?
|
||||||
String::from_utf8_lossy(self.0.local_name().as_ref()).to_string()
|
self.0.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ impl XmlDeserialize for Unparsed {
|
|||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
reader.read_to_end_into(start.name(), &mut buf)?;
|
reader.read_to_end_into(start.name(), &mut buf)?;
|
||||||
}
|
}
|
||||||
Ok(Self(start.to_owned()))
|
let tag_name = String::from_utf8_lossy(start.local_name().as_ref()).to_string();
|
||||||
|
Ok(Self(tag_name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
253
docs/developers/rfcs/rfc4791.md
Normal file
253
docs/developers/rfcs/rfc4791.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# RFC 4791 (CalDAV)
|
||||||
|
|
||||||
|
## ☑ 1. Introduction
|
||||||
|
|
||||||
|
### ☑ 1.1 Notational Conventions
|
||||||
|
|
||||||
|
### ☑ 1.2 XML Namespaces and Processing
|
||||||
|
|
||||||
|
### ☐ 1.3 Method Preconditions and Postconditions
|
||||||
|
|
||||||
|
## ☐ 2. Requirements Overview
|
||||||
|
|
||||||
|
- [x] MUST support iCalendar [RFC2445] as a media type for the calendar
|
||||||
|
object resource format;
|
||||||
|
|
||||||
|
- [ ] MUST support WebDAV Class 1 [RFC2518] (note that [rfc2518bis]
|
||||||
|
describes clarifications to [RFC2518] that aid interoperability);
|
||||||
|
|
||||||
|
- [x] MUST support WebDAV ACL [RFC3744] with the additional privilege
|
||||||
|
defined in Section 6.1 of this document;
|
||||||
|
|
||||||
|
- [x] MUST support transport over TLS [RFC2246] as defined in [RFC2818]
|
||||||
|
(note that [RFC2246] has been obsoleted by [RFC4346]);
|
||||||
|
|
||||||
|
- [x] MUST support ETags [RFC2616] with additional requirements
|
||||||
|
specified in Section 5.3.4 of this document;
|
||||||
|
|
||||||
|
- [ ] MUST support all calendaring reports defined in Section 7 of this
|
||||||
|
document; and
|
||||||
|
|
||||||
|
- [x] MUST advertise support on all calendar collections and calendar
|
||||||
|
object resources for the calendaring reports in the DAV:supported-
|
||||||
|
report-set property, as defined in Versioning Extensions to WebDAV
|
||||||
|
[RFC3253].
|
||||||
|
|
||||||
|
In addition, a server:
|
||||||
|
|
||||||
|
- [x] SHOULD support the MKCALENDAR method defined in Section 5.3.1 of
|
||||||
|
this document.
|
||||||
|
|
||||||
|
## ☑ 3. Calendaring Data Model
|
||||||
|
|
||||||
|
### ☑ 3.1 Calendar Server
|
||||||
|
|
||||||
|
### ☑ 3.2 Recurrence and the Data Model
|
||||||
|
|
||||||
|
## ☑ 4. Calendar Resources
|
||||||
|
|
||||||
|
### ☑ 4.1 Calendar Object Resources
|
||||||
|
|
||||||
|
### ☑ 4.2 Calendar Collection
|
||||||
|
|
||||||
|
## ☐ 5. Calendar Access Feature
|
||||||
|
|
||||||
|
### ☑ 5.1 Calendar Access Support
|
||||||
|
|
||||||
|
#### ☑ 5.1.1 Example: Using OPTIONS for the Discovery of Calendar Access Support
|
||||||
|
|
||||||
|
### ☑ 5.2 Calendar Collection Properties
|
||||||
|
|
||||||
|
#### ☑ 5.2.1 CALDAV:calendar-description Property
|
||||||
|
|
||||||
|
#### ☑ 5.2.2 CALDAV:calendar-timezone Property
|
||||||
|
|
||||||
|
#### ☑ 5.2.3 CALDAV:supported-calendar-component-set Property
|
||||||
|
|
||||||
|
#### ☑ 5.2.4 CALDAV:supported-calendar-data Property
|
||||||
|
|
||||||
|
#### ☑ 5.2.5 CALDAV:max-resource-size Property
|
||||||
|
|
||||||
|
#### ☑ 5.2.6 CALDAV:min-date-time Property
|
||||||
|
|
||||||
|
#### ☑ 5.2.7 CALDAV:max-date-time Property
|
||||||
|
|
||||||
|
#### ☐ 5.2.8 CALDAV:max-instances Property (Maybe set this :))
|
||||||
|
|
||||||
|
#### ☑ 5.2.9 CALDAV:max-attendees-per-instance Property (does not apply)
|
||||||
|
|
||||||
|
#### ☑ 5.2.10 Additional Precondition for PROPPATCH
|
||||||
|
|
||||||
|
### ☑ 5.3 Creating Resources
|
||||||
|
|
||||||
|
#### ☑ 5.3.1 MKCALENDAR Method
|
||||||
|
|
||||||
|
##### ☑ 5.3.1.1 Status Codes
|
||||||
|
|
||||||
|
##### ☑ 5.3.1.2 Example: Successful MKCALENDAR Request
|
||||||
|
|
||||||
|
- Example fails because of the tzid is not in the Olson database, but that's okay
|
||||||
|
|
||||||
|
#### ☑ 5.3.2 Creating Calendar Object Resources
|
||||||
|
|
||||||
|
##### ☐ 5.3.2.1 Additional Preconditions for PUT, COPY, and MOVE
|
||||||
|
|
||||||
|
### ☑ 5.3.3 Non-Standard Components, Properties, and Parameters
|
||||||
|
|
||||||
|
### ☑ 5.3.4 Calendar Object Resource Entity Tag
|
||||||
|
|
||||||
|
## ☐ 6. Calendaring Access Control
|
||||||
|
|
||||||
|
### ☐ 6.1 Calendaring Privilege
|
||||||
|
|
||||||
|
#### ☐ 6.1.1 CALDAV:read-free-busy Privilege
|
||||||
|
|
||||||
|
### ☑ 6.2 Additional Principal Property
|
||||||
|
|
||||||
|
#### ☑ 6.2.1 CALDAV:calendar-home-set Property
|
||||||
|
|
||||||
|
## ☐ 7. Calendaring Reports
|
||||||
|
|
||||||
|
- [ ] `DAV:expand-property`
|
||||||
|
|
||||||
|
### ☑ 7.1 REPORT Method
|
||||||
|
|
||||||
|
### ☑ 7.2 Ordinary Collections
|
||||||
|
|
||||||
|
### ☑ 7.3 Date and Floating Time
|
||||||
|
|
||||||
|
### ☑ 7.4 Time Range Filtering
|
||||||
|
|
||||||
|
### ☑ 7.5 Searching Text: Collations
|
||||||
|
|
||||||
|
#### ☑ 7.5.1 CALDAV:supported-collation-set Property
|
||||||
|
|
||||||
|
### ☐ 7.6 Partial Retrieval
|
||||||
|
|
||||||
|
### ☑ 7.7 Non-Standard Components, Properties, and Parameters
|
||||||
|
|
||||||
|
### ☑ 7.8 CALDAV:calendar-query REPORT
|
||||||
|
|
||||||
|
#### ☐ 7.8.1 Example: Partial Retrieval of Events by Time Range
|
||||||
|
|
||||||
|
#### ☐ 7.8.2 Example: Partial Retrieval of Recurring Events
|
||||||
|
|
||||||
|
#### ☐ 7.8.3 Example: Expanded Retrieval of Recurring Events
|
||||||
|
|
||||||
|
#### ☐ 7.8.4 Example: Partial Retrieval of Stored Free Busy Components
|
||||||
|
|
||||||
|
#### ☐ 7.8.5 Example: Retrieval of To-Dos by Alarm Time Range
|
||||||
|
|
||||||
|
#### ☐ 7.8.6 Example: Retrieval of Event by UID
|
||||||
|
|
||||||
|
#### ☐ 7.8.7 Example: Retrieval of Events by PARTSTAT
|
||||||
|
|
||||||
|
#### ☐ 7.8.8 Example: Retrieval of Events Only
|
||||||
|
|
||||||
|
#### ☐ 7.8.9 Example: Retrieval of All Pending To-Dos
|
||||||
|
|
||||||
|
#### ☐ 7.8.10 Example: Attempt to Query Unsupported Property
|
||||||
|
|
||||||
|
### ☐ 7.9 CALDAV:calendar-multiget REPORT
|
||||||
|
|
||||||
|
#### ☐ 7.9.1 Example: Successful CALDAV:calendar-multiget REPORT
|
||||||
|
|
||||||
|
### ☐ 7.10 CALDAV:free-busy-query REPORT
|
||||||
|
|
||||||
|
#### ☐ 7.10.1 Example: Successful CALDAV:free-busy-query REPORT
|
||||||
|
|
||||||
|
## ☐ 8. Guidelines
|
||||||
|
|
||||||
|
### ☐ 8.1 Client-to-Client Interoperability
|
||||||
|
|
||||||
|
### ☐ 8.2 Synchronization Operations
|
||||||
|
|
||||||
|
#### ☐ 8.2.1 Use of Reports
|
||||||
|
|
||||||
|
##### ☐ 8.2.1.1 Restrict the Time Range
|
||||||
|
|
||||||
|
##### ☐ 8.2.1.2 Synchronize by Time Range
|
||||||
|
|
||||||
|
##### ☐ 8.2.1.3 Synchronization Process
|
||||||
|
|
||||||
|
#### ☐ 8.2.2 Restrict the Properties Returned
|
||||||
|
|
||||||
|
### ☐ 8.3 Use of Locking
|
||||||
|
|
||||||
|
### ☐ 8.4 Finding Calendars
|
||||||
|
|
||||||
|
### ☐ 8.5 Storing and Using Attachments
|
||||||
|
|
||||||
|
#### ☐ 8.5.1 Inline Attachments
|
||||||
|
|
||||||
|
#### ☐ 8.5.2 External Attachments
|
||||||
|
|
||||||
|
### ☐ 8.6 Storing and Using Alarms
|
||||||
|
|
||||||
|
## ☐ 9. XML Element Definitions
|
||||||
|
|
||||||
|
### ☐ 9.1 CALDAV:calendar XML Element
|
||||||
|
|
||||||
|
### ☐ 9.2 CALDAV:mkcalendar XML Element
|
||||||
|
|
||||||
|
### ☐ 9.3 CALDAV:mkcalendar-response XML Element
|
||||||
|
|
||||||
|
### ☐ 9.4 CALDAV:supported-collation XML Element
|
||||||
|
|
||||||
|
### ☐ 9.5 CALDAV:calendar-query XML Element
|
||||||
|
|
||||||
|
### ☐ 9.6 CALDAV:calendar-data XML Element
|
||||||
|
|
||||||
|
#### ☐ 9.6.1 CALDAV:comp XML Element
|
||||||
|
|
||||||
|
#### ☐ 9.6.2 CALDAV:allcomp XML Element
|
||||||
|
|
||||||
|
#### ☐ 9.6.3 CALDAV:allprop XML Element
|
||||||
|
|
||||||
|
#### ☐ 9.6.4 CALDAV:prop XML Element
|
||||||
|
|
||||||
|
#### ☐ 9.6.5 CALDAV:expand XML Element
|
||||||
|
|
||||||
|
#### ☐ 9.6.6 CALDAV:limit-recurrence-set XML Element
|
||||||
|
|
||||||
|
#### ☐ 9.6.7 CALDAV:limit-freebusy-set XML Element
|
||||||
|
|
||||||
|
### ☐ 9.7 CALDAV:filter XML Element
|
||||||
|
|
||||||
|
#### ☐ 9.7.1 CALDAV:comp-filter XML Element
|
||||||
|
|
||||||
|
#### ☐ 9.7.2 CALDAV:prop-filter XML Element
|
||||||
|
|
||||||
|
#### ☐ 9.7.3 CALDAV:param-filter XML Element
|
||||||
|
|
||||||
|
#### ☐ 9.7.4 CALDAV:is-not-defined XML Element
|
||||||
|
|
||||||
|
#### ☐ 9.7.5 CALDAV:text-match XML Element
|
||||||
|
|
||||||
|
### ☐ 9.8 CALDAV:timezone XML Element
|
||||||
|
|
||||||
|
### ☐ 9.9 CALDAV:time-range XML Element
|
||||||
|
|
||||||
|
### ☐ 9.10 CALDAV:calendar-multiget XML Element
|
||||||
|
|
||||||
|
### ☐ 9.11 CALDAV:free-busy-query XML Element
|
||||||
|
|
||||||
|
## ☐ 10. Internationalization Considerations
|
||||||
|
|
||||||
|
## ☐ 11. Security Considerations
|
||||||
|
|
||||||
|
## ☐ 12. IANA Considerations
|
||||||
|
|
||||||
|
### ☐ 12.1 Namespace Registration
|
||||||
|
|
||||||
|
## ☐ 13. Acknowledgements
|
||||||
|
|
||||||
|
## ☐ 14. References
|
||||||
|
|
||||||
|
### ☐ 14.1 Normative References
|
||||||
|
|
||||||
|
### ☐ 14.2 Informative References
|
||||||
|
|
||||||
|
## ☐ A. CalDAV Method Privilege Table (Normative)
|
||||||
|
|
||||||
|
## ☐ B. Calendar Collections Used in the Examples
|
||||||
175
docs/developers/rfcs/rfc6352.md
Normal file
175
docs/developers/rfcs/rfc6352.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# RFC 6352 (CardDAV)
|
||||||
|
|
||||||
|
## ☑ 1. Introduction and Overview
|
||||||
|
|
||||||
|
## ☑ 2. Conventions
|
||||||
|
|
||||||
|
## ☐ 3. Requirements Overview
|
||||||
|
|
||||||
|
## ☑ 4. Address Book Data Model
|
||||||
|
|
||||||
|
### ☑ 4.1 Address Book Server
|
||||||
|
|
||||||
|
## ☐ 5. Address Book Resources
|
||||||
|
|
||||||
|
### ☐ 5.1 Address Object Resources
|
||||||
|
|
||||||
|
- vCard objects MUST have a unique UID
|
||||||
|
- Right now the uniqueness is not enforced in store_sqlite :(
|
||||||
|
|
||||||
|
#### ☐ 5.1.1 Data Type Conversion
|
||||||
|
|
||||||
|
Again, the client can use content negotiation to
|
||||||
|
request that data be returned in a specific media type by specifying
|
||||||
|
appropriate attributes on the CARDDAV:address-data XML element used
|
||||||
|
in the request body (see Section 10.4).
|
||||||
|
|
||||||
|
- [ ] Accept address-data attributes
|
||||||
|
|
||||||
|
#### ☐ 5.1.1.1 Additional Precondition for GET
|
||||||
|
|
||||||
|
- Make sure that Accept header matches content type
|
||||||
|
|
||||||
|
### ☐ 5.2 Address Book Collections
|
||||||
|
|
||||||
|
## ☑ 6. Address Book Feature
|
||||||
|
|
||||||
|
### ☑ 6.1 Address Book Support
|
||||||
|
|
||||||
|
#### ☑ 6.1.1 Example: Using OPTIONS for the Discovery of Support for CardDAV
|
||||||
|
|
||||||
|
### ☐ 6.2 Address Book Properties
|
||||||
|
|
||||||
|
#### ☑ 6.2.1 CARDDAV:addressbook-description Property
|
||||||
|
|
||||||
|
#### ☑ 6.2.2 CARDDAV:supported-address-data Property
|
||||||
|
|
||||||
|
#### ☑ 6.2.3 CARDDAV:max-resource-size Property
|
||||||
|
|
||||||
|
### ☐ 6.3 Creating Resources
|
||||||
|
|
||||||
|
#### ☑ 6.3.1 Extended MKCOL Method
|
||||||
|
|
||||||
|
##### ☑ 6.3.1.1 Example - Successful MKCOL Request
|
||||||
|
|
||||||
|
#### ☐ 6.3.2 Creating Address Object Resources
|
||||||
|
|
||||||
|
- [x] If-None-Match support
|
||||||
|
|
||||||
|
##### ☐ 6.3.2.1 Additional Preconditions for PUT, COPY, and MOVE
|
||||||
|
|
||||||
|
- [ ] Make sure UID is unique
|
||||||
|
|
||||||
|
##### ☑ 6.3.2.2 Non-Standard vCard Properties and Parameters
|
||||||
|
|
||||||
|
##### ☑ 6.3.2.3 Address Object Resource Entity Tag
|
||||||
|
|
||||||
|
## ☑ 7. Address Book Access Control
|
||||||
|
|
||||||
|
### ☑ 7.1 Additional Principal Properties
|
||||||
|
|
||||||
|
#### ☑ 7.1.1 CARDDAV:addressbook-home-set Property
|
||||||
|
|
||||||
|
#### ☑ 7.1.2 CARDDAV:principal-address Property
|
||||||
|
|
||||||
|
## ☐ 8. Address Book Reports
|
||||||
|
|
||||||
|
### ☐ 8.1 REPORT Method
|
||||||
|
|
||||||
|
- [ ] DAV:expand-property REPORT
|
||||||
|
|
||||||
|
### ☑ 8.2 Ordinary Collections
|
||||||
|
|
||||||
|
### ☑ 8.3 Searching Text: Collations
|
||||||
|
|
||||||
|
#### ☑ 8.3.1 CARDDAV:supported-collation-set Property
|
||||||
|
|
||||||
|
### ☐ 8.4 Partial Retrieval (Optional)
|
||||||
|
|
||||||
|
### ☑ 8.5 Non-Standard Properties and Parameters
|
||||||
|
|
||||||
|
### ☑ 8.6 CARDDAV:addressbook-query Report
|
||||||
|
|
||||||
|
#### ☐ 8.6.1 Limiting Results
|
||||||
|
|
||||||
|
#### ☑ 8.6.2 Truncation of Results (does not apply)
|
||||||
|
|
||||||
|
#### ☐ 8.6.3 Example: Partial Retrieval of vCards Matching NICKNAME
|
||||||
|
|
||||||
|
#### ☐ 8.6.4 Example: Partial Retrieval of vCards Matching a Full Name or Email Address
|
||||||
|
|
||||||
|
#### ☐ 8.6.5 Example: Truncated Results
|
||||||
|
|
||||||
|
### ☐ 8.7 CARDDAV:addressbook-multiget Report
|
||||||
|
|
||||||
|
#### ☑ 8.7.1 Example: CARDDAV:addressbook-multiget Report
|
||||||
|
|
||||||
|
#### ☐ 8.7.2 Example: CARDDAV:addressbook-multiget Report
|
||||||
|
|
||||||
|
- [ ] Check for content-type of requested data
|
||||||
|
|
||||||
|
```
|
||||||
|
<C:address-data content-type='text/vcard' version='4.0'/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## ☑ 9. Client Guidelines
|
||||||
|
|
||||||
|
### ☑ 9.1 Restrict the Properties Returned
|
||||||
|
|
||||||
|
### ☑ 9.2 Avoiding Lost Updates
|
||||||
|
|
||||||
|
### ☑ 9.3 Client Configuration
|
||||||
|
|
||||||
|
### ☐ 9.4 Finding Other Users' Address Books
|
||||||
|
|
||||||
|
- [ ] Implement DAV:principal-property-search REPORT [RFC3744]
|
||||||
|
|
||||||
|
## ☑ 10. XML Element Definitions
|
||||||
|
|
||||||
|
### ☑ 10.1 CARDDAV:addressbook XML Element
|
||||||
|
|
||||||
|
### ☑ 10.2 CARDDAV:supported-collation XML Element
|
||||||
|
|
||||||
|
### ☑ 10.3 CARDDAV:addressbook-query XML Element
|
||||||
|
|
||||||
|
### ☑ 10.4 CARDDAV:address-data XML Element
|
||||||
|
|
||||||
|
- [ ] Support content-type and version
|
||||||
|
|
||||||
|
#### ☐ 10.4.1 CARDDAV:allprop XML Element (does not apply, is for vCard props)
|
||||||
|
|
||||||
|
#### ☐ 10.4.2 CARDDAV:prop XML Element (does not apply, is for vCard props)
|
||||||
|
|
||||||
|
### ☑ 10.5 CARDDAV:filter XML Element
|
||||||
|
|
||||||
|
#### ☑ 10.5.1 CARDDAV:prop-filter XML Element
|
||||||
|
|
||||||
|
#### ☑ 10.5.2 CARDDAV:param-filter XML Element
|
||||||
|
|
||||||
|
#### ☑ 10.5.3 CARDDAV:is-not-defined XML Element
|
||||||
|
|
||||||
|
#### ☑ 10.5.4 CARDDAV:text-match XML Element
|
||||||
|
|
||||||
|
### ☑ 10.6 CARDDAV:limit XML Element
|
||||||
|
|
||||||
|
#### ☑ 10.6.1 CARDDAV:nresults XML Element
|
||||||
|
|
||||||
|
### ☑ 10.7 CARDDAV:addressbook-multiget XML Element
|
||||||
|
|
||||||
|
## ☑ 11. Service Discovery via SRV Records
|
||||||
|
|
||||||
|
## ☑ 12. Internationalization Considerations
|
||||||
|
|
||||||
|
## ☑ 13. Security Considerations
|
||||||
|
|
||||||
|
## ☑ 14. IANA Consideration
|
||||||
|
|
||||||
|
### ☑ 14.1 Namespace Registration
|
||||||
|
|
||||||
|
## ☑ 15. Acknowledgments
|
||||||
|
|
||||||
|
## ☑ 16. References
|
||||||
|
|
||||||
|
### ☑ 16.1 Normative References
|
||||||
|
|
||||||
|
### ☑ 16.2 Informative References
|
||||||
@@ -59,6 +59,8 @@ markdown_extensions:
|
|||||||
- admonition
|
- admonition
|
||||||
- attr_list
|
- attr_list
|
||||||
- pymdownx.tabbed
|
- pymdownx.tabbed
|
||||||
|
- pymdownx.tasklist:
|
||||||
|
custom_checkbox: true
|
||||||
- pymdownx.emoji:
|
- pymdownx.emoji:
|
||||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||||
@@ -73,7 +75,9 @@ nav:
|
|||||||
- OpenID Connect: setup/oidc.md
|
- OpenID Connect: setup/oidc.md
|
||||||
- Developers:
|
- Developers:
|
||||||
- developers/index.md
|
- developers/index.md
|
||||||
- Relevant RFCs: developers/rfcs.md
|
- Relevant RFCs:
|
||||||
|
- developers/rfcs.md
|
||||||
|
- RFC 6352: developers/rfcs/rfc6352.md
|
||||||
- Frontend: developers/frontend.md
|
- Frontend: developers/frontend.md
|
||||||
- Debugging: developers/debugging.md
|
- Debugging: developers/debugging.md
|
||||||
- Cargo docs: /rustical/_crate/rustical/
|
- Cargo docs: /rustical/_crate/rustical/
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use headers::{Authorization, HeaderMapExt};
|
|||||||
use http::{HeaderValue, StatusCode};
|
use http::{HeaderValue, StatusCode};
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
use rustical_store::{CalendarMetadata, CalendarStore};
|
use rustical_store::{CalendarMetadata, CalendarStore};
|
||||||
use rustical_store_sqlite::{calendar_store::SqliteCalendarStore, tests::get_test_calendar_store};
|
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
fn mkcalendar_template(
|
fn mkcalendar_template(
|
||||||
@@ -29,12 +29,34 @@ fn mkcalendar_template(
|
|||||||
<displayname>{displayname}</displayname>
|
<displayname>{displayname}</displayname>
|
||||||
<CAL:calendar-description>{description}</CAL:calendar-description>
|
<CAL:calendar-description>{description}</CAL:calendar-description>
|
||||||
<n0:calendar-color xmlns:n0="http://apple.com/ns/ical/">{color}</n0:calendar-color>
|
<n0:calendar-color xmlns:n0="http://apple.com/ns/ical/">{color}</n0:calendar-color>
|
||||||
<CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
|
|
||||||
<CAL:supported-calendar-component-set>
|
<CAL:supported-calendar-component-set>
|
||||||
<CAL:comp name="VEVENT"/>
|
<CAL:comp name="VEVENT"/>
|
||||||
<CAL:comp name="VTODO"/>
|
<CAL:comp name="VTODO"/>
|
||||||
<CAL:comp name="VJOURNAL"/>
|
<CAL:comp name="VJOURNAL"/>
|
||||||
</CAL:supported-calendar-component-set>
|
</CAL:supported-calendar-component-set>
|
||||||
|
<CAL:calendar-timezone><![CDATA[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 & Canada)
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
DTSTART:19870405T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
|
||||||
|
TZOFFSETFROM:-0500
|
||||||
|
TZOFFSETTO:-0400
|
||||||
|
TZNAME:Eastern Daylight Time (US & Canada)
|
||||||
|
END:DAYLIGHT
|
||||||
|
END:VTIMEZONE
|
||||||
|
END:VCALENDAR
|
||||||
|
]]></CAL:calendar-timezone>
|
||||||
</prop>
|
</prop>
|
||||||
</set>
|
</set>
|
||||||
</CAL:mkcalendar>
|
</CAL:mkcalendar>
|
||||||
@@ -48,15 +70,13 @@ fn mkcalendar_template(
|
|||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_caldav_calendar(
|
async fn test_caldav_calendar(
|
||||||
#[from(get_app)]
|
#[from(test_store_context)]
|
||||||
#[future]
|
#[future]
|
||||||
app: axum::Router,
|
context: TestStoreContext,
|
||||||
#[from(get_test_calendar_store)]
|
|
||||||
#[future]
|
|
||||||
cal_store: SqliteCalendarStore,
|
|
||||||
) {
|
) {
|
||||||
let app = app.await;
|
let context = context.await;
|
||||||
let cal_store = cal_store.await;
|
let app = get_app(context.clone());
|
||||||
|
let cal_store = context.cal_store;
|
||||||
|
|
||||||
let mut calendar_meta = CalendarMetadata {
|
let mut calendar_meta = CalendarMetadata {
|
||||||
displayname: Some("Calendar".to_string()),
|
displayname: Some("Calendar".to_string()),
|
||||||
@@ -211,3 +231,106 @@ async fn test_caldav_calendar(
|
|||||||
Err(rustical_store::Error::NotFound)
|
Err(rustical_store::Error::NotFound)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_rfc4791_5_3_2(
|
||||||
|
#[from(test_store_context)]
|
||||||
|
#[future]
|
||||||
|
context: TestStoreContext,
|
||||||
|
) {
|
||||||
|
let context = context.await;
|
||||||
|
let app = get_app(context.clone());
|
||||||
|
|
||||||
|
let calendar_meta = CalendarMetadata {
|
||||||
|
displayname: Some("Calendar".to_string()),
|
||||||
|
description: Some("Description".to_string()),
|
||||||
|
color: Some("#00FF00".to_string()),
|
||||||
|
order: 0,
|
||||||
|
};
|
||||||
|
let (principal, cal_id) = ("user", "calendar");
|
||||||
|
let url = format!("/caldav/principal/{principal}/{cal_id}");
|
||||||
|
|
||||||
|
let request_template = || {
|
||||||
|
Request::builder()
|
||||||
|
.method("MKCALENDAR")
|
||||||
|
.uri(&url)
|
||||||
|
.body(Body::from(mkcalendar_template(&calendar_meta)))
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try with correct credentials
|
||||||
|
let mut request = request_template();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::CREATED);
|
||||||
|
|
||||||
|
let ical = r"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";
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("PUT")
|
||||||
|
.uri(format!("{url}/qwue23489.ics"))
|
||||||
|
.header("If-None-Match", "*")
|
||||||
|
.header("Content-Type", "text/calendar")
|
||||||
|
.body(Body::from(ical))
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::CREATED);
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("PUT")
|
||||||
|
.uri(format!("{url}/qwue23489.ics"))
|
||||||
|
.header("If-None-Match", "*")
|
||||||
|
.header("Content-Type", "text/calendar")
|
||||||
|
.body(Body::from(ical))
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::CONFLICT);
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("REPORT")
|
||||||
|
.uri(&url)
|
||||||
|
.header("Depth", "infinity")
|
||||||
|
.header("Content-Type", "text/xml; charset=\"utf-8\"")
|
||||||
|
.body(Body::from(format!(
|
||||||
|
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/>
|
||||||
|
</D:prop>
|
||||||
|
<D:href>{url}/qwue23489.ics</D:href>
|
||||||
|
<D:href>/home/bernard/addressbook/vcf1.vcf</D:href>
|
||||||
|
</C:calendar-multiget>
|
||||||
|
"#
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::MULTI_STATUS);
|
||||||
|
let body = response.extract_string().await;
|
||||||
|
insta::assert_snapshot!("multiget_body", body);
|
||||||
|
}
|
||||||
|
|||||||
98
src/integration_tests/caldav/calendar_import.rs
Normal file
98
src/integration_tests/caldav/calendar_import.rs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
use crate::integration_tests::{ResponseExtractString, get_app};
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::extract::Request;
|
||||||
|
use headers::{Authorization, HeaderMapExt};
|
||||||
|
use http::StatusCode;
|
||||||
|
use rstest::rstest;
|
||||||
|
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||||
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
const ICAL: &str = r"
|
||||||
|
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
|
||||||
|
";
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(0, ICAL)]
|
||||||
|
#[case(1, include_str!("resources/rfc4791_appb.ics"))]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_import(
|
||||||
|
#[from(test_store_context)]
|
||||||
|
#[future]
|
||||||
|
context: TestStoreContext,
|
||||||
|
#[case] case: usize,
|
||||||
|
#[case] ical: &'static str,
|
||||||
|
) {
|
||||||
|
let context = context.await;
|
||||||
|
let app = get_app(context.clone());
|
||||||
|
|
||||||
|
let (principal, addr_id) = ("user", "calendar");
|
||||||
|
let url = format!("/caldav/principal/{principal}/{addr_id}");
|
||||||
|
|
||||||
|
let request_template = || {
|
||||||
|
Request::builder()
|
||||||
|
.method("IMPORT")
|
||||||
|
.uri(&url)
|
||||||
|
.body(Body::from(ical))
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try without authentication
|
||||||
|
let request = request_template();
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
|
||||||
|
// Try with correct credentials
|
||||||
|
let mut request = request_template();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.extract_string().await;
|
||||||
|
insta::assert_snapshot!(format!("{case}_import_body"), body);
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("GET")
|
||||||
|
.uri(&url)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.extract_string().await;
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => vec![
|
||||||
|
(r"UID:.+", "UID:[UID]")
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
insta::assert_snapshot!(format!("{case}_get_body"), body);
|
||||||
|
});
|
||||||
|
}
|
||||||
196
src/integration_tests/caldav/calendar_report.rs
Normal file
196
src/integration_tests/caldav/calendar_report.rs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
use crate::integration_tests::{ResponseExtractString, get_app};
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::extract::Request;
|
||||||
|
use headers::{Authorization, HeaderMapExt};
|
||||||
|
use http::StatusCode;
|
||||||
|
use rstest::rstest;
|
||||||
|
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||||
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
const ICS_1: &str = include_str!("resources/rfc4791_appb.ics");
|
||||||
|
|
||||||
|
const REPORT_7_8_1: &str = 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>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const REPORT_7_8_2: &str = 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>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const REPORT_7_8_3: &str = 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:expand 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>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const OUTPUT_7_8_3: &str = r#"
|
||||||
|
<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
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DTSTART:20060103T170000
|
||||||
|
DURATION:PT1H
|
||||||
|
RECURRENCE-ID:20060103T170000
|
||||||
|
SUMMARY:Event #2
|
||||||
|
UID:00959BC664CA650E933C892C@example.com
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DTSTART:20060104T190000
|
||||||
|
DURATION:PT1H
|
||||||
|
RECURRENCE-ID:20060104T170000
|
||||||
|
SUMMARY:Event #2 bis
|
||||||
|
UID:00959BC664CA650E933C892C@example.com
|
||||||
|
END:VEVENT
|
||||||
|
END: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
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
|
||||||
|
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
|
||||||
|
DTSTAMP:20060206T001220Z
|
||||||
|
DTSTART:20060104T150000
|
||||||
|
DURATION:PT1H
|
||||||
|
LAST-MODIFIED:20060206T001330Z
|
||||||
|
ORGANIZER:mailto:cyrus@example.com
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:TENTATIVE
|
||||||
|
SUMMARY:Event #3
|
||||||
|
UID:DC6C50A017428C5216A2F1CD@example.com
|
||||||
|
X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
</C:calendar-data>
|
||||||
|
</D:prop>
|
||||||
|
<D:status>HTTP/1.1 200 OK</D:status>
|
||||||
|
</D:propstat>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(0, ICS_1, REPORT_7_8_1)]
|
||||||
|
#[case(1, ICS_1, REPORT_7_8_2)]
|
||||||
|
#[case(2, ICS_1, REPORT_7_8_3)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_report(
|
||||||
|
#[from(test_store_context)]
|
||||||
|
#[future]
|
||||||
|
context: TestStoreContext,
|
||||||
|
#[case] case: usize,
|
||||||
|
#[case] ics: &'static str,
|
||||||
|
#[case] report: &'static str,
|
||||||
|
) {
|
||||||
|
let context = context.await;
|
||||||
|
let app = get_app(context.clone());
|
||||||
|
|
||||||
|
let (principal, addr_id) = ("user", "calendar");
|
||||||
|
let url = format!("/caldav/principal/{principal}/{addr_id}");
|
||||||
|
|
||||||
|
let request_template = || {
|
||||||
|
Request::builder()
|
||||||
|
.method("IMPORT")
|
||||||
|
.uri(&url)
|
||||||
|
.body(Body::from(ics))
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
// Try with correct credentials
|
||||||
|
let mut request = request_template();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("REPORT")
|
||||||
|
.uri(&url)
|
||||||
|
.body(Body::from(report))
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::MULTI_STATUS);
|
||||||
|
let body = response.extract_string().await;
|
||||||
|
insta::assert_snapshot!(format!("{case}_report_body"), body);
|
||||||
|
}
|
||||||
@@ -4,18 +4,21 @@ use axum::extract::Request;
|
|||||||
use headers::{Authorization, HeaderMapExt};
|
use headers::{Authorization, HeaderMapExt};
|
||||||
use http::{HeaderValue, StatusCode};
|
use http::{HeaderValue, StatusCode};
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
mod calendar;
|
mod calendar;
|
||||||
|
mod calendar_import;
|
||||||
|
mod calendar_report;
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_caldav_root(
|
async fn test_caldav_root(
|
||||||
#[from(get_app)]
|
#[from(test_store_context)]
|
||||||
#[future]
|
#[future]
|
||||||
app: axum::Router,
|
context: TestStoreContext,
|
||||||
) {
|
) {
|
||||||
let app = app.await;
|
let app = get_app(context.await);
|
||||||
|
|
||||||
let request_template = || {
|
let request_template = || {
|
||||||
Request::builder()
|
Request::builder()
|
||||||
@@ -53,11 +56,11 @@ async fn test_caldav_root(
|
|||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_caldav_principal(
|
async fn test_caldav_principal(
|
||||||
#[from(get_app)]
|
#[from(test_store_context)]
|
||||||
#[future]
|
#[future]
|
||||||
app: axum::Router,
|
context: TestStoreContext,
|
||||||
) {
|
) {
|
||||||
let app = app.await;
|
let app = get_app(context.await);
|
||||||
|
|
||||||
let request_template = || {
|
let request_template = || {
|
||||||
Request::builder()
|
Request::builder()
|
||||||
|
|||||||
102
src/integration_tests/caldav/resources/rfc4791_appb.ics
Normal file
102
src/integration_tests/caldav/resources/rfc4791_appb.ics
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
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
|
||||||
|
DTSTAMP:20060206T001102Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060102T100000
|
||||||
|
DURATION:PT1H
|
||||||
|
SUMMARY:Event #1
|
||||||
|
Description:Go Steelers!
|
||||||
|
UID:abcd1
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060102T120000
|
||||||
|
DURATION:PT1H
|
||||||
|
RRULE:FREQ=DAILY;COUNT=5
|
||||||
|
SUMMARY:Event #2
|
||||||
|
UID:abcd2
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060104T140000
|
||||||
|
DURATION:PT1H
|
||||||
|
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
|
||||||
|
SUMMARY:Event #2 bis
|
||||||
|
UID:abcd2
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
|
||||||
|
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
|
||||||
|
DTSTAMP:20060206T001220Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060104T100000
|
||||||
|
DURATION:PT1H
|
||||||
|
LAST-MODIFIED:20060206T001330Z
|
||||||
|
ORGANIZER:mailto:cyrus@example.com
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:TENTATIVE
|
||||||
|
SUMMARY:Event #3
|
||||||
|
UID:abcd3
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VTODO
|
||||||
|
DTSTAMP:20060205T235335Z
|
||||||
|
DUE;VALUE=DATE:20060104
|
||||||
|
STATUS:NEEDS-ACTION
|
||||||
|
SUMMARY:Task #1
|
||||||
|
UID:abcd4
|
||||||
|
BEGIN:VALARM
|
||||||
|
ACTION:AUDIO
|
||||||
|
TRIGGER;RELATED=START:-PT10M
|
||||||
|
END:VALARM
|
||||||
|
END:VTODO
|
||||||
|
BEGIN:VTODO
|
||||||
|
DTSTAMP:20060205T235300Z
|
||||||
|
DUE;VALUE=DATE:20060106
|
||||||
|
LAST-MODIFIED:20060205T235308Z
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:NEEDS-ACTION
|
||||||
|
SUMMARY:Task #2
|
||||||
|
UID:abcd5
|
||||||
|
BEGIN:VALARM
|
||||||
|
ACTION:AUDIO
|
||||||
|
TRIGGER;RELATED=START:-PT10M
|
||||||
|
END:VALARM
|
||||||
|
END:VTODO
|
||||||
|
BEGIN:VTODO
|
||||||
|
COMPLETED:20051223T122322Z
|
||||||
|
DTSTAMP:20060205T235400Z
|
||||||
|
DUE;VALUE=DATE:20051225
|
||||||
|
LAST-MODIFIED:20060205T235308Z
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:COMPLETED
|
||||||
|
SUMMARY:Task #3
|
||||||
|
UID:abcd6
|
||||||
|
END:VTODO
|
||||||
|
BEGIN:VTODO
|
||||||
|
DTSTAMP:20060205T235600Z
|
||||||
|
DUE;VALUE=DATE:20060101
|
||||||
|
LAST-MODIFIED:20060205T235308Z
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:CANCELLED
|
||||||
|
SUMMARY:Task #4
|
||||||
|
UID:abcd7
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR
|
||||||
@@ -8,5 +8,5 @@ CALSCALE:GREGORIAN
|
|||||||
PRODID:RustiCal
|
PRODID:RustiCal
|
||||||
X-WR-CALNAME:Calendar
|
X-WR-CALNAME:Calendar
|
||||||
X-WR-CALDESC:Description
|
X-WR-CALDESC:Description
|
||||||
X-WR-TIMEZONE:Europe/Berlin
|
X-WR-TIMEZONE:US/Eastern
|
||||||
END:VCALENDAR
|
END:VCALENDAR
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/caldav/calendar.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
|
<response>
|
||||||
|
<href>/caldav/principal/user/calendar/qwue23489.ics</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<getetag>"aea50382a7775bb9742bfec277382e3a260b6066f503b5f5ae34548d7215ee46"</getetag>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
|
<response>
|
||||||
|
<href>/home/bernard/addressbook/vcf1.vcf</href>
|
||||||
|
<status>HTTP/1.1 404 Not Found</status>
|
||||||
|
</response>
|
||||||
|
</multistatus>
|
||||||
@@ -4,7 +4,7 @@ expression: body
|
|||||||
---
|
---
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<response>
|
||||||
<href>/caldav/principal/user/calendar/</href>
|
<href>/caldav/principal/user/calendar/</href>
|
||||||
<propstat>
|
<propstat>
|
||||||
<prop>
|
<prop>
|
||||||
@@ -14,109 +14,117 @@ expression: body
|
|||||||
PRODID:-//github.com/lennart-k/vzic-rs//RustiCal Calendar server//EN
|
PRODID:-//github.com/lennart-k/vzic-rs//RustiCal Calendar server//EN
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
BEGIN:VTIMEZONE
|
BEGIN:VTIMEZONE
|
||||||
TZID:Europe/Berlin
|
TZID:America/New_York
|
||||||
LAST-MODIFIED:20250723T190331Z
|
LAST-MODIFIED:20250723T190331Z
|
||||||
X-LIC-LOCATION:Europe/Berlin
|
X-LIC-LOCATION:America/New_York
|
||||||
X-PROLEPTIC-TZNAME:LMT
|
X-PROLEPTIC-TZNAME:LMT
|
||||||
BEGIN:STANDARD
|
BEGIN:STANDARD
|
||||||
TZNAME:CET
|
TZNAME:EST
|
||||||
TZOFFSETFROM:+005328
|
TZOFFSETFROM:-045602
|
||||||
TZOFFSETTO:+0100
|
TZOFFSETTO:-0500
|
||||||
DTSTART:18930401T000000
|
DTSTART:18831118T120358
|
||||||
END:STANDARD
|
END:STANDARD
|
||||||
BEGIN:DAYLIGHT
|
BEGIN:DAYLIGHT
|
||||||
TZNAME:CEST
|
TZNAME:EDT
|
||||||
TZOFFSETFROM:+0100
|
TZOFFSETFROM:-0500
|
||||||
TZOFFSETTO:+0200
|
TZOFFSETTO:-0400
|
||||||
DTSTART:19160430T230000
|
DTSTART:19180331T020000
|
||||||
RDATE:19400401T020000
|
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T070000Z
|
||||||
RDATE:19430329T020000
|
|
||||||
RDATE:19460414T020000
|
|
||||||
RDATE:19470406T030000
|
|
||||||
RDATE:19480418T020000
|
|
||||||
RDATE:19490410T020000
|
|
||||||
RDATE:19800406T020000
|
|
||||||
END:DAYLIGHT
|
END:DAYLIGHT
|
||||||
BEGIN:STANDARD
|
BEGIN:STANDARD
|
||||||
TZNAME:CET
|
TZNAME:EST
|
||||||
TZOFFSETFROM:+0200
|
TZOFFSETFROM:-0400
|
||||||
TZOFFSETTO:+0100
|
TZOFFSETTO:-0500
|
||||||
DTSTART:19161001T010000
|
DTSTART:19181027T020000
|
||||||
RDATE:19421102T030000
|
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19201031T060000Z
|
||||||
RDATE:19431004T030000
|
|
||||||
RDATE:19441002T030000
|
|
||||||
RDATE:19451118T030000
|
|
||||||
RDATE:19461007T030000
|
|
||||||
END:STANDARD
|
END:STANDARD
|
||||||
BEGIN:DAYLIGHT
|
BEGIN:DAYLIGHT
|
||||||
TZNAME:CEST
|
TZNAME:EDT
|
||||||
TZOFFSETFROM:+0100
|
TZOFFSETFROM:-0500
|
||||||
TZOFFSETTO:+0200
|
TZOFFSETTO:-0400
|
||||||
DTSTART:19170416T020000
|
DTSTART:19210424T020000
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3MO;UNTIL=19180415T010000Z
|
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19410427T070000Z
|
||||||
END:DAYLIGHT
|
END:DAYLIGHT
|
||||||
BEGIN:STANDARD
|
BEGIN:STANDARD
|
||||||
TZNAME:CET
|
TZNAME:EST
|
||||||
TZOFFSETFROM:+0200
|
TZOFFSETFROM:-0400
|
||||||
TZOFFSETTO:+0100
|
TZOFFSETTO:-0500
|
||||||
DTSTART:19170917T030000
|
DTSTART:19210925T020000
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3MO;UNTIL=19180916T010000Z
|
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19410928T060000Z
|
||||||
END:STANDARD
|
END:STANDARD
|
||||||
BEGIN:DAYLIGHT
|
BEGIN:DAYLIGHT
|
||||||
TZNAME:CEST
|
TZNAME:EWT
|
||||||
TZOFFSETFROM:+0100
|
TZOFFSETFROM:-0500
|
||||||
TZOFFSETTO:+0200
|
TZOFFSETTO:-0400
|
||||||
DTSTART:19440403T020000
|
DTSTART:19420209T020000
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1MO;UNTIL=19450402T010000Z
|
|
||||||
END:DAYLIGHT
|
END:DAYLIGHT
|
||||||
BEGIN:DAYLIGHT
|
BEGIN:DAYLIGHT
|
||||||
TZNAME:CEMT
|
TZNAME:EPT
|
||||||
TZOFFSETFROM:+0200
|
TZOFFSETFROM:-0400
|
||||||
TZOFFSETTO:+0300
|
TZOFFSETTO:-0400
|
||||||
DTSTART:19450524T020000
|
DTSTART:19450814T190000
|
||||||
RDATE:19470511T030000
|
|
||||||
END:DAYLIGHT
|
|
||||||
BEGIN:DAYLIGHT
|
|
||||||
TZNAME:CEST
|
|
||||||
TZOFFSETFROM:+0300
|
|
||||||
TZOFFSETTO:+0200
|
|
||||||
DTSTART:19450924T030000
|
|
||||||
RDATE:19470629T030000
|
|
||||||
END:DAYLIGHT
|
END:DAYLIGHT
|
||||||
BEGIN:STANDARD
|
BEGIN:STANDARD
|
||||||
TZNAME:CET
|
TZNAME:EST
|
||||||
TZOFFSETFROM:+0100
|
TZOFFSETFROM:-0400
|
||||||
TZOFFSETTO:+0100
|
TZOFFSETTO:-0500
|
||||||
DTSTART:19460101T000000
|
DTSTART:19450930T020000
|
||||||
RDATE:19800101T000000
|
|
||||||
END:STANDARD
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+0200
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:19471005T030000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19491002T010000Z
|
|
||||||
END:STANDARD
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+0200
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:19800928T030000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19950924T010000Z
|
|
||||||
END:STANDARD
|
END:STANDARD
|
||||||
BEGIN:DAYLIGHT
|
BEGIN:DAYLIGHT
|
||||||
TZNAME:CEST
|
TZNAME:EDT
|
||||||
TZOFFSETFROM:+0100
|
TZOFFSETFROM:-0500
|
||||||
TZOFFSETTO:+0200
|
TZOFFSETTO:-0400
|
||||||
DTSTART:19810329T020000
|
DTSTART:19460428T020000
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19730429T070000Z
|
||||||
END:DAYLIGHT
|
END:DAYLIGHT
|
||||||
BEGIN:STANDARD
|
BEGIN:STANDARD
|
||||||
TZNAME:CET
|
TZNAME:EST
|
||||||
TZOFFSETFROM:+0200
|
TZOFFSETFROM:-0400
|
||||||
TZOFFSETTO:+0100
|
TZOFFSETTO:-0500
|
||||||
DTSTART:19961027T030000
|
DTSTART:19460929T020000
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19540926T060000Z
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:EST
|
||||||
|
TZOFFSETFROM:-0400
|
||||||
|
TZOFFSETTO:-0500
|
||||||
|
DTSTART:19551030T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=20061029T060000Z
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:EDT
|
||||||
|
TZOFFSETFROM:-0500
|
||||||
|
TZOFFSETTO:-0400
|
||||||
|
DTSTART:19740106T020000
|
||||||
|
RDATE:19750223T020000
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:EDT
|
||||||
|
TZOFFSETFROM:-0500
|
||||||
|
TZOFFSETTO:-0400
|
||||||
|
DTSTART:19760425T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19860427T070000Z
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:EDT
|
||||||
|
TZOFFSETFROM:-0500
|
||||||
|
TZOFFSETTO:-0400
|
||||||
|
DTSTART:19870405T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=20060402T070000Z
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:EDT
|
||||||
|
TZOFFSETFROM:-0500
|
||||||
|
TZOFFSETTO:-0400
|
||||||
|
DTSTART:20070311T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:EST
|
||||||
|
TZOFFSETFROM:-0400
|
||||||
|
TZOFFSETTO:-0500
|
||||||
|
DTSTART:20071104T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||||
END:STANDARD
|
END:STANDARD
|
||||||
END:VTIMEZONE
|
END:VTIMEZONE
|
||||||
END:VCALENDAR
|
END:VCALENDAR
|
||||||
@@ -124,7 +132,7 @@ END:VCALENDAR
|
|||||||
<CAL:timezone-service-set>
|
<CAL:timezone-service-set>
|
||||||
<href>https://www.iana.org/time-zones</href>
|
<href>https://www.iana.org/time-zones</href>
|
||||||
</CAL:timezone-service-set>
|
</CAL:timezone-service-set>
|
||||||
<CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
|
<CAL:calendar-timezone-id>US/Eastern</CAL:calendar-timezone-id>
|
||||||
<calendar-order xmlns="http://apple.com/ns/ical/">0</calendar-order>
|
<calendar-order xmlns="http://apple.com/ns/ical/">0</calendar-order>
|
||||||
<CAL:supported-calendar-component-set>
|
<CAL:supported-calendar-component-set>
|
||||||
<CAL:comp name="VEVENT"/>
|
<CAL:comp name="VEVENT"/>
|
||||||
@@ -136,9 +144,10 @@ END:VCALENDAR
|
|||||||
</CAL:supported-calendar-data>
|
</CAL:supported-calendar-data>
|
||||||
<CAL:supported-collation-set>
|
<CAL:supported-collation-set>
|
||||||
<CAL:supported-collation>i;ascii-casemap</CAL:supported-collation>
|
<CAL:supported-collation>i;ascii-casemap</CAL:supported-collation>
|
||||||
|
<CAL:supported-collation>i;unicode-casemap</CAL:supported-collation>
|
||||||
<CAL:supported-collation>i;octet</CAL:supported-collation>
|
<CAL:supported-collation>i;octet</CAL:supported-collation>
|
||||||
</CAL:supported-collation-set>
|
</CAL:supported-collation-set>
|
||||||
<max-resource-size>10000000</max-resource-size>
|
<CAL:max-resource-size>10000000</CAL:max-resource-size>
|
||||||
<supported-report-set>
|
<supported-report-set>
|
||||||
<supported-report>
|
<supported-report>
|
||||||
<report>
|
<report>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ expression: body
|
|||||||
---
|
---
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<response>
|
||||||
<href>/caldav/principal/user/calendar</href>
|
<href>/caldav/principal/user/calendar</href>
|
||||||
<propstat>
|
<propstat>
|
||||||
<prop>
|
<prop>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/caldav/calendar_import.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
PRODID:RustiCal
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:[UID]
|
||||||
|
SUMMARY:One-off Meeting
|
||||||
|
DTSTAMP:20041210T183904Z
|
||||||
|
DTSTART:20041207T120000Z
|
||||||
|
DTEND:20041207T130000Z
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:[UID]
|
||||||
|
SUMMARY:Weekly Meeting
|
||||||
|
DTSTAMP:20041210T183838Z
|
||||||
|
DTSTART:20041206T120000Z
|
||||||
|
DTEND:20041206T130000Z
|
||||||
|
RRULE:FREQ=WEEKLY
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:[UID]
|
||||||
|
SUMMARY:Weekly Meeting
|
||||||
|
RECURRENCE-ID:20041213T120000Z
|
||||||
|
DTSTAMP:20041210T183838Z
|
||||||
|
DTSTART:20041213T130000Z
|
||||||
|
DTEND:20041213T140000Z
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/caldav/calendar_import.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/caldav/calendar_import.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
PRODID:RustiCal
|
||||||
|
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
|
||||||
|
DTSTAMP:20060206T001102Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060102T100000
|
||||||
|
DURATION:PT1H
|
||||||
|
SUMMARY:Event #1
|
||||||
|
Description:Go Steelers!
|
||||||
|
UID:[UID]
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060102T120000
|
||||||
|
DURATION:PT1H
|
||||||
|
RRULE:FREQ=DAILY;COUNT=5
|
||||||
|
SUMMARY:Event #2
|
||||||
|
UID:[UID]
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060104T140000
|
||||||
|
DURATION:PT1H
|
||||||
|
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
|
||||||
|
SUMMARY:Event #2 bis
|
||||||
|
UID:[UID]
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
|
||||||
|
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
|
||||||
|
DTSTAMP:20060206T001220Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060104T100000
|
||||||
|
DURATION:PT1H
|
||||||
|
LAST-MODIFIED:20060206T001330Z
|
||||||
|
ORGANIZER:mailto:cyrus@example.com
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:TENTATIVE
|
||||||
|
SUMMARY:Event #3
|
||||||
|
UID:[UID]
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VTODO
|
||||||
|
DTSTAMP:20060205T235335Z
|
||||||
|
DUE;VALUE=DATE:20060104
|
||||||
|
STATUS:NEEDS-ACTION
|
||||||
|
SUMMARY:Task #1
|
||||||
|
UID:[UID]
|
||||||
|
BEGIN:VALARM
|
||||||
|
ACTION:AUDIO
|
||||||
|
TRIGGER;RELATED=START:-PT10M
|
||||||
|
END:VALARM
|
||||||
|
END:VTODO
|
||||||
|
BEGIN:VTODO
|
||||||
|
DTSTAMP:20060205T235300Z
|
||||||
|
DUE;VALUE=DATE:20060106
|
||||||
|
LAST-MODIFIED:20060205T235308Z
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:NEEDS-ACTION
|
||||||
|
SUMMARY:Task #2
|
||||||
|
UID:[UID]
|
||||||
|
BEGIN:VALARM
|
||||||
|
ACTION:AUDIO
|
||||||
|
TRIGGER;RELATED=START:-PT10M
|
||||||
|
END:VALARM
|
||||||
|
END:VTODO
|
||||||
|
BEGIN:VTODO
|
||||||
|
COMPLETED:20051223T122322Z
|
||||||
|
DTSTAMP:20060205T235400Z
|
||||||
|
DUE;VALUE=DATE:20051225
|
||||||
|
LAST-MODIFIED:20060205T235308Z
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:COMPLETED
|
||||||
|
SUMMARY:Task #3
|
||||||
|
UID:[UID]
|
||||||
|
END:VTODO
|
||||||
|
BEGIN:VTODO
|
||||||
|
DTSTAMP:20060205T235600Z
|
||||||
|
DUE;VALUE=DATE:20060101
|
||||||
|
LAST-MODIFIED:20060205T235308Z
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:CANCELLED
|
||||||
|
SUMMARY:Task #4
|
||||||
|
UID:[UID]
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/caldav/calendar_import.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/caldav/calendar_import.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
PRODID:RustiCal
|
||||||
|
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
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060104T140000
|
||||||
|
DURATION:PT1H
|
||||||
|
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
|
||||||
|
SUMMARY:Event #2 bis
|
||||||
|
UID:[UID]
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20060206T001102Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060102T100000
|
||||||
|
DURATION:PT1H
|
||||||
|
SUMMARY:Event #1
|
||||||
|
Description:Go Steelers!
|
||||||
|
UID:[UID]
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060102T120000
|
||||||
|
DURATION:PT1H
|
||||||
|
RRULE:FREQ=DAILY;COUNT=5
|
||||||
|
SUMMARY:Event #2
|
||||||
|
UID:[UID]
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
|
||||||
|
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
|
||||||
|
DTSTAMP:20060206T001220Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060104T100000
|
||||||
|
DURATION:PT1H
|
||||||
|
LAST-MODIFIED:20060206T001330Z
|
||||||
|
ORGANIZER:mailto:cyrus@example.com
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:TENTATIVE
|
||||||
|
SUMMARY:Event #3
|
||||||
|
UID:[UID]
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VTODO
|
||||||
|
DTSTAMP:20060205T235335Z
|
||||||
|
DUE;VALUE=DATE:20060104
|
||||||
|
STATUS:NEEDS-ACTION
|
||||||
|
SUMMARY:Task #1
|
||||||
|
UID:[UID]
|
||||||
|
BEGIN:VALARM
|
||||||
|
ACTION:AUDIO
|
||||||
|
TRIGGER;RELATED=START:-PT10M
|
||||||
|
END:VALARM
|
||||||
|
END:VTODO
|
||||||
|
BEGIN:VTODO
|
||||||
|
DTSTAMP:20060205T235300Z
|
||||||
|
DUE;VALUE=DATE:20060106
|
||||||
|
LAST-MODIFIED:20060205T235308Z
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:NEEDS-ACTION
|
||||||
|
SUMMARY:Task #2
|
||||||
|
UID:[UID]
|
||||||
|
BEGIN:VALARM
|
||||||
|
ACTION:AUDIO
|
||||||
|
TRIGGER;RELATED=START:-PT10M
|
||||||
|
END:VALARM
|
||||||
|
END:VTODO
|
||||||
|
BEGIN:VTODO
|
||||||
|
COMPLETED:20051223T122322Z
|
||||||
|
DTSTAMP:20060205T235400Z
|
||||||
|
DUE;VALUE=DATE:20051225
|
||||||
|
LAST-MODIFIED:20060205T235308Z
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:COMPLETED
|
||||||
|
SUMMARY:Task #3
|
||||||
|
UID:[UID]
|
||||||
|
END:VTODO
|
||||||
|
BEGIN:VTODO
|
||||||
|
DTSTAMP:20060205T235600Z
|
||||||
|
DUE;VALUE=DATE:20060101
|
||||||
|
LAST-MODIFIED:20060205T235308Z
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:CANCELLED
|
||||||
|
SUMMARY:Task #4
|
||||||
|
UID:[UID]
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/caldav/calendar_import.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/caldav/calendar_report.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
|
<response>
|
||||||
|
<href>/caldav/principal/user/calendar/abcd2.ics</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<getetag>"7d80077c5655339885a36b6dbe97336767fb85e6b12c94668bcac100ed971fac"</getetag>
|
||||||
|
<CAL:calendar-data>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
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060102T120000
|
||||||
|
DURATION:PT1H
|
||||||
|
RRULE:FREQ=DAILY;COUNT=5
|
||||||
|
SUMMARY:Event #2
|
||||||
|
UID:abcd2
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060104T140000
|
||||||
|
DURATION:PT1H
|
||||||
|
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
|
||||||
|
SUMMARY:Event #2 bis
|
||||||
|
UID:abcd2
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
</CAL:calendar-data>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
|
<response>
|
||||||
|
<href>/caldav/principal/user/calendar/abcd3.ics</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<getetag>"c6a5b1cf6985805686df99e7f2e1cf286567dcb3383fc6fa1b12ce42d3fbc01c"</getetag>
|
||||||
|
<CAL:calendar-data>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:20060206T001220Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060104T100000
|
||||||
|
DURATION:PT1H
|
||||||
|
LAST-MODIFIED:20060206T001330Z
|
||||||
|
ORGANIZER:mailto:cyrus@example.com
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:TENTATIVE
|
||||||
|
SUMMARY:Event #3
|
||||||
|
UID:abcd3
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
</CAL:calendar-data>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
|
</multistatus>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/caldav/calendar_report.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
|
<response>
|
||||||
|
<href>/caldav/principal/user/calendar/abcd2.ics</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<CAL:calendar-data>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
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060102T120000
|
||||||
|
DURATION:PT1H
|
||||||
|
RRULE:FREQ=DAILY;COUNT=5
|
||||||
|
SUMMARY:Event #2
|
||||||
|
UID:abcd2
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060104T140000
|
||||||
|
DURATION:PT1H
|
||||||
|
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
|
||||||
|
SUMMARY:Event #2 bis
|
||||||
|
UID:abcd2
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
</CAL:calendar-data>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
|
<response>
|
||||||
|
<href>/caldav/principal/user/calendar/abcd3.ics</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<CAL:calendar-data>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:20060206T001220Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060104T100000
|
||||||
|
DURATION:PT1H
|
||||||
|
LAST-MODIFIED:20060206T001330Z
|
||||||
|
ORGANIZER:mailto:cyrus@example.com
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:TENTATIVE
|
||||||
|
SUMMARY:Event #3
|
||||||
|
UID:abcd3
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
</CAL:calendar-data>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
|
</multistatus>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/caldav/calendar_report.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
|
<response>
|
||||||
|
<href>/caldav/principal/user/calendar/abcd2.ics</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<CAL:calendar-data>BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DURATION:PT1H
|
||||||
|
SUMMARY:Event #2
|
||||||
|
UID:abcd2
|
||||||
|
RECURRENCE-ID:20060103T170000Z
|
||||||
|
DTSTART:20060103T170000Z
|
||||||
|
END:VEVENT
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20060206T001121Z
|
||||||
|
DURATION:PT1H
|
||||||
|
SUMMARY:Event #2
|
||||||
|
UID:abcd2
|
||||||
|
RECURRENCE-ID:20060104T170000Z
|
||||||
|
DTSTART:20060104T170000Z
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
</CAL:calendar-data>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
|
<response>
|
||||||
|
<href>/caldav/principal/user/calendar/abcd3.ics</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<CAL:calendar-data>BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
|
||||||
|
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
|
||||||
|
DTSTAMP:20060206T001220Z
|
||||||
|
DTSTART;TZID=US/Eastern:20060104T100000
|
||||||
|
DURATION:PT1H
|
||||||
|
LAST-MODIFIED:20060206T001330Z
|
||||||
|
ORGANIZER:mailto:cyrus@example.com
|
||||||
|
SEQUENCE:1
|
||||||
|
STATUS:TENTATIVE
|
||||||
|
SUMMARY:Event #3
|
||||||
|
UID:abcd3
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
</CAL:calendar-data>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
|
</multistatus>
|
||||||
@@ -4,7 +4,7 @@ expression: body
|
|||||||
---
|
---
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<response>
|
||||||
<href>/caldav/</href>
|
<href>/caldav/</href>
|
||||||
<propstat>
|
<propstat>
|
||||||
<prop>
|
<prop>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ expression: body
|
|||||||
---
|
---
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<response>
|
||||||
<href>/caldav/principal/user/</href>
|
<href>/caldav/principal/user/</href>
|
||||||
<propstat>
|
<propstat>
|
||||||
<prop>
|
<prop>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ expression: body
|
|||||||
---
|
---
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<response>
|
||||||
<href>/caldav/principal/user/</href>
|
<href>/caldav/principal/user/</href>
|
||||||
<propstat>
|
<propstat>
|
||||||
<prop>
|
<prop>
|
||||||
@@ -50,192 +50,4 @@ expression: body
|
|||||||
<status>HTTP/1.1 200 OK</status>
|
<status>HTTP/1.1 200 OK</status>
|
||||||
</propstat>
|
</propstat>
|
||||||
</response>
|
</response>
|
||||||
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
|
||||||
<href>/caldav/principal/user/calendar/</href>
|
|
||||||
<propstat>
|
|
||||||
<prop>
|
|
||||||
<calendar-color xmlns="http://apple.com/ns/ical/">#00FF00</calendar-color>
|
|
||||||
<CAL:calendar-description>Description</CAL:calendar-description>
|
|
||||||
<CAL:calendar-timezone>BEGIN:VCALENDAR
|
|
||||||
PRODID:-//github.com/lennart-k/vzic-rs//RustiCal Calendar server//EN
|
|
||||||
VERSION:2.0
|
|
||||||
BEGIN:VTIMEZONE
|
|
||||||
TZID:Europe/Berlin
|
|
||||||
LAST-MODIFIED:20250723T190331Z
|
|
||||||
X-LIC-LOCATION:Europe/Berlin
|
|
||||||
X-PROLEPTIC-TZNAME:LMT
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+005328
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:18930401T000000
|
|
||||||
END:STANDARD
|
|
||||||
BEGIN:DAYLIGHT
|
|
||||||
TZNAME:CEST
|
|
||||||
TZOFFSETFROM:+0100
|
|
||||||
TZOFFSETTO:+0200
|
|
||||||
DTSTART:19160430T230000
|
|
||||||
RDATE:19400401T020000
|
|
||||||
RDATE:19430329T020000
|
|
||||||
RDATE:19460414T020000
|
|
||||||
RDATE:19470406T030000
|
|
||||||
RDATE:19480418T020000
|
|
||||||
RDATE:19490410T020000
|
|
||||||
RDATE:19800406T020000
|
|
||||||
END:DAYLIGHT
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+0200
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:19161001T010000
|
|
||||||
RDATE:19421102T030000
|
|
||||||
RDATE:19431004T030000
|
|
||||||
RDATE:19441002T030000
|
|
||||||
RDATE:19451118T030000
|
|
||||||
RDATE:19461007T030000
|
|
||||||
END:STANDARD
|
|
||||||
BEGIN:DAYLIGHT
|
|
||||||
TZNAME:CEST
|
|
||||||
TZOFFSETFROM:+0100
|
|
||||||
TZOFFSETTO:+0200
|
|
||||||
DTSTART:19170416T020000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3MO;UNTIL=19180415T010000Z
|
|
||||||
END:DAYLIGHT
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+0200
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:19170917T030000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3MO;UNTIL=19180916T010000Z
|
|
||||||
END:STANDARD
|
|
||||||
BEGIN:DAYLIGHT
|
|
||||||
TZNAME:CEST
|
|
||||||
TZOFFSETFROM:+0100
|
|
||||||
TZOFFSETTO:+0200
|
|
||||||
DTSTART:19440403T020000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1MO;UNTIL=19450402T010000Z
|
|
||||||
END:DAYLIGHT
|
|
||||||
BEGIN:DAYLIGHT
|
|
||||||
TZNAME:CEMT
|
|
||||||
TZOFFSETFROM:+0200
|
|
||||||
TZOFFSETTO:+0300
|
|
||||||
DTSTART:19450524T020000
|
|
||||||
RDATE:19470511T030000
|
|
||||||
END:DAYLIGHT
|
|
||||||
BEGIN:DAYLIGHT
|
|
||||||
TZNAME:CEST
|
|
||||||
TZOFFSETFROM:+0300
|
|
||||||
TZOFFSETTO:+0200
|
|
||||||
DTSTART:19450924T030000
|
|
||||||
RDATE:19470629T030000
|
|
||||||
END:DAYLIGHT
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+0100
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:19460101T000000
|
|
||||||
RDATE:19800101T000000
|
|
||||||
END:STANDARD
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+0200
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:19471005T030000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19491002T010000Z
|
|
||||||
END:STANDARD
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+0200
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:19800928T030000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19950924T010000Z
|
|
||||||
END:STANDARD
|
|
||||||
BEGIN:DAYLIGHT
|
|
||||||
TZNAME:CEST
|
|
||||||
TZOFFSETFROM:+0100
|
|
||||||
TZOFFSETTO:+0200
|
|
||||||
DTSTART:19810329T020000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
|
||||||
END:DAYLIGHT
|
|
||||||
BEGIN:STANDARD
|
|
||||||
TZNAME:CET
|
|
||||||
TZOFFSETFROM:+0200
|
|
||||||
TZOFFSETTO:+0100
|
|
||||||
DTSTART:19961027T030000
|
|
||||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
|
||||||
END:STANDARD
|
|
||||||
END:VTIMEZONE
|
|
||||||
END:VCALENDAR
|
|
||||||
</CAL:calendar-timezone>
|
|
||||||
<CAL:timezone-service-set>
|
|
||||||
<href>https://www.iana.org/time-zones</href>
|
|
||||||
</CAL:timezone-service-set>
|
|
||||||
<CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
|
|
||||||
<calendar-order xmlns="http://apple.com/ns/ical/">0</calendar-order>
|
|
||||||
<CAL:supported-calendar-component-set>
|
|
||||||
<CAL:comp name="VEVENT"/>
|
|
||||||
<CAL:comp name="VTODO"/>
|
|
||||||
<CAL:comp name="VJOURNAL"/>
|
|
||||||
</CAL:supported-calendar-component-set>
|
|
||||||
<CAL:supported-calendar-data>
|
|
||||||
<CAL:calendar-data content-type="text/calendar" version="2.0"/>
|
|
||||||
</CAL:supported-calendar-data>
|
|
||||||
<CAL:supported-collation-set>
|
|
||||||
<CAL:supported-collation>i;ascii-casemap</CAL:supported-collation>
|
|
||||||
<CAL:supported-collation>i;octet</CAL:supported-collation>
|
|
||||||
</CAL:supported-collation-set>
|
|
||||||
<max-resource-size>10000000</max-resource-size>
|
|
||||||
<supported-report-set>
|
|
||||||
<supported-report>
|
|
||||||
<report>
|
|
||||||
<CAL:calendar-query/>
|
|
||||||
</report>
|
|
||||||
</supported-report>
|
|
||||||
<supported-report>
|
|
||||||
<report>
|
|
||||||
<CAL:calendar-multiget/>
|
|
||||||
</report>
|
|
||||||
</supported-report>
|
|
||||||
<supported-report>
|
|
||||||
<report>
|
|
||||||
<sync-collection/>
|
|
||||||
</report>
|
|
||||||
</supported-report>
|
|
||||||
</supported-report-set>
|
|
||||||
<CAL:min-date-time>-2621430101T000000Z</CAL:min-date-time>
|
|
||||||
<CAL:max-date-time>+2621421231T235959Z</CAL:max-date-time>
|
|
||||||
<sync-token>github.com/lennart-k/rustical/ns/0</sync-token>
|
|
||||||
<CS:getctag>github.com/lennart-k/rustical/ns/0</CS:getctag>
|
|
||||||
<PUSH:transports>
|
|
||||||
<PUSH:web-push/>
|
|
||||||
</PUSH:transports>
|
|
||||||
<PUSH:topic>[PUSH_TOPIC]</PUSH:topic>
|
|
||||||
<PUSH:supported-triggers>
|
|
||||||
<PUSH:content-update>
|
|
||||||
<depth>1</depth>
|
|
||||||
</PUSH:content-update>
|
|
||||||
<PUSH:property-update>
|
|
||||||
<depth>1</depth>
|
|
||||||
</PUSH:property-update>
|
|
||||||
</PUSH:supported-triggers>
|
|
||||||
<resourcetype>
|
|
||||||
<collection/>
|
|
||||||
<CAL:calendar/>
|
|
||||||
</resourcetype>
|
|
||||||
<displayname>Calendar</displayname>
|
|
||||||
<current-user-principal>
|
|
||||||
<href>/caldav/principal/user/</href>
|
|
||||||
</current-user-principal>
|
|
||||||
<current-user-privilege-set>
|
|
||||||
<privilege>
|
|
||||||
<all/>
|
|
||||||
</privilege>
|
|
||||||
</current-user-privilege-set>
|
|
||||||
<owner>
|
|
||||||
<href>/caldav/principal/user/</href>
|
|
||||||
</owner>
|
|
||||||
</prop>
|
|
||||||
<status>HTTP/1.1 200 OK</status>
|
|
||||||
</propstat>
|
|
||||||
</response>
|
|
||||||
</multistatus>
|
</multistatus>
|
||||||
|
|||||||
447
src/integration_tests/carddav/addressbook.rs
Normal file
447
src/integration_tests/carddav/addressbook.rs
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
use crate::integration_tests::{ResponseExtractString, get_app};
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::extract::Request;
|
||||||
|
use headers::{Authorization, HeaderMapExt};
|
||||||
|
use http::{HeaderValue, StatusCode};
|
||||||
|
use rstest::rstest;
|
||||||
|
use rustical_store::AddressbookStore;
|
||||||
|
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||||
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
fn mkcol_template(displayname: &str, description: &str) -> String {
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
<?xml version='1.0' encoding='UTF-8' ?>
|
||||||
|
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
||||||
|
<set>
|
||||||
|
<prop>
|
||||||
|
<resourcetype>
|
||||||
|
<collection />
|
||||||
|
<CARD:addressbook />
|
||||||
|
</resourcetype>
|
||||||
|
<displayname>{displayname}</displayname>
|
||||||
|
<CARD:addressbook-description>{description}</CARD:addressbook-description>
|
||||||
|
</prop>
|
||||||
|
</set>
|
||||||
|
</mkcol>
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_carddav_addressbook(
|
||||||
|
#[from(test_store_context)]
|
||||||
|
#[future]
|
||||||
|
context: TestStoreContext,
|
||||||
|
) {
|
||||||
|
let context = context.await;
|
||||||
|
let app = get_app(context.clone());
|
||||||
|
let addr_store = context.addr_store;
|
||||||
|
|
||||||
|
let (mut displayname, mut description) = (
|
||||||
|
Some("Contacts".to_owned()),
|
||||||
|
Some("Amazing contacts!".to_owned()),
|
||||||
|
);
|
||||||
|
let (principal, addr_id) = ("user", "contacts");
|
||||||
|
let url = format!("/carddav/principal/{principal}/{addr_id}");
|
||||||
|
|
||||||
|
let request_template = || {
|
||||||
|
Request::builder()
|
||||||
|
.method("MKCOL")
|
||||||
|
.uri(&url)
|
||||||
|
.body(Body::from(mkcol_template(
|
||||||
|
displayname.as_ref().unwrap(),
|
||||||
|
description.as_ref().unwrap(),
|
||||||
|
)))
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try OPTIONS without authentication
|
||||||
|
let request = Request::builder()
|
||||||
|
.method("OPTIONS")
|
||||||
|
.uri(&url)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
insta::assert_debug_snapshot!(response, @r#"
|
||||||
|
Response {
|
||||||
|
status: 200,
|
||||||
|
version: HTTP/1.1,
|
||||||
|
headers: {
|
||||||
|
"dav": "1, 3, access-control, addressbook, webdav-push",
|
||||||
|
"allow": "PROPFIND, PROPPATCH, COPY, MOVE, DELETE, OPTIONS, REPORT, GET, HEAD, POST, MKCOL, IMPORT",
|
||||||
|
},
|
||||||
|
body: Body(
|
||||||
|
UnsyncBoxBody,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
"#);
|
||||||
|
|
||||||
|
// Try without authentication
|
||||||
|
let request = request_template();
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
|
||||||
|
// Try with correct credentials
|
||||||
|
let mut request = request_template();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::CREATED);
|
||||||
|
let body = response.extract_string().await;
|
||||||
|
insta::assert_snapshot!("mkcol_body", body);
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("GET")
|
||||||
|
.uri(&url)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.extract_string().await;
|
||||||
|
insta::assert_snapshot!("get_body", body);
|
||||||
|
|
||||||
|
let saved_addressbook = addr_store
|
||||||
|
.get_addressbook(principal, addr_id, false)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
(saved_addressbook.displayname, saved_addressbook.description),
|
||||||
|
(displayname, description)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("PROPFIND")
|
||||||
|
.uri(&url)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::MULTI_STATUS);
|
||||||
|
let body = response.extract_string().await;
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => vec![
|
||||||
|
(r"<PUSH:topic>[0-9a-f-]+</PUSH:topic>", "<PUSH:topic>[PUSH_TOPIC]</PUSH:topic>")
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
insta::assert_snapshot!("propfind_body", body);
|
||||||
|
});
|
||||||
|
|
||||||
|
let proppatch_request: &str = r#"
|
||||||
|
<propertyupdate xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
||||||
|
<set>
|
||||||
|
<prop>
|
||||||
|
<displayname>New Displayname</displayname>
|
||||||
|
<CARD:addressbook-description>Test</CARD:addressbook-description>
|
||||||
|
</prop>
|
||||||
|
</set>
|
||||||
|
<remove>
|
||||||
|
<prop>
|
||||||
|
<CARD:addressbook-description />
|
||||||
|
</prop>
|
||||||
|
</remove>
|
||||||
|
</propertyupdate>
|
||||||
|
"#;
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("PROPPATCH")
|
||||||
|
.uri(&url)
|
||||||
|
.body(Body::from(proppatch_request))
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::MULTI_STATUS);
|
||||||
|
let body = response.extract_string().await;
|
||||||
|
insta::assert_snapshot!("proppatch_body", body);
|
||||||
|
|
||||||
|
displayname = Some("New Displayname".to_string());
|
||||||
|
description = None;
|
||||||
|
let saved_addressbook = addr_store
|
||||||
|
.get_addressbook(principal, addr_id, false)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
(saved_addressbook.displayname, saved_addressbook.description),
|
||||||
|
(displayname, description)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("DELETE")
|
||||||
|
.uri(&url)
|
||||||
|
.header("X-No-Trashbin", HeaderValue::from_static("1"))
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.extract_string().await;
|
||||||
|
insta::assert_snapshot!("delete_body", body);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
addr_store.get_addressbook(principal, addr_id, false).await,
|
||||||
|
Err(rustical_store::Error::NotFound)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_mkcol_rfc6352_6_3_1_1(
|
||||||
|
#[from(test_store_context)]
|
||||||
|
#[future]
|
||||||
|
context: TestStoreContext,
|
||||||
|
) {
|
||||||
|
let context = context.await;
|
||||||
|
let app = get_app(context.clone());
|
||||||
|
let addr_store = context.addr_store;
|
||||||
|
|
||||||
|
let (displayname, description) = (
|
||||||
|
"Lisa's Contacts".to_owned(),
|
||||||
|
"My primary address book.".to_owned(),
|
||||||
|
);
|
||||||
|
let (principal, addr_id) = ("user", "contacts");
|
||||||
|
let url = format!("/carddav/principal/{principal}/{addr_id}");
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("MKCOL")
|
||||||
|
.uri(&url)
|
||||||
|
.body(Body::from(format!(
|
||||||
|
r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<D:mkcol xmlns:D="DAV:"
|
||||||
|
xmlns:C="urn:ietf:params:xml:ns:carddav">
|
||||||
|
<D:set>
|
||||||
|
<D:prop>
|
||||||
|
<D:resourcetype>
|
||||||
|
<D:collection/>
|
||||||
|
<C:addressbook/>
|
||||||
|
</D:resourcetype>
|
||||||
|
<D:displayname>{displayname}</D:displayname>
|
||||||
|
<C:addressbook-description xml:lang="en"
|
||||||
|
>{description}</C:addressbook-description>
|
||||||
|
</D:prop>
|
||||||
|
</D:set>
|
||||||
|
</D:mkcol>"#
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::CREATED);
|
||||||
|
let body = response.extract_string().await;
|
||||||
|
insta::assert_snapshot!("mkcol_body", body);
|
||||||
|
let saved_addressbook = addr_store
|
||||||
|
.get_addressbook(principal, addr_id, false)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
saved_addressbook.displayname.unwrap(),
|
||||||
|
saved_addressbook.description.unwrap()
|
||||||
|
),
|
||||||
|
(displayname, description)
|
||||||
|
);
|
||||||
|
|
||||||
|
let vcard = r"BEGIN:VCARD
|
||||||
|
VERSION:3.0
|
||||||
|
FN:Cyrus Daboo
|
||||||
|
N:Daboo;Cyrus
|
||||||
|
ADR;TYPE=POSTAL:;2822 Email HQ;Suite 2821;RFCVille;PA;15213;USA
|
||||||
|
EMAIL;TYPE=INTERNET,PREF:cyrus@example.com
|
||||||
|
NICKNAME:me
|
||||||
|
NOTE:Example VCard.
|
||||||
|
ORG:Self Employed
|
||||||
|
TEL;TYPE=WORK,VOICE:412 605 0499
|
||||||
|
TEL;TYPE=FAX:412 605 0705
|
||||||
|
URL:http://www.example.com
|
||||||
|
UID:1234-5678-9000-1
|
||||||
|
END:VCARD
|
||||||
|
";
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("PUT")
|
||||||
|
.uri(format!("{url}/newcard.vcf"))
|
||||||
|
.header("If-None-Match", "*")
|
||||||
|
.header("Content-Type", "text/vcard")
|
||||||
|
.body(Body::from(vcard))
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::CREATED);
|
||||||
|
let etag = response.headers().get("ETag").unwrap();
|
||||||
|
|
||||||
|
// This should overwrite
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("PUT")
|
||||||
|
.uri(format!("{url}/newcard.vcf"))
|
||||||
|
.header("If-None-Match", "\"somearbitraryetag\"")
|
||||||
|
.header("Content-Type", "text/vcard")
|
||||||
|
.body(Body::from(vcard))
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::CREATED);
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("PUT")
|
||||||
|
.uri(format!("{url}/newcard.vcf"))
|
||||||
|
.header("If-None-Match", etag)
|
||||||
|
.header("Content-Type", "text/vcard")
|
||||||
|
.body(Body::from(vcard))
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::CONFLICT);
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("PUT")
|
||||||
|
.uri(format!("{url}/newcard.vcf"))
|
||||||
|
.header("If-None-Match", "*")
|
||||||
|
.header("Content-Type", "text/vcard")
|
||||||
|
.body(Body::from(vcard))
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_rfc6352_8_7_1(
|
||||||
|
#[from(test_store_context)]
|
||||||
|
#[future]
|
||||||
|
context: TestStoreContext,
|
||||||
|
) {
|
||||||
|
let context = context.await;
|
||||||
|
let app = get_app(context.clone());
|
||||||
|
let addr_store = context.addr_store;
|
||||||
|
|
||||||
|
let (displayname, description) = (
|
||||||
|
"Lisa's Contacts".to_owned(),
|
||||||
|
"My primary address book.".to_owned(),
|
||||||
|
);
|
||||||
|
let (principal, addr_id) = ("user", "contacts");
|
||||||
|
let url = format!("/carddav/principal/{principal}/{addr_id}");
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("MKCOL")
|
||||||
|
.uri(&url)
|
||||||
|
.body(Body::from(format!(
|
||||||
|
r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<D:mkcol xmlns:D="DAV:"
|
||||||
|
xmlns:C="urn:ietf:params:xml:ns:carddav">
|
||||||
|
<D:set>
|
||||||
|
<D:prop>
|
||||||
|
<D:resourcetype>
|
||||||
|
<D:collection/>
|
||||||
|
<C:addressbook/>
|
||||||
|
</D:resourcetype>
|
||||||
|
<D:displayname>{displayname}</D:displayname>
|
||||||
|
<C:addressbook-description xml:lang="en"
|
||||||
|
>{description}</C:addressbook-description>
|
||||||
|
</D:prop>
|
||||||
|
</D:set>
|
||||||
|
</D:mkcol>"#
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::CREATED);
|
||||||
|
let body = response.extract_string().await;
|
||||||
|
insta::assert_snapshot!("mkcol_body", body);
|
||||||
|
let saved_addressbook = addr_store
|
||||||
|
.get_addressbook(principal, addr_id, false)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
saved_addressbook.displayname.unwrap(),
|
||||||
|
saved_addressbook.description.unwrap()
|
||||||
|
),
|
||||||
|
(displayname, description)
|
||||||
|
);
|
||||||
|
|
||||||
|
let vcard = r"BEGIN:VCARD
|
||||||
|
VERSION:3.0
|
||||||
|
FN:Cyrus Daboo
|
||||||
|
N:Daboo;Cyrus
|
||||||
|
ADR;TYPE=POSTAL:;2822 Email HQ;Suite 2821;RFCVille;PA;15213;USA
|
||||||
|
EMAIL;TYPE=INTERNET,PREF:cyrus@example.com
|
||||||
|
NICKNAME:me
|
||||||
|
NOTE:Example VCard.
|
||||||
|
ORG:Self Employed
|
||||||
|
TEL;TYPE=WORK,VOICE:412 605 0499
|
||||||
|
TEL;TYPE=FAX:412 605 0705
|
||||||
|
URL:http://www.example.com
|
||||||
|
UID:1234-5678-9000-1
|
||||||
|
END:VCARD
|
||||||
|
";
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("PUT")
|
||||||
|
.uri(format!("{url}/newcard.vcf"))
|
||||||
|
.header("If-None-Match", "*")
|
||||||
|
.header("Content-Type", "text/vcard")
|
||||||
|
.body(Body::from(vcard))
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::CREATED);
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("REPORT")
|
||||||
|
.uri(&url)
|
||||||
|
.header("Depth", "infinity")
|
||||||
|
.header("Content-Type", "text/xml; charset=\"utf-8\"")
|
||||||
|
.body(Body::from(format!(
|
||||||
|
r#"
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<C:addressbook-multiget xmlns:D="DAV:"
|
||||||
|
xmlns:C="urn:ietf:params:xml:ns:carddav">
|
||||||
|
<D:prop>
|
||||||
|
<D:getetag/>
|
||||||
|
<C:address-data>
|
||||||
|
<C:prop name="VERSION"/>
|
||||||
|
<C:prop name="UID"/>
|
||||||
|
<C:prop name="NICKNAME"/>
|
||||||
|
<C:prop name="EMAIL"/>
|
||||||
|
<C:prop name="FN"/>
|
||||||
|
</C:address-data>
|
||||||
|
</D:prop>
|
||||||
|
<D:href>{url}/newcard.vcf</D:href>
|
||||||
|
<D:href>/home/bernard/addressbook/vcf1.vcf</D:href>
|
||||||
|
</C:addressbook-multiget>
|
||||||
|
"#
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::MULTI_STATUS);
|
||||||
|
let body = response.extract_string().await;
|
||||||
|
insta::assert_snapshot!("multiget_body", body);
|
||||||
|
}
|
||||||
73
src/integration_tests/carddav/addressbook_import.rs
Normal file
73
src/integration_tests/carddav/addressbook_import.rs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
use crate::integration_tests::{ResponseExtractString, get_app};
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::extract::Request;
|
||||||
|
use headers::{Authorization, HeaderMapExt};
|
||||||
|
use http::StatusCode;
|
||||||
|
use rstest::rstest;
|
||||||
|
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||||
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_import(
|
||||||
|
#[from(test_store_context)]
|
||||||
|
#[future]
|
||||||
|
context: TestStoreContext,
|
||||||
|
) {
|
||||||
|
let context = context.await;
|
||||||
|
let app = get_app(context.clone());
|
||||||
|
|
||||||
|
let (principal, addr_id) = ("user", "contacts");
|
||||||
|
let url = format!("/carddav/principal/{principal}/{addr_id}");
|
||||||
|
|
||||||
|
let request_template = || {
|
||||||
|
Request::builder()
|
||||||
|
.method("IMPORT")
|
||||||
|
.uri(&url)
|
||||||
|
.body(Body::from(
|
||||||
|
r"BEGIN:VCARD
|
||||||
|
VERSION:4.0
|
||||||
|
FN:Simon Perreault
|
||||||
|
N:Perreault;Simon;;;ing. jr,M.Sc.
|
||||||
|
BDAY:--0203
|
||||||
|
GENDER:M
|
||||||
|
EMAIL;TYPE=work:simon.perreault@viagenie.ca
|
||||||
|
END:VCARD",
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try without authentication
|
||||||
|
let request = request_template();
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
|
||||||
|
// Try with correct credentials
|
||||||
|
let mut request = request_template();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.extract_string().await;
|
||||||
|
insta::assert_snapshot!("import_body", body);
|
||||||
|
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method("GET")
|
||||||
|
.uri(&url)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.typed_insert(Authorization::basic("user", "pass"));
|
||||||
|
let response = app.clone().oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.extract_string().await;
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => vec![
|
||||||
|
(r"UID:.+", "UID:[UID]")
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
insta::assert_snapshot!("get_body", body);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,16 +4,20 @@ use axum::extract::Request;
|
|||||||
use headers::{Authorization, HeaderMapExt};
|
use headers::{Authorization, HeaderMapExt};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
mod addressbook;
|
||||||
|
mod addressbook_import;
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_carddav_root(
|
async fn test_carddav_root(
|
||||||
#[from(get_app)]
|
#[from(test_store_context)]
|
||||||
#[future]
|
#[future]
|
||||||
app: axum::Router,
|
context: TestStoreContext,
|
||||||
) {
|
) {
|
||||||
let app = app.await;
|
let app = get_app(context.await);
|
||||||
|
|
||||||
let request_template = || {
|
let request_template = || {
|
||||||
Request::builder()
|
Request::builder()
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/carddav/addressbook.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/carddav/addressbook.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/carddav/addressbook.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/carddav/addressbook.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
|
<response>
|
||||||
|
<href>/carddav/principal/user/contacts/newcard.vcf</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<getetag>"24835b6c11816c864f9edadd4c7c296234c643892afcbbc5fbf5c9b7ac935cf8"</getetag>
|
||||||
|
<CARD:address-data>BEGIN:VCARD
|
||||||
|
VERSION:3.0
|
||||||
|
FN:Cyrus Daboo
|
||||||
|
N:Daboo;Cyrus
|
||||||
|
ADR;TYPE=POSTAL:;2822 Email HQ;Suite 2821;RFCVille;PA;15213;USA
|
||||||
|
EMAIL;TYPE=INTERNET,PREF:cyrus@example.com
|
||||||
|
NICKNAME:me
|
||||||
|
NOTE:Example VCard.
|
||||||
|
ORG:Self Employed
|
||||||
|
TEL;TYPE=WORK,VOICE:412 605 0499
|
||||||
|
TEL;TYPE=FAX:412 605 0705
|
||||||
|
URL:http://www.example.com
|
||||||
|
UID:1234-5678-9000-1
|
||||||
|
END:VCARD
|
||||||
|
</CARD:address-data>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
|
<response>
|
||||||
|
<href>/home/bernard/addressbook/vcf1.vcf</href>
|
||||||
|
<status>HTTP/1.1 404 Not Found</status>
|
||||||
|
</response>
|
||||||
|
</multistatus>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/carddav/addressbook.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
|
<response>
|
||||||
|
<href>/carddav/principal/user/contacts/</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<CARD:addressbook-description>Amazing contacts!</CARD:addressbook-description>
|
||||||
|
<CARD:supported-address-data>
|
||||||
|
<CARD:address-data-type content-type="text/vcard" version="3.0"/>
|
||||||
|
<CARD:address-data-type content-type="text/vcard" version="4.0"/>
|
||||||
|
</CARD:supported-address-data>
|
||||||
|
<CARD:supported-collation-set>
|
||||||
|
<CARD:supported-collation>i;ascii-casemap</CARD:supported-collation>
|
||||||
|
<CARD:supported-collation>i;unicode-casemap</CARD:supported-collation>
|
||||||
|
<CARD:supported-collation>i;octet</CARD:supported-collation>
|
||||||
|
</CARD:supported-collation-set>
|
||||||
|
<supported-report-set>
|
||||||
|
<supported-report>
|
||||||
|
<report>
|
||||||
|
<CARD:addressbook-multiget/>
|
||||||
|
</report>
|
||||||
|
</supported-report>
|
||||||
|
<supported-report>
|
||||||
|
<report>
|
||||||
|
<sync-collection/>
|
||||||
|
</report>
|
||||||
|
</supported-report>
|
||||||
|
</supported-report-set>
|
||||||
|
<CARD:max-resource-size>10000000</CARD:max-resource-size>
|
||||||
|
<sync-token>github.com/lennart-k/rustical/ns/0</sync-token>
|
||||||
|
<CS:getctag>github.com/lennart-k/rustical/ns/0</CS:getctag>
|
||||||
|
<PUSH:transports>
|
||||||
|
<PUSH:web-push/>
|
||||||
|
</PUSH:transports>
|
||||||
|
<PUSH:topic>[PUSH_TOPIC]</PUSH:topic>
|
||||||
|
<PUSH:supported-triggers>
|
||||||
|
<PUSH:content-update>
|
||||||
|
<depth>1</depth>
|
||||||
|
</PUSH:content-update>
|
||||||
|
<PUSH:property-update>
|
||||||
|
<depth>1</depth>
|
||||||
|
</PUSH:property-update>
|
||||||
|
</PUSH:supported-triggers>
|
||||||
|
<resourcetype>
|
||||||
|
<collection/>
|
||||||
|
<CARD:addressbook/>
|
||||||
|
</resourcetype>
|
||||||
|
<displayname>Contacts</displayname>
|
||||||
|
<current-user-principal>
|
||||||
|
<href>/carddav/principal/user/</href>
|
||||||
|
</current-user-principal>
|
||||||
|
<current-user-privilege-set>
|
||||||
|
<privilege>
|
||||||
|
<all/>
|
||||||
|
</privilege>
|
||||||
|
</current-user-privilege-set>
|
||||||
|
<owner>
|
||||||
|
<href>/carddav/principal/user/</href>
|
||||||
|
</owner>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
|
</multistatus>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/carddav/addressbook.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
|
<response>
|
||||||
|
<href>/carddav/principal/user/contacts</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<displayname xmlns="DAV:"/>
|
||||||
|
<addressbook-description xmlns="urn:ietf:params:xml:ns:carddav"/>
|
||||||
|
<addressbook-description/>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 404 Not Found</status>
|
||||||
|
</propstat>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 409 Conflict</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
|
</multistatus>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/carddav/addressbook_import.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
BEGIN:VCARD
|
||||||
|
VERSION:4.0
|
||||||
|
FN:Simon Perreault
|
||||||
|
N:Perreault;Simon;;;ing. jr,M.Sc.
|
||||||
|
BDAY:--0203
|
||||||
|
GENDER:M
|
||||||
|
EMAIL;TYPE=work:simon.perreault@viagenie.ca
|
||||||
|
UID:[UID]
|
||||||
|
END:VCARD
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: src/integration_tests/carddav/addressbook_import.rs
|
||||||
|
expression: body
|
||||||
|
---
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ expression: body
|
|||||||
---
|
---
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
<response>
|
||||||
<href>/carddav/</href>
|
<href>/carddav/</href>
|
||||||
<propstat>
|
<propstat>
|
||||||
<prop>
|
<prop>
|
||||||
|
|||||||
@@ -3,44 +3,24 @@ use axum::extract::Request;
|
|||||||
use axum::{body::Body, response::Response};
|
use axum::{body::Body, response::Response};
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
use rustical_frontend::FrontendConfig;
|
use rustical_frontend::FrontendConfig;
|
||||||
use rustical_store_sqlite::{
|
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
|
||||||
SqliteStore,
|
|
||||||
addressbook_store::SqliteAddressbookStore,
|
|
||||||
calendar_store::SqliteCalendarStore,
|
|
||||||
principal_store::SqlitePrincipalStore,
|
|
||||||
tests::{
|
|
||||||
get_test_addressbook_store, get_test_calendar_store, get_test_principal_store,
|
|
||||||
get_test_subscription_store,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
#[rstest::fixture]
|
pub fn get_app(context: TestStoreContext) -> axum::Router {
|
||||||
pub async fn get_app(
|
let TestStoreContext {
|
||||||
#[from(get_test_calendar_store)]
|
|
||||||
#[future]
|
|
||||||
cal_store: SqliteCalendarStore,
|
|
||||||
#[from(get_test_addressbook_store)]
|
|
||||||
#[future]
|
|
||||||
addr_store: SqliteAddressbookStore,
|
|
||||||
#[from(get_test_principal_store)]
|
|
||||||
#[future]
|
|
||||||
principal_store: SqlitePrincipalStore,
|
|
||||||
#[from(get_test_subscription_store)]
|
|
||||||
#[future]
|
|
||||||
sub_store: SqliteStore,
|
|
||||||
) -> axum::Router {
|
|
||||||
let addr_store = Arc::new(addr_store.await);
|
|
||||||
let cal_store = Arc::new(cal_store.await);
|
|
||||||
let sub_store = Arc::new(sub_store.await);
|
|
||||||
let principal_store = Arc::new(principal_store.await);
|
|
||||||
|
|
||||||
make_app(
|
|
||||||
addr_store,
|
addr_store,
|
||||||
cal_store,
|
cal_store,
|
||||||
sub_store,
|
|
||||||
principal_store,
|
principal_store,
|
||||||
|
sub_store,
|
||||||
|
..
|
||||||
|
} = context;
|
||||||
|
|
||||||
|
make_app(
|
||||||
|
Arc::new(addr_store),
|
||||||
|
Arc::new(cal_store),
|
||||||
|
Arc::new(sub_store),
|
||||||
|
Arc::new(principal_store),
|
||||||
FrontendConfig {
|
FrontendConfig {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
allow_password_login: true,
|
allow_password_login: true,
|
||||||
@@ -70,11 +50,11 @@ impl ResponseExtractString for Response {
|
|||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_ping(
|
async fn test_ping(
|
||||||
#[from(get_app)]
|
#[from(test_store_context)]
|
||||||
#[future]
|
#[future]
|
||||||
app: axum::Router,
|
context: TestStoreContext,
|
||||||
) {
|
) {
|
||||||
let app = app.await;
|
let app = get_app(context.await);
|
||||||
|
|
||||||
let response = app
|
let response = app
|
||||||
.oneshot(Request::builder().uri("/ping").body(Body::empty()).unwrap())
|
.oneshot(Request::builder().uri("/ping").body(Body::empty()).unwrap())
|
||||||
|
|||||||
11
src/main.rs
11
src/main.rs
@@ -25,7 +25,7 @@ use std::sync::Arc;
|
|||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::Receiver;
|
||||||
use tower::Layer;
|
use tower::Layer;
|
||||||
use tower_http::normalize_path::NormalizePathLayer;
|
use tower_http::normalize_path::NormalizePathLayer;
|
||||||
use tracing::info;
|
use tracing::{info, warn};
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod commands;
|
mod commands;
|
||||||
@@ -34,6 +34,9 @@ mod config;
|
|||||||
pub mod integration_tests;
|
pub mod integration_tests;
|
||||||
mod setup_tracing;
|
mod setup_tracing;
|
||||||
|
|
||||||
|
mod migration_0_12;
|
||||||
|
use migration_0_12::{validate_address_objects_0_12, validate_calendar_objects_0_12};
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
struct Args {
|
struct Args {
|
||||||
@@ -115,6 +118,12 @@ async fn main() -> Result<()> {
|
|||||||
let (addr_store, cal_store, subscription_store, principal_store, update_recv) =
|
let (addr_store, cal_store, subscription_store, principal_store, update_recv) =
|
||||||
get_data_stores(!args.no_migrations, &config.data_store).await?;
|
get_data_stores(!args.no_migrations, &config.data_store).await?;
|
||||||
|
|
||||||
|
warn!(
|
||||||
|
"Validating calendar data against the next-version ical parser.\nIn the next major release these will be rejected and cause errors.\nIf any errors occur, please open an issue so they can be fixed before the next major release."
|
||||||
|
);
|
||||||
|
validate_calendar_objects_0_12(principal_store.as_ref(), cal_store.as_ref()).await?;
|
||||||
|
validate_address_objects_0_12(principal_store.as_ref(), addr_store.as_ref()).await?;
|
||||||
|
|
||||||
let mut tasks = vec![];
|
let mut tasks = vec![];
|
||||||
|
|
||||||
if config.dav_push.enabled {
|
if config.dav_push.enabled {
|
||||||
|
|||||||
80
src/migration_0_12.rs
Normal file
80
src/migration_0_12.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
use rustical_store::{AddressbookStore, CalendarStore, auth::AuthenticationProvider};
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
pub async fn validate_calendar_objects_0_12(
|
||||||
|
principal_store: &impl AuthenticationProvider,
|
||||||
|
cal_store: &impl CalendarStore,
|
||||||
|
) -> Result<(), rustical_store::Error> {
|
||||||
|
let mut success = true;
|
||||||
|
for principal in principal_store.get_principals().await? {
|
||||||
|
for calendar in cal_store.get_calendars(&principal.id).await? {
|
||||||
|
for object in cal_store
|
||||||
|
.get_objects(&calendar.principal, &calendar.id)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
if let Err(err) =
|
||||||
|
ical_dev::parser::ical::IcalObjectParser::new(object.get_ics().as_bytes())
|
||||||
|
.expect_one()
|
||||||
|
{
|
||||||
|
success = false;
|
||||||
|
error!(
|
||||||
|
"An error occured parsing a calendar object: principal={principal}, calendar={calendar}, object_id={object_id}: {err}",
|
||||||
|
principal = principal.id,
|
||||||
|
calendar = calendar.id,
|
||||||
|
object_id = object.get_id()
|
||||||
|
);
|
||||||
|
println!("{}", object.get_ics());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if success {
|
||||||
|
info!("Your calendar data seems to be valid in the next major version.");
|
||||||
|
} else {
|
||||||
|
error!(
|
||||||
|
"Not all calendar objects will be successfully parsed in the next major version (v0.12).
|
||||||
|
This will not cause issues in this version, but please comment under the tracking issue on GitHub:
|
||||||
|
https://github.com/lennart-k/rustical/issues/165"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn validate_address_objects_0_12(
|
||||||
|
principal_store: &impl AuthenticationProvider,
|
||||||
|
addr_store: &impl AddressbookStore,
|
||||||
|
) -> Result<(), rustical_store::Error> {
|
||||||
|
let mut success = true;
|
||||||
|
for principal in principal_store.get_principals().await? {
|
||||||
|
for addressbook in addr_store.get_addressbooks(&principal.id).await? {
|
||||||
|
for object in addr_store
|
||||||
|
.get_objects(&addressbook.principal, &addressbook.id)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
if let Err(err) =
|
||||||
|
ical_dev::parser::vcard::VcardParser::new(object.get_vcf().as_bytes())
|
||||||
|
.expect_one()
|
||||||
|
{
|
||||||
|
success = false;
|
||||||
|
error!(
|
||||||
|
"An error occured parsing an address object: principal={principal}, addressbook={addressbook}, object_id={object_id}: {err}",
|
||||||
|
principal = principal.id,
|
||||||
|
addressbook = addressbook.id,
|
||||||
|
object_id = object.get_id()
|
||||||
|
);
|
||||||
|
println!("{}", object.get_vcf());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if success {
|
||||||
|
info!("Your addressbook data seems to be valid in the next major version.");
|
||||||
|
} else {
|
||||||
|
error!(
|
||||||
|
"Not all address objects will be successfully parsed in the next major version (v0.12).
|
||||||
|
This will not cause issues in this version, but please comment under the tracking issue on GitHub:
|
||||||
|
https://github.com/lennart-k/rustical/issues/165"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user