Merge upstream v1.0.0 #215

Closed
jcoffey-dev wants to merge 5 commits from merge/upstream-v1.0.0 into next
pull from: merge/upstream-v1.0.0
2142 changed files with 448746 additions and 110122 deletions

No files matched your search

+5
View File
@@ -1,2 +1,7 @@
[env]
RUST_MIN_STACK = "8388608"
CXXFLAGS_aarch64_unknown_linux_gnu = "-march=armv8-a+crc+crypto -DROCKSDB_AUXV_GETAUXVAL_PRESENT"
CXXFLAGS_aarch64_unknown_linux_musl = "-march=armv8-a+crc+crypto -DROCKSDB_AUXV_GETAUXVAL_PRESENT"
[target.'cfg(all(target_arch = "x86_64", not(target_env = "musl")))']
rustflags = ["-C", "target-cpu=x86-64-v2"]
-1
View File
@@ -3,7 +3,6 @@
# Allow what is needed
!crates
!tests
!resources
# The patched dependency Cargo.toml's [patch.crates-io] points at. Without
+4
View File
@@ -88,7 +88,11 @@ jobs:
- run: cargo build -p inbuxa --locked
# --no-run: the workflow compiled every test target without running them,
# which catches a test that no longer builds without paying for the suite.
# The test helpers sit behind `--cfg inbuxa_tests`, never set for a
# release build; the flag rebuilds the workspace once more.
- run: cargo test --workspace --locked --no-run
env:
RUSTFLAGS: --cfg inbuxa_tests
# The release profile, on main only. It is the profile the image is
# built with, and it fails in ways the dev profile does not: v2026.9.24
# was tagged on a commit whose CI was green and whose release build
+30 -10
View File
@@ -115,39 +115,48 @@ jobs:
matrix:
include:
- target: x86_64-unknown-linux-gnu
runner: ubuntu-latest
platform: linux/amd64
suffix: ""
build_env: ""
- target: x86_64-unknown-linux-musl
runner: ubuntu-latest
platform: linux/amd64
suffix: "-alpine"
build_env: ""
- target: aarch64-unknown-linux-gnu
runner: ubuntu-24.04-arm
platform: linux/arm64
suffix: ""
build_env: "JEMALLOC_SYS_WITH_LG_PAGE=16 "
- target: aarch64-unknown-linux-musl
runner: ubuntu-24.04-arm
platform: linux/arm64
suffix: "-alpine"
build_env: "JEMALLOC_SYS_WITH_LG_PAGE=16 "
- target: armv7-unknown-linux-gnueabihf
runner: ubuntu-latest
platform: linux/arm/v7
suffix: ""
build_env: "JEMALLOC_SYS_WITH_LG_PAGE=16 "
- target: armv7-unknown-linux-musleabihf
runner: ubuntu-latest
platform: linux/arm/v7
suffix: "-alpine"
build_env: "JEMALLOC_SYS_WITH_LG_PAGE=16 "
- target: arm-unknown-linux-gnueabihf
runner: ubuntu-latest
platform: linux/arm/v6
suffix: ""
build_env: ""
- target: arm-unknown-linux-musleabihf
runner: ubuntu-latest
platform: linux/arm/v6
suffix: "-alpine"
build_env: ""
name: Build / ${{matrix.target}}
runs-on: ubuntu-latest
runs-on: ${{matrix.runner}}
timeout-minutes: 180
steps:
- name: Checkout
uses: actions/checkout@v7
@@ -163,23 +172,31 @@ jobs:
- name: Add swap (heavy ARM targets)
if: contains(matrix.target, 'arm') || contains(matrix.target, 'aarch64')
run: |
mnt_avail=$(df --output=avail -k /mnt | tail -1)
if [ "$mnt_avail" -lt 18874368 ]; then
echo "Insufficient space on /mnt (${mnt_avail}K available), aborting swap setup"
swap_dir=""
for dir in /mnt /; do
if [ -d "$dir" ] && [ "$(df --output=avail -k "$dir" | tail -1)" -ge 18874368 ]; then
swap_dir="$dir"
break
fi
done
if [ -z "$swap_dir" ]; then
echo "Insufficient space for swap on /mnt or /, aborting swap setup"
df -h
exit 1
fi
sudo fallocate -l 16G /mnt/swapfile
sudo chmod 600 /mnt/swapfile
sudo mkswap /mnt/swapfile
sudo swapon /mnt/swapfile
sudo fallocate -l 16G "${swap_dir%/}/swapfile"
sudo chmod 600 "${swap_dir%/}/swapfile"
sudo mkswap "${swap_dir%/}/swapfile"
sudo swapon "${swap_dir%/}/swapfile"
sudo sysctl vm.swappiness=80
free -h
swapon --show
- name: Set up QEMU
if: startsWith(matrix.platform, 'linux/arm/')
uses: docker/setup-qemu-action@v4
with:
platforms: "arm64,arm"
platforms: "arm"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
@@ -198,6 +215,7 @@ jobs:
password: ${{github.token}}
- name: Log In to DockerHub
if: github.event_name == 'push' || inputs.Docker
uses: docker/login-action@v4
with:
username: ${{secrets.DOCKERHUB_USERNAME}}
@@ -215,7 +233,7 @@ jobs:
path: |
var-cache-apt
var-lib-apt
key: apt-cache-${{ hashFiles('Dockerfile.build') }}
key: apt-cache-${{ runner.arch }}-${{ hashFiles('Dockerfile.build') }}
- name: Cache Cargo
uses: actions/[email protected]
@@ -252,12 +270,14 @@ jobs:
type=ref,event=branch,prefix=branch-
type=edge,branch=main
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}}
- name: Build Artifact
id: bake
uses: docker/bake-action@v7
env:
DOCKER_BUILD_RECORD_UPLOAD: false
GITHUB_TOKEN: ${{github.token}}
TARGET: ${{matrix.target}}
GHCR_REPO: ghcr.io/${{github.repository}}
BUILD_ENV: ${{matrix.build_env}}
-57
View File
@@ -1,57 +0,0 @@
name: Test
on:
workflow_dispatch:
jobs:
style:
name: Check Style
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Check Style
run: cargo fmt --all --check
test:
name: Test
needs: style
runs-on: ubuntu-latest
env:
STORE: RocksDb
RUST_MIN_STACK: "16777216"
steps:
- name: Checkout
uses: actions/checkout@v7
# External services (OpenLDAP, Keycloak, PostgreSQL, MySQL, Redis, NATS,
# MinIO, OpenSearch, Meilisearch) are provisioned on demand by the test
# suite via testcontainers using the Docker daemon available on the
# runner; see tests/src/utils/containers.rs.
- name: Rust Cache
uses: Swatinem/rust-cache@v2
- name: JMAP Protocol Tests
run: cargo test -p jmap_proto -- --nocapture
- name: IMAP Protocol Tests
run: cargo test -p imap_proto -- --nocapture
- name: Full-text search Tests
run: cargo test -p store -- --nocapture
- name: Directory Tests
run: cargo test -p tests directory -- --nocapture
- name: SMTP Tests
run: cargo test -p tests smtp -- --nocapture
- name: IMAP Tests
run: cargo test -p tests imap -- --nocapture
- name: JMAP Tests
run: cargo test -p tests jmap -- --nocapture
+2
View File
@@ -19,3 +19,5 @@ tools/fork/cutover-rehearsal/__pycache__/
tools/fork/cutover-rehearsal/before.json
tools/fork/cutover-rehearsal/phase2_notes.json
tools/fork/__pycache__/
tools/registry-gen/__pycache__/
tools/registry-gen/out/
+55
View File
@@ -2,6 +2,61 @@
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
## [1.0.0] - 2026-10-26
This version includes **multiple breaking changes** and a new database layout, so existing v0.16.x deployments cannot be upgraded in place. Please read the [migration documentation](https://stalw.art/docs/migration/upgrade-0-16/) for more information on how to migrate from v0.16.x.
## Added
- Full-text search:
- Text extraction and indexing of PDF, Microsoft Office (`docx`, `xlsx`, `pptx`), OpenDocument (`odt`, `ods`, `odp`), EPUB and RTF documents found in attachments and files.
- Full-text search of file storage contents.
- Index queue with per-partition status, concurrency control, retries and index rebuilds.
- Metadata:
- JMAP Object Metadata ([draft-ietf-jmap-metadata-04](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-metadata-04)) for e-mails, mailboxes, Sieve scripts, calendars, events, address books, contacts and files.
- IMAP `METADATA` extension ([RFC 5464](https://www.rfc-editor.org/rfc/rfc5464.html)).
- DKIM2:
- Post-quantum composite signatures using `ML-DSA-44` and `Ed25519` with `SHA-512`.
- Configurable `Message-Instance` hash algorithms (`SHA-256` and `SHA-512`).
- OAuth and OIDC:
- OAuth Client ID Metadata Documents ([draft-ietf-oauth-client-id-metadata-document-02](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-02)).
- OAuth scopes (`mail`, `contacts`, `calendars` and `files`) per client and grant.
- Connected applications: users can list and revoke the applications authorized to access their account.
- First-party clients, post-logout redirect URIs and per-client grant types.
- External OIDC directories: audience matching, opaque token validation via UserInfo or token introspection and a policy for unscoped tokens.
- Caching:
- Redesigned cache subsystem with a dedicated tier for large accounts.
- Cache snapshots can be swapped to local disk, Redis or the blob store.
- Built-in local in-memory store for single-node deployments.
- Azure Blob Storage authentication using shared keys, SAS tokens, client secrets, managed identities or workload identities.
- Secondary LDAP servers for failover.
- SSRF protection for outbound HTTP requests (webhooks, OIDC discovery, AI and DNS providers).
- Configurable client IP source (`X-Forwarded-For` or `Forwarded`) and number of trusted proxy hops.
- Memory budget for buffered data.
- Configurable limits for e-mail headers, keywords and mailboxes per message, calendars, address books and spam filter text processing.
- TLS options for Kafka and NATS cluster coordinators.
- Automatic cleanup of per-user data after a share is revoked.
- AI (*Enterprise*):
- AI models from multiple providers: OpenAI-compatible endpoints (OpenAI, Azure, vLLM, llama.cpp), Anthropic, Google Gemini and Ollama, with per-model rate limits, concurrency caps, timeouts and reasoning levels.
- AI spam classifier with structured verdicts, model tiers with escalation, training from AI verdicts and optional web search.
## Changed
- New database layout: the key layout was redesigned and the database schema version is now tied to the edition (AGPL-only or AGPL/SEL). Data must be migrated from v0.16.x using the migration tool.
- Settings schema: multiple objects, fields and enumeration values were renamed, restructured (AI models, OIDC directories, Azure Blob Storage, HTTP forwarded headers) or removed, and internal identifiers were renumbered. See [SCHEMA_CHANGES.md](https://github.com/stalwartlabs/stalwart-migrate/blob/main/SCHEMA_CHANGES.md) for the full list.
- Telemetry: New events and metrics were added and event identifiers were renumbered. See [SCHEMA_CHANGES.md](https://github.com/stalwartlabs/stalwart-migrate/blob/main/SCHEMA_CHANGES.md).
- OAuth: The [OAuth Profile for Open Public Clients](https://datatracker.ietf.org/doc/draft-ietf-mailmaint-oauth-public/) is now fully enforced. If you use an external OIDC provider, make sure it conforms to the draft; otherwise mail clients such as Apple Mail will stop working with it as they adopt the profile. Ask your provider to add support, or, at your own risk, relax the corresponding security checks.
- Updated IETF drafts:
- DKIM2 to [draft-ietf-dkim-dkim2-spec-07](https://datatracker.ietf.org/doc/html/draft-ietf-dkim-dkim2-spec-07).
- JMAP for Calendars to [draft-ietf-jmap-calendars-33](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-calendars-33).
- JMAP for File Storage to [draft-ietf-jmap-filenode-18](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-filenode-18).
- JSCalendar to [draft-ietf-calext-jscalendarbis-22](https://datatracker.ietf.org/doc/html/draft-ietf-calext-jscalendarbis-22) and [draft-ietf-calext-jscalendar-icalendar-28](https://datatracker.ietf.org/doc/html/draft-ietf-calext-jscalendar-icalendar-28).
- iCalendar tasks to [draft-ietf-calext-ical-tasks-17](https://datatracker.ietf.org/doc/html/draft-ietf-calext-ical-tasks-17).
- IMAP object identifiers to [draft-ietf-mailmaint-imap-objectid-bis-08](https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-imap-objectid-bis-08).
- Performance improvements in storage backends, expression evaluation, IMAP parsing, spam filter tokenization and search.
- Development: Test suites (unit, integration, conformance and stress) are now kept in a private repository, and new features are developed privately before release. The source code of every release remains public under the AGPL-3.0. [Learn why](https://stalw.art/blog/protecting-stalwart/)
## Fixed
- Multiple bug and security fixes.
## [0.16.27] - 2026-10-18
If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions.
+8 -1
View File
@@ -83,7 +83,14 @@ that is missing from there on every import.
## Running the tests
`cargo test -p tests` runs what needs nothing but a store on disk. The rest
The test helpers are compiled in only with `--cfg inbuxa_tests`, which a
release build never sets, so every test command here runs with it:
```sh
export RUSTFLAGS="--cfg inbuxa_tests"
```
`cargo test -p tests` then runs what needs nothing but a store on disk. The rest
need containers, a particular backend, or a copy of real data, and are
`#[ignore]`d:
Generated
+1190 -742
View File
File diff suppressed because it is too large. Load diff
+64 -10
View File
@@ -30,10 +30,57 @@ members = [
"crates/utils",
"crates/common",
"crates/trc",
"crates/migration",
"crates/text-extract",
"crates/smtp-proto",
"crates/smtp-report",
"crates/calcard",
"crates/sieve",
"crates/jmap-tools",
"crates/s3",
"crates/features",
"tests",
]
default-members = [
"crates/main",
"crates/types",
"crates/http",
"crates/http-proto",
"crates/jmap",
"crates/jmap-proto",
"crates/email",
"crates/imap",
"crates/imap-proto",
"crates/smtp",
"crates/managesieve",
"crates/pop3",
"crates/dav-proto",
"crates/scim-proto",
"crates/scim",
"crates/dav",
"crates/groupware",
"crates/spam-filter",
"crates/nlp",
"crates/store",
"crates/coordinator",
"crates/directory",
"crates/registry",
"crates/services",
"crates/utils",
"crates/common",
"crates/trc",
"crates/text-extract",
"crates/smtp-proto",
"crates/smtp-report",
"crates/calcard",
"crates/sieve",
"crates/jmap-tools",
]
[workspace.dependencies]
rkyv = { version = "0.8.18", features = ["little_endian", "unaligned"] }
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(inbuxa_tests)'] }
[workspace.lints.clippy]
result_unit_err = "allow"
@@ -49,6 +96,12 @@ debug-assertions = true
overflow-checks = false
rpath = false
[profile.dev.package.argon2]
opt-level = 3
[profile.dev.package.blake2]
opt-level = 3
[profile.release]
opt-level = 3
debug = false
@@ -81,16 +134,17 @@ debug-assertions = false
overflow-checks = false
rpath = false
# inbuxa: sieve-rs spells upstream's name into its Sieve extension names
# (vnd.stalwart.*), which scripts `require` and ManageSieve advertises.
# vendor/sieve-rs is the published 0.7.3 with those renamed; see its
# VENDORED.md. Re-vendor when the version in Cargo.lock moves.
#
# inbuxa: hickory is upstream's patched branch (DNSSEC fixes), vendored at
# the commit upstream pins so builds never fetch from upstream's
# repositories; see vendor/hickory-dns/VENDORED.md.
# inbuxa: hickory and opentelemetry are upstream's patched branches, vendored
# at the commits upstream pins so builds never fetch from upstream's
# repositories; see vendor/hickory-dns/VENDORED.md and
# vendor/opentelemetry-rust/VENDORED.md.
[patch.crates-io]
sieve-rs = { path = "vendor/sieve-rs" }
hickory-proto = { path = "vendor/hickory-dns/hickory-proto" }
hickory-net = { path = "vendor/hickory-dns/hickory-net" }
hickory-resolver = { path = "vendor/hickory-dns/hickory-resolver" }
opentelemetry = { path = "vendor/opentelemetry-rust/opentelemetry" }
opentelemetry_sdk = { path = "vendor/opentelemetry-rust/opentelemetry_sdk" }
opentelemetry-otlp = { path = "vendor/opentelemetry-rust/opentelemetry-otlp" }
opentelemetry-proto = { path = "vendor/opentelemetry-rust/opentelemetry-proto" }
opentelemetry-http = { path = "vendor/opentelemetry-rust/opentelemetry-http" }
opentelemetry-semantic-conventions = { path = "vendor/opentelemetry-rust/opentelemetry-semantic-conventions" }
+1 -1
View File
@@ -48,6 +48,6 @@ VOLUME ["/etc/inbuxa", "/var/lib/inbuxa"]
EXPOSE 443 25 110 587 465 143 993 995 4190 8080
ENV INBUXA_HEALTHCHECK_URL=https://127.0.0.1:443/healthz/live
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD curl -fsSk -H "X-Forwarded-For: 127.0.0.1" "$INBUXA_HEALTHCHECK_URL" || curl -fsS -H "X-Forwarded-For: 127.0.0.1" http://127.0.0.1:8080/healthz/live || exit 1
CMD curl -fsSk "$INBUXA_HEALTHCHECK_URL" || curl -fsS http://127.0.0.1:8080/healthz/live || exit 1
ENTRYPOINT ["/usr/local/bin/inbuxa"]
CMD ["--config", "/etc/inbuxa/config.json"]
+18 -2
View File
@@ -79,6 +79,22 @@ RUN \
echo "export ${BUILD_ENV}" >> /env-cargo; \
echo "Setting up ${BUILD_ENV}"; \
fi && \
if [[ "${TARGET}" == arm*-*eabihf ]]; then \
apt-get update && \
apt-get install -yq --no-install-recommends cmake && \
cargo binstall --no-confirm [email protected] && \
case "${TARGET}" in \
*-gnueabihf) zig_target="arm-linux-gnueabihf.${GLIBC_VERSION}" ;; \
*-musleabihf) zig_target="arm-linux-musleabihf" ;; \
esac && \
includes="$(zig cc -target "${zig_target}" -E -v -x c /dev/null -o /dev/null 2>&1 \
| sed -n '/<...> search starts here/,/End of search/p' \
| grep '^ ' | grep -v '/lib/include$' \
| sed 's/^ */-isystem /' | tr '\n' ' ')" && \
[ -n "${includes}" ] && \
target_var="${TARGET//-/_}" && \
echo "export BINDGEN_EXTRA_CLANG_ARGS_${target_var}='${includes}'" >> /env-cargo; \
fi && \
if [[ "${TARGET}" == *gnu ]]; then \
base_arch="${TARGET%%-*}"; \
case "$base_arch" in \
@@ -182,7 +198,7 @@ VOLUME ["/etc/inbuxa", "/var/lib/inbuxa"]
EXPOSE 443 25 110 587 465 143 993 995 4190 8080
ENV INBUXA_HEALTHCHECK_URL=https://127.0.0.1:443/healthz/live
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD curl -fsSk -H "X-Forwarded-For: 127.0.0.1" "$INBUXA_HEALTHCHECK_URL" || curl -fsS -H "X-Forwarded-For: 127.0.0.1" http://127.0.0.1:8080/healthz/live || exit 1
CMD curl -fsSk "$INBUXA_HEALTHCHECK_URL" || curl -fsS http://127.0.0.1:8080/healthz/live || exit 1
ENTRYPOINT ["/usr/local/bin/inbuxa"]
CMD ["--config", "/etc/inbuxa/config.json"]
@@ -204,6 +220,6 @@ VOLUME ["/etc/inbuxa", "/var/lib/inbuxa"]
EXPOSE 443 25 110 587 465 143 993 995 4190 8080
ENV INBUXA_HEALTHCHECK_URL=https://127.0.0.1:443/healthz/live
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD curl -fsSk -H "X-Forwarded-For: 127.0.0.1" "$INBUXA_HEALTHCHECK_URL" || curl -fsS -H "X-Forwarded-For: 127.0.0.1" http://127.0.0.1:8080/healthz/live || exit 1
CMD curl -fsSk "$INBUXA_HEALTHCHECK_URL" || curl -fsS http://127.0.0.1:8080/healthz/live || exit 1
ENTRYPOINT ["/usr/local/bin/inbuxa"]
CMD ["--config", "/etc/inbuxa/config.json"]
+1 -2
View File
@@ -42,7 +42,6 @@ COPY Cargo.toml .
COPY Cargo.lock .
COPY crates/ crates/
COPY resources/ resources/
COPY tests/ tests/
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
@@ -75,6 +74,6 @@ VOLUME ["/etc/inbuxa", "/var/lib/inbuxa"]
EXPOSE 443 25 110 587 465 143 993 995 4190 8080
ENV INBUXA_HEALTHCHECK_URL=https://127.0.0.1:443/healthz/live
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD curl -fsSk -H "X-Forwarded-For: 127.0.0.1" "$INBUXA_HEALTHCHECK_URL" || curl -fsS -H "X-Forwarded-For: 127.0.0.1" http://127.0.0.1:8080/healthz/live || exit 1
CMD curl -fsSk "$INBUXA_HEALTHCHECK_URL" || curl -fsS http://127.0.0.1:8080/healthz/live || exit 1
ENTRYPOINT ["/usr/local/bin/inbuxa"]
CMD ["--config", "/etc/inbuxa/config.json"]
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+23
View File
@@ -0,0 +1,23 @@
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+24
View File
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
+82 -2
View File
@@ -24,6 +24,9 @@ carry their own license files.
| `crates/common/src/network/acme/directory.rs`, `crates/common/src/network/acme/jose.rs`, `crates/common/src/network/acme/order.rs` | [rustls-acme](https://github.com/FlorianUekermann/rustls-acme) (MIT or Apache-2.0) | Copyright (c) Florian Uekermann |
| `crates/types/src/id.rs` | [crockford](https://github.com/archer884/crockford) (MIT or Apache-2.0) | Copyright (c) 2017 J/A <archer884@gmail.com> |
| `crates/nlp/src/tokenizers/types.rs` | test cases from [linkify](https://github.com/robinst/linkify) (MIT or Apache-2.0) | Copyright (c) 2017 Robin Stocker |
| `crates/calcard/src/datecalc/error.rs` | [rust-rrule](https://github.com/fmeringdal/rust-rrule) (MIT or Apache-2.0) | Copyright (c) 2021 Fredrik Meringdal, Ralph Bisschops |
| `crates/calcard/src/datecalc/recurrence.rs`, `crates/calcard/src/datecalc/weekdate.rs` | [bttf](https://github.com/BurntSushi/bttf), by Andrew Gallant (MIT or Unlicense) | Copyright (c) 2025 Andrew Gallant <jamslam@gmail.com> |
| `crates/jmap-tools/src/json/deserializer.rs`, `crates/jmap-tools/src/json/index.rs`, `crates/jmap-tools/src/json/key.rs`, `crates/jmap-tools/src/json/mod.rs`, `crates/jmap-tools/src/json/num.rs`, `crates/jmap-tools/src/json/object_vec.rs`, `crates/jmap-tools/src/json/ser.rs` | code by Pascal Seitz, apparently [serde_json_borrow](https://github.com/PSeitz/serde_json_borrow) (MIT or Apache-2.0); the files name the author, not the project. `crates/jmap-tools/src/json/value.rs` carries no notice but belongs with them | Copyright (c) 2021 Pascal Seitz <pascal.seitz@gmail.com> |
| `resources/spam-filter/spam-filter-rules.json.gz` | the published rules of [spam-filter](https://github.com/stalwartlabs/spam-filter) v3.0.2, unmodified, built into the server as its default spam rules (MIT or Apache-2.0) | Copyright (C) 2024, Stalwart Labs LLC |
Each notice above applies with this permission notice:
@@ -50,7 +53,7 @@ Each notice above applies with this permission notice:
| Where | From | Notice |
|---|---|---|
| `crates/jmap-proto/src/types/date.rs`, `crates/registry/src/types/datetime.rs` | [upb](https://github.com/protocolbuffers/upb/blob/22182e6e/upb/json_decode.c), the date parsing marked in each file | Copyright (c) 2009-2011, Google Inc. All rights reserved. |
| `crates/jmap-proto/src/types/date.rs`, `crates/registry/src/types/datetime.rs`, `crates/smtp-proto/src/request/parser.rs` | [upb](https://github.com/protocolbuffers/upb/blob/22182e6e/upb/json_decode.c), the date parsing marked in each file | Copyright (c) 2009-2011, Google Inc. All rights reserved. |
```text
Copyright (c) 2009-2011, Google Inc.
@@ -80,14 +83,91 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
```
## Data derived from Adobe's published resources
The PDF text extractor (`crates/text-extract/src/pdf/tables/`) ships compact
tables that, going by their contents, were built from Adobe's published font
resources. The files don't say so; they're credited here because the data
matches. Each is a modified, compacted form, not the original files.
| Where | From | Notice |
|---|---|---|
| `cid_cns1.bin`, `cid_gb1.bin`, `cid_japan1.bin`, `cid_korea1.bin`, `cid_data.rs`, `cmap_data.rs` | the CID-to-Unicode mappings and predefined CMaps of [cmap-resources](https://github.com/adobe-type-tools/cmap-resources) | Copyright 1990-2023 Adobe. All rights reserved. |
| `glyph_data.rs` | the [Adobe Glyph List](https://github.com/adobe-type-tools/agl-aglfn) | Copyright 2002-2019 Adobe (http://www.adobe.com/). |
| `font_data.rs` | the character widths of the Core 14 AFM files | Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All Rights Reserved. |
The cmap-resources and Adobe Glyph List notices apply with this license:
```text
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Adobe nor the names of its contributors may be
used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
```
The Core 14 AFM notice applies with Adobe's permission paragraph:
> This file and the 14 PostScript(R) AFM files it accompanies may be used,
> copied, and distributed for any purpose and without charge, with or without
> modification, provided that all copyright notices are retained; that the AFM
> files are not distributed without this file; that all modifications to this
> file or any of the AFM files are prominently noted in the modified file(s);
> and that this paragraph is not modified.
INBUXA distributes none of the AFM files themselves, only widths taken from
them and repacked as above.
## Credited algorithms
These files implement published algorithms and credit their source. No code
is copied, so there's no notice to carry. They're listed so the strip report
doesn't flag them as new.
- `crates/jmap-proto/src/types/date.rs`, `crates/registry/src/types/datetime.rs`:
- `crates/jmap-proto/src/types/date.rs`, `crates/registry/src/types/datetime.rs`,
`crates/smtp-proto/src/response/generate.rs`:
`civil_from_days`, from Howard Hinnant's
[date algorithms](http://howardhinnant.github.io/date_algorithms.html)
- `crates/utils/src/glob.rs`: Russ Cox's
[glob matching](https://research.swtch.com/glob)
## Upstream's own files under a permissive license
Stalwart Labs' own code, released under a license other than the AGPL. No
third-party notice applies; listed so the strip report doesn't flag them.
- `crates/calcard/src/datecalc/rrule.rs` (Apache-2.0 or MIT)
- `crates/smtp-report/src/lib.rs` (flagged only because its documentation
says an address "is taken from" a struct)
## Crates upstream moved into its tree
Stalwart 1.0 brought four of its own libraries into the server's tree and
relabeled them AGPL-3.0-only OR LicenseRef-SEL. Each was published on
crates.io by Stalwart Labs under Apache-2.0 OR MIT, and those releases stay
under those terms. The third-party parts inside them are listed above.
| In this tree | Last published permissive release |
|---|---|
| `crates/calcard` | calcard 0.3.14 |
| `crates/jmap-tools` | jmap-tools 0.1.8 |
| `crates/smtp-proto` | smtp-proto 0.2.5 |
| `crates/smtp-report` | the `report` module of mail-auth 0.13.3 |
+46
View File
@@ -0,0 +1,46 @@
[package]
name = "calcard"
description = "iCalendar/JSCalendar and vCard/JSContact parsing, building and conversion library"
version = "0.4.0"
edition = "2024"
license = "AGPL-3.0-only OR LicenseRef-SEL"
publish = false
[dependencies]
jmap-tools = { path = "../jmap-tools", optional = true }
ahash = "0.8.12"
jiff = "0.2.38"
hashify = "0.3"
memchr = "2.8"
encodify = "1.0"
mail-parser = "1.0"
simdutf8 = "0.1.5"
smallvec = { version = "1.16", features = ["union"] }
uuid = { version = "1.29", default-features = false, optional = true }
sha1 = { version = "0.11", default-features = false, optional = true }
rkyv = { version = "0.8", optional = true, features = ["smallvec-1"] }
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
jiff = { version = "0.2.38", features = ["js"] }
[features]
default = ["jmap", "tzdb-bundle"]
rkyv = ["dep:rkyv"]
serde = ["dep:serde", "smallvec/serde"]
jmap = ["jmap-tools", "uuid", "sha1", "serde", "serde_json"]
tzdb-bundle = ["jiff/tzdb-bundle-always"]
[dev-dependencies]
smallvec = { version = "1.16", features = ["serde"] }
insta = "1.49"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.29", default-features = false, features = ["v5"] }
[lib]
doctest = false
[lints]
workspace = true
+88
View File
@@ -0,0 +1,88 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use rkyv::{
Archive, Archived, Deserialize, Place, Serialize,
rancor::Fallible,
ser::{Allocator, Writer},
vec::{ArchivedVec, VecResolver},
with::{ArchiveWith, DeserializeWith, SerializeWith},
};
use smallvec::{Array, SmallVec};
pub struct Unboxed;
impl<T: Archive> ArchiveWith<Box<T>> for Unboxed {
type Archived = T::Archived;
type Resolver = T::Resolver;
fn resolve_with(field: &Box<T>, resolver: Self::Resolver, out: Place<Self::Archived>) {
T::resolve(field, resolver, out);
}
}
impl<T: Serialize<S>, S: Fallible + ?Sized> SerializeWith<Box<T>, S> for Unboxed {
fn serialize_with(field: &Box<T>, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
T::serialize(field, serializer)
}
}
impl<T, D> DeserializeWith<T::Archived, Box<T>, D> for Unboxed
where
T: Archive,
T::Archived: Deserialize<T, D>,
D: Fallible + ?Sized,
{
fn deserialize_with(field: &T::Archived, deserializer: &mut D) -> Result<Box<T>, D::Error> {
field.deserialize(deserializer).map(Box::new)
}
}
pub struct InlineVec;
impl<A> ArchiveWith<SmallVec<A>> for InlineVec
where
A: Array,
A::Item: Archive,
{
type Archived = ArchivedVec<Archived<A::Item>>;
type Resolver = VecResolver;
fn resolve_with(field: &SmallVec<A>, resolver: Self::Resolver, out: Place<Self::Archived>) {
ArchivedVec::resolve_from_slice(field.as_slice(), resolver, out);
}
}
impl<A, S> SerializeWith<SmallVec<A>, S> for InlineVec
where
A: Array,
A::Item: Serialize<S>,
S: Fallible + Allocator + Writer + ?Sized,
{
fn serialize_with(field: &SmallVec<A>, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
ArchivedVec::serialize_from_slice(field.as_slice(), serializer)
}
}
impl<A, D> DeserializeWith<ArchivedVec<Archived<A::Item>>, SmallVec<A>, D> for InlineVec
where
A: Array,
A::Item: Archive,
Archived<A::Item>: Deserialize<A::Item, D>,
D: Fallible + ?Sized,
{
fn deserialize_with(
field: &ArchivedVec<Archived<A::Item>>,
deserializer: &mut D,
) -> Result<SmallVec<A>, D::Error> {
let mut items = SmallVec::with_capacity(field.len());
for item in field.iter() {
items.push(item.deserialize(deserializer)?);
}
Ok(items)
}
}
+361
View File
@@ -0,0 +1,361 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::common::export::EmbeddedBudget;
use ahash::AHashMap;
use std::hash::Hash;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BlobIdOutcome<B> {
Generated(B),
Declined(Vec<u8>),
Failed,
}
pub trait BlobIdGenerator<B> {
fn blob_id(&mut self, data: Vec<u8>, content_type: Option<&str>) -> BlobIdOutcome<B>;
fn is_enabled(&self) -> bool {
true
}
}
pub trait BlobResolver<B> {
fn resolve(&mut self, blob_id: &B) -> Option<Vec<u8>>;
fn is_enabled(&self) -> bool {
true
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NoBlobIds;
#[derive(Debug, Clone, Copy)]
pub struct BlobIdFn<F>(pub F);
#[derive(Debug, Clone, Copy)]
pub struct BlobResolverFn<F>(pub F);
impl<B> BlobIdGenerator<B> for NoBlobIds {
fn blob_id(&mut self, data: Vec<u8>, _: Option<&str>) -> BlobIdOutcome<B> {
BlobIdOutcome::Declined(data)
}
fn is_enabled(&self) -> bool {
false
}
}
impl<B> BlobResolver<B> for NoBlobIds {
fn resolve(&mut self, _: &B) -> Option<Vec<u8>> {
None
}
fn is_enabled(&self) -> bool {
false
}
}
impl<B, F> BlobIdGenerator<B> for BlobIdFn<F>
where
F: FnMut(&[u8]) -> Option<B>,
{
fn blob_id(&mut self, data: Vec<u8>, _: Option<&str>) -> BlobIdOutcome<B> {
match (self.0)(&data) {
Some(blob_id) => BlobIdOutcome::Generated(blob_id),
None => BlobIdOutcome::Declined(data),
}
}
}
impl<B, F> BlobResolver<B> for BlobResolverFn<F>
where
F: FnMut(&B) -> Option<Vec<u8>>,
{
fn resolve(&mut self, blob_id: &B) -> Option<Vec<u8>> {
(self.0)(blob_id)
}
}
impl<B, T: BlobIdGenerator<B> + ?Sized> BlobIdGenerator<B> for &mut T {
fn blob_id(&mut self, data: Vec<u8>, content_type: Option<&str>) -> BlobIdOutcome<B> {
(**self).blob_id(data, content_type)
}
fn is_enabled(&self) -> bool {
(**self).is_enabled()
}
}
impl<B, T: BlobResolver<B> + ?Sized> BlobResolver<B> for &mut T {
fn resolve(&mut self, blob_id: &B) -> Option<Vec<u8>> {
(**self).resolve(blob_id)
}
fn is_enabled(&self) -> bool {
(**self).is_enabled()
}
}
#[derive(Debug, Clone, Copy)]
pub struct BlobOptions<T = NoBlobIds> {
handler: Option<T>,
max_embedded_size: Option<usize>,
}
impl<T> Default for BlobOptions<T> {
fn default() -> Self {
Self {
handler: None,
max_embedded_size: None,
}
}
}
impl<T> BlobOptions<T> {
pub(crate) fn with_handler<U>(self, handler: U) -> BlobOptions<U> {
BlobOptions {
handler: Some(handler),
max_embedded_size: self.max_embedded_size,
}
}
pub(crate) fn with_max_embedded_size(mut self, max_embedded_size: usize) -> Self {
self.max_embedded_size = Some(max_embedded_size);
self
}
pub(crate) fn budget(&self) -> EmbeddedBudget {
EmbeddedBudget::new(self.max_embedded_size)
}
pub(crate) fn blob_ids<B: Clone>(&mut self) -> Option<BlobIds<'_, B>>
where
T: BlobIdGenerator<B>,
{
self.handler
.as_mut()
.filter(|generator| BlobIdGenerator::<B>::is_enabled(*generator))
.map(|generator| BlobIds::new(generator as &mut dyn BlobIdGenerator<B>))
}
pub(crate) fn resolved_blobs<B: Clone + Eq + Hash>(&mut self) -> Option<ResolvedBlobs<'_, B>>
where
T: BlobResolver<B>,
{
self.handler
.as_mut()
.filter(|resolver| BlobResolver::<B>::is_enabled(*resolver))
.map(|resolver| ResolvedBlobs::new(resolver as &mut dyn BlobResolver<B>))
}
}
pub(crate) struct BlobIds<'a, B> {
generator: &'a mut dyn BlobIdGenerator<B>,
expected_sizes: Option<AHashMap<usize, ExpectedSize>>,
generated: AHashMap<(usize, u64), Vec<GeneratedBinary<B>>>,
has_failed: bool,
}
#[derive(Default)]
struct ExpectedSize {
total: usize,
pending: usize,
}
struct GeneratedBinary<B> {
blob_id: B,
data: Vec<u8>,
}
pub(crate) struct GeneratedBlobId<B> {
pub blob_id: B,
pub size: usize,
}
impl<'a, B: Clone> BlobIds<'a, B> {
pub(crate) fn new(generator: &'a mut dyn BlobIdGenerator<B>) -> Self {
Self {
generator,
expected_sizes: None,
generated: AHashMap::new(),
has_failed: false,
}
}
pub(crate) fn has_failed(&self) -> bool {
self.has_failed
}
pub(crate) fn expect_sizes(&mut self, sizes: impl IntoIterator<Item = usize>) {
let expected_sizes = self.expected_sizes.get_or_insert_with(AHashMap::new);
for size in sizes {
let expected = expected_sizes.entry(size).or_default();
expected.total += 1;
expected.pending += 1;
}
}
pub(crate) fn blob_id(
&mut self,
data: Vec<u8>,
content_type: Option<&str>,
) -> Result<GeneratedBlobId<B>, Vec<u8>> {
if data.is_empty() {
return Err(data);
}
let size = data.len();
let (is_repeated, expects_more) = match &mut self.expected_sizes {
Some(expected_sizes) => {
expected_sizes
.get_mut(&size)
.map_or((false, false), |expected| {
expected.pending = expected.pending.saturating_sub(1);
(expected.total > 1, expected.pending > 0)
})
}
None => (true, true),
};
if !is_repeated {
return self.generate(data, content_type);
}
let key = (size, self.generated.hasher().hash_one(&data));
if let Some(generated) = self
.generated
.get(&key)
.and_then(|generated| generated.iter().find(|generated| generated.data == data))
{
return Ok(GeneratedBlobId {
blob_id: generated.blob_id.clone(),
size,
});
}
if !expects_more {
return self.generate(data, content_type);
}
let blob_id = self.generate(data.clone(), content_type)?.blob_id;
self.generated
.entry(key)
.or_default()
.push(GeneratedBinary {
blob_id: blob_id.clone(),
data,
});
Ok(GeneratedBlobId { blob_id, size })
}
fn generate(
&mut self,
data: Vec<u8>,
content_type: Option<&str>,
) -> Result<GeneratedBlobId<B>, Vec<u8>> {
let size = data.len();
match self.generator.blob_id(data, content_type) {
BlobIdOutcome::Generated(blob_id) => Ok(GeneratedBlobId { blob_id, size }),
BlobIdOutcome::Declined(data) => Err(data),
BlobIdOutcome::Failed => {
self.has_failed = true;
Err(Vec::new())
}
}
}
}
pub(crate) struct ResolvedBlobs<'a, B> {
resolver: &'a mut dyn BlobResolver<B>,
uses: AHashMap<B, usize>,
resolved: AHashMap<B, Option<Vec<u8>>>,
}
impl<'a, B: Clone + Eq + Hash> ResolvedBlobs<'a, B> {
pub(crate) fn new(resolver: &'a mut dyn BlobResolver<B>) -> Self {
Self {
resolver,
uses: AHashMap::new(),
resolved: AHashMap::new(),
}
}
pub(crate) fn add_uses<'x>(&mut self, blob_ids: impl Iterator<Item = &'x B>)
where
B: 'x,
{
for blob_id in blob_ids {
*self.uses.entry(blob_id.clone()).or_default() += 1;
}
}
pub(crate) fn resolve(&mut self, blob_id: &B) -> Option<Vec<u8>> {
let remaining = self.uses.get_mut(blob_id).map_or(0, |uses| {
*uses = uses.saturating_sub(1);
*uses
});
let data = match self.resolved.remove(blob_id) {
Some(data) => data,
None => self
.resolver
.resolve(blob_id)
.filter(|data| !data.is_empty()),
};
if remaining > 0 {
self.resolved.insert(blob_id.clone(), data.clone());
}
data
}
}
pub(crate) fn sniff_media_type(data: &[u8]) -> &'static str {
match data {
[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A, ..] => "image/png",
[0xFF, 0xD8, 0xFF, ..] => "image/jpeg",
[b'G', b'I', b'F', b'8', b'7' | b'9', b'a', ..] => "image/gif",
[
b'R',
b'I',
b'F',
b'F',
_,
_,
_,
_,
b'W',
b'E',
b'B',
b'P',
..,
] => "image/webp",
[
b'R',
b'I',
b'F',
b'F',
_,
_,
_,
_,
b'W',
b'A',
b'V',
b'E',
..,
] => "audio/wav",
[b'O', b'g', b'g', b'S', ..] => "audio/ogg",
[b'I', b'D', b'3', ..] | [0xFF, 0xFB | 0xF3 | 0xF2, ..] => "audio/mpeg",
_ if is_svg(data) => "image/svg+xml",
_ => "application/octet-stream",
}
}
fn is_svg(data: &[u8]) -> bool {
let head = data.get(..data.len().min(512)).unwrap_or_default();
let text = head.trim_ascii_start();
(text.starts_with(b"<svg") || text.starts_with(b"<?xml"))
&& head.windows(4).any(|window| window == b"<svg")
}
+101
View File
@@ -0,0 +1,101 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::common::{Encoding, tokenizer::StopChar};
use encodify::{
base64,
qp::{self, QuotedPrintable},
};
use mail_parser::Charset;
use std::borrow::Cow;
const QUOTED_PRINTABLE_CHARSET: &str = "iso-8859-1";
const QUOTED_PRINTABLE: QuotedPrintable = qp::BODY.strict();
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Decoded {
Binary(Vec<u8>),
Text(String),
Undecodable,
}
pub(crate) struct ValueDecoder<'x> {
pub(crate) encoding: Encoding,
pub(crate) charset: Option<&'x str>,
pub(crate) binary: bool,
}
impl ValueDecoder<'_> {
#[inline(never)]
pub(crate) fn decode(
&self,
input: &[u8],
stop_char: StopChar,
payload: Option<Vec<u8>>,
) -> Decoded {
let Some(bytes) = payload
.map(Cow::Owned)
.or_else(|| self.encoding.decode_value(input, stop_char))
else {
return Decoded::Undecodable;
};
if self.binary {
return Decoded::Binary(bytes.into_owned());
}
let default_charset = match self.encoding {
Encoding::QuotedPrintable => Some(QUOTED_PRINTABLE_CHARSET),
Encoding::Base64 => None,
};
match self
.charset
.or(default_charset)
.and_then(|charset| Charset::from_label(charset.as_bytes()))
.filter(|charset| *charset != Charset::Utf8)
{
Some(charset) => Decoded::Text(match bytes {
Cow::Borrowed(bytes) => charset.decode(bytes).into_owned(),
Cow::Owned(bytes) => charset.decode_owned(bytes),
}),
None => match String::from_utf8(bytes.into_owned()) {
Ok(text) => Decoded::Text(text),
Err(err) => Decoded::Binary(err.into_bytes()),
},
}
}
}
impl Encoding {
pub(crate) fn decode(self, input: &[u8]) -> Option<Cow<'_, [u8]>> {
match self {
Encoding::Base64 => base64::LENIENT.decode(input).ok().map(Cow::Owned),
Encoding::QuotedPrintable => QUOTED_PRINTABLE.decode(input).ok(),
}
}
fn decode_value(self, input: &[u8], stop_char: StopChar) -> Option<Cow<'_, [u8]>> {
match (self, stop_char) {
(Encoding::QuotedPrintable, StopChar::Lf) | (Encoding::Base64, _) => self.decode(input),
(Encoding::QuotedPrintable, _) => Self::decode_quoted_printable_component(input),
}
}
fn decode_quoted_printable_component(input: &[u8]) -> Option<Cow<'_, [u8]>> {
let mut text = input;
while let [head @ .., b' ' | b'\t'] = text {
text = head;
}
match input.get(text.len()..) {
Some(blanks @ [_, ..]) => {
let mut bytes = Vec::new();
QUOTED_PRINTABLE.decode_append(text, &mut bytes).ok()?;
bytes.extend_from_slice(blanks);
Some(Cow::Owned(bytes))
}
_ => QUOTED_PRINTABLE.decode(input).ok(),
}
}
}
+42
View File
@@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use jmap_tools::{Element, Property, Value};
pub(crate) struct Elements<'a, 'x, P: Property, E: Element<Property = P>> {
stack: Vec<&'a Value<'x, P, E>>,
}
impl<'a, 'x, P: Property, E: Element<Property = P>> Elements<'a, 'x, P, E> {
pub(crate) fn new(value: &'a Value<'x, P, E>) -> Self {
let mut stack = Vec::with_capacity(16);
stack.push(value);
Self { stack }
}
pub(crate) fn from_values(values: impl Iterator<Item = &'a Value<'x, P, E>>) -> Self {
let mut stack = Vec::with_capacity(16);
stack.extend(values);
Self { stack }
}
}
impl<'a, 'x, P: Property, E: Element<Property = P>> Iterator for Elements<'a, 'x, P, E> {
type Item = &'a E;
fn next(&mut self) -> Option<Self::Item> {
while let Some(value) = self.stack.pop() {
match value {
Value::Element(element) => return Some(element),
Value::Array(items) => self.stack.extend(items.iter().rev()),
Value::Object(obj) => self.stack.extend(obj.as_vec().iter().rev().map(|(_, v)| v)),
Value::Null | Value::Bool(_) | Value::Number(_) | Value::Str(_) => {}
}
}
None
}
}
+187
View File
@@ -0,0 +1,187 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use ahash::AHashSet;
use crate::{
icalendar::{ICalendar, ICalendarProperty, ICalendarValue, Uri},
vcard::{VCard, VCardProperty, VCardValue},
};
pub(crate) fn distinct_size<'x>(binaries: impl Iterator<Item = &'x [u8]>) -> usize {
let mut distinct: AHashSet<&[u8]> = AHashSet::new();
let mut size = 0usize;
for data in binaries {
if distinct.insert(data) {
size = size.saturating_add(data.len());
}
}
size
}
impl ICalendar {
pub fn blob_binaries(&self) -> impl Iterator<Item = &[u8]> {
self.components
.iter()
.filter(|component| component.component_type.converts_links())
.flat_map(|component| component.entries.iter())
.filter(|entry| {
matches!(
entry.name,
ICalendarProperty::Attach | ICalendarProperty::Image
)
})
.filter_map(|entry| entry.values.first().and_then(ICalendarValue::binary_bytes))
}
pub fn embedded_binaries(&self) -> impl Iterator<Item = &[u8]> {
self.components
.iter()
.flat_map(|component| component.entries.iter())
.flat_map(|entry| entry.values.iter())
.filter_map(|value| match value {
ICalendarValue::Binary(data) => Some(data.as_slice()),
ICalendarValue::Uri(Uri::Data(data)) => Some(data.data.as_slice()),
_ => None,
})
}
pub fn embedded_size(&self) -> usize {
distinct_size(self.embedded_binaries())
}
}
impl VCard {
pub fn blob_binaries(&self) -> impl Iterator<Item = &[u8]> {
self.entries
.iter()
.filter(|entry| {
matches!(
entry.name,
VCardProperty::Photo | VCardProperty::Logo | VCardProperty::Sound
)
})
.filter_map(|entry| match entry.values.first() {
Some(VCardValue::Binary(data)) if !data.data.is_empty() => {
Some(data.data.as_slice())
}
_ => None,
})
}
pub fn embedded_binaries(&self) -> impl Iterator<Item = &[u8]> {
self.entries
.iter()
.flat_map(|entry| entry.values.iter())
.filter_map(|value| match value {
VCardValue::Binary(data) => Some(data.data.as_slice()),
_ => None,
})
}
pub fn embedded_size(&self) -> usize {
distinct_size(self.embedded_binaries())
}
}
#[cfg(feature = "rkyv")]
mod archived {
use super::distinct_size;
use crate::{
icalendar::{
ArchivedICalendar, ArchivedICalendarProperty, ArchivedICalendarValue, ArchivedUri,
},
vcard::{ArchivedVCard, ArchivedVCardProperty, ArchivedVCardValue},
};
impl ArchivedICalendar {
pub fn blob_binaries(&self) -> impl Iterator<Item = &[u8]> {
self.components
.iter()
.filter(|component| component.component_type.converts_links())
.flat_map(|component| component.entries.iter())
.filter(|entry| {
matches!(
entry.name,
ArchivedICalendarProperty::Attach | ArchivedICalendarProperty::Image
)
})
.filter_map(|entry| {
entry
.values
.first()
.and_then(ArchivedICalendarValue::binary_bytes)
})
}
pub fn embedded_binaries(&self) -> impl Iterator<Item = &[u8]> {
self.components
.iter()
.flat_map(|component| component.entries.iter())
.flat_map(|entry| entry.values.iter())
.filter_map(|value| match value {
ArchivedICalendarValue::Binary(data) => Some(data.as_ref()),
ArchivedICalendarValue::Uri(ArchivedUri::Data(data)) => {
Some(data.data.as_ref())
}
_ => None,
})
}
pub fn embedded_size(&self) -> usize {
distinct_size(self.embedded_binaries())
}
}
impl ArchivedICalendarValue {
pub(crate) fn binary_bytes(&self) -> Option<&[u8]> {
match self {
ArchivedICalendarValue::Binary(data) => Some(data.as_ref()),
ArchivedICalendarValue::Uri(ArchivedUri::Data(data)) => Some(data.data.as_ref()),
_ => None,
}
.filter(|data| !data.is_empty())
}
}
impl ArchivedVCard {
pub fn blob_binaries(&self) -> impl Iterator<Item = &[u8]> {
self.entries
.iter()
.filter(|entry| {
matches!(
entry.name,
ArchivedVCardProperty::Photo
| ArchivedVCardProperty::Logo
| ArchivedVCardProperty::Sound
)
})
.filter_map(|entry| match entry.values.first() {
Some(ArchivedVCardValue::Binary(data)) if !data.data.is_empty() => {
Some(data.data.as_ref())
}
_ => None,
})
}
pub fn embedded_binaries(&self) -> impl Iterator<Item = &[u8]> {
self.entries
.iter()
.flat_map(|entry| entry.values.iter())
.filter_map(|value| match value {
ArchivedVCardValue::Binary(data) => Some(data.data.as_ref()),
_ => None,
})
}
pub fn embedded_size(&self) -> usize {
distinct_size(self.embedded_binaries())
}
}
}
+126
View File
@@ -0,0 +1,126 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use ahash::AHashSet;
use std::fmt::{Display, Formatter};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ExportError {
NotGroup,
NoComponents,
InvalidPatch {
recurrence_id: String,
pointer: String,
},
EmbeddedSizeExceeded {
max: usize,
},
UnresolvedBlob {
blob_id: String,
},
}
impl Display for ExportError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ExportError::NotGroup => {
f.write_str("the top-level object is not a JSCalendar Group or a JSContact Card")
}
ExportError::NoComponents => {
f.write_str("the object has no entries that convert to a component")
}
ExportError::InvalidPatch {
recurrence_id,
pointer,
} => write!(
f,
"the patch at recurrenceOverrides/{recurrence_id}/{pointer} does not apply"
),
ExportError::EmbeddedSizeExceeded { max } => write!(
f,
"the embedded binaries exceed the maximum size of {max} bytes"
),
ExportError::UnresolvedBlob { blob_id } => {
write!(f, "the blobId {blob_id} could not be resolved")
}
}
}
}
impl std::error::Error for ExportError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ImportError {
BlobIdFailed,
}
impl Display for ImportError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ImportError::BlobIdFailed => {
f.write_str("the blob id generator failed to store a binary")
}
}
}
}
impl std::error::Error for ImportError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct RejectedPatch {
pub recurrence_id: String,
pub pointer: String,
}
impl Display for RejectedPatch {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"the patch at recurrenceOverrides/{}/{} does not apply",
self.recurrence_id, self.pointer
)
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct EmbeddedBudget {
max: Option<usize>,
left: usize,
charged: AHashSet<(usize, u64)>,
}
impl EmbeddedBudget {
pub(crate) fn new(max: Option<usize>) -> Self {
Self {
max,
left: max.unwrap_or(usize::MAX),
charged: AHashSet::new(),
}
}
pub(crate) fn take(&mut self, data: &[u8]) -> Result<(), ExportError> {
let Some(max) = self.max else {
return Ok(());
};
let key = (data.len(), self.charged.hasher().hash_one(data));
if !self.charged.insert(key) {
return Ok(());
}
match self.left.checked_sub(data.len()) {
Some(left) => {
self.left = left;
Ok(())
}
None => {
self.left = 0;
Err(ExportError::EmbeddedSizeExceeded { max })
}
}
}
}
+320
View File
@@ -0,0 +1,320 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use simdutf8::basic::from_utf8;
use std::fmt::{self, Write};
const MAX_DIGITS: usize = 20;
const DIRECT_WRITE_LEN: usize = 256;
trait CopyShort {
fn copy_short_from(&mut self, src: &[u8]);
}
impl CopyShort for [u8] {
#[inline(always)]
fn copy_short_from(&mut self, src: &[u8]) {
match src.len() {
0 => {}
len @ 1..=3 => {
for pos in [0, len / 2, len - 1] {
if let (Some(dst), Some(src)) = (self.get_mut(pos), src.get(pos)) {
*dst = *src;
}
}
}
4..=7 => {
if let (Some(dst), Some(src)) =
(self.first_chunk_mut::<4>(), src.first_chunk::<4>())
{
*dst = *src;
}
if let (Some(dst), Some(src)) = (self.last_chunk_mut::<4>(), src.last_chunk::<4>())
{
*dst = *src;
}
}
8..=16 => {
if let (Some(dst), Some(src)) =
(self.first_chunk_mut::<8>(), src.first_chunk::<8>())
{
*dst = *src;
}
if let (Some(dst), Some(src)) = (self.last_chunk_mut::<8>(), src.last_chunk::<8>())
{
*dst = *src;
}
}
17..=32 => {
if let (Some(dst), Some(src)) =
(self.first_chunk_mut::<16>(), src.first_chunk::<16>())
{
*dst = *src;
}
if let (Some(dst), Some(src)) =
(self.last_chunk_mut::<16>(), src.last_chunk::<16>())
{
*dst = *src;
}
}
33..=64 => {
if let (Some(dst), Some(src)) =
(self.first_chunk_mut::<32>(), src.first_chunk::<32>())
{
*dst = *src;
}
if let (Some(dst), Some(src)) =
(self.last_chunk_mut::<32>(), src.last_chunk::<32>())
{
*dst = *src;
}
}
65..=128 => {
if let (Some(dst), Some(src)) =
(self.first_chunk_mut::<64>(), src.first_chunk::<64>())
{
*dst = *src;
}
if let (Some(dst), Some(src)) =
(self.last_chunk_mut::<64>(), src.last_chunk::<64>())
{
*dst = *src;
}
}
_ => self.copy_from_slice(src),
}
}
}
pub(crate) trait AsciiPush {
fn push_ascii(&mut self, bytes: &[u8]) -> fmt::Result;
#[inline(always)]
fn push_ascii_str(&mut self, text: &'static str) -> fmt::Result {
self.push_ascii(text.as_bytes())
}
#[inline(always)]
fn push_byte(&mut self, ch: u8) -> fmt::Result {
self.push_ascii(&[ch])
}
#[inline(always)]
fn push_u64(&mut self, value: u64) -> fmt::Result {
if value < 10 {
self.push_digits::<1>(value)
} else {
self.push_digits::<2>(value)
}
}
#[inline(always)]
fn push_i64(&mut self, value: i64) -> fmt::Result {
if value < 0 {
self.push_byte(b'-')?;
}
self.push_u64(value.unsigned_abs())
}
#[inline(always)]
fn push_2_digits(&mut self, value: u8) -> fmt::Result {
self.push_digits::<2>(value.into())
}
#[inline(always)]
fn push_4_digits(&mut self, value: u16) -> fmt::Result {
self.push_digits::<4>(value.into())
}
#[inline(always)]
fn push_digits<const WIDTH: usize>(&mut self, value: u64) -> fmt::Result {
match u32::try_from(value) {
Ok(value) if value < 10u32.pow(WIDTH as u32) => {
let mut digits = [b'0'; WIDTH];
for (place, digit) in digits.iter_mut().rev().enumerate() {
*digit += (value / 10u32.pow(place as u32) % 10) as u8;
}
self.push_ascii(&digits)
}
_ => self.push_padded(value, WIDTH),
}
}
fn push_padded(&mut self, value: u64, width: usize) -> fmt::Result {
let mut digits = [b'0'; MAX_DIGITS];
let mut rest = value;
let mut count = 0;
for slot in digits.iter_mut().rev() {
*slot = b'0' + (rest % 10) as u8;
rest /= 10;
count += 1;
if rest == 0 {
break;
}
}
let start = MAX_DIGITS - count.max(width).min(MAX_DIGITS);
self.push_ascii(digits.get(start..).unwrap_or_default())
}
fn push_list<T>(
&mut self,
name: &'static str,
items: &[T],
mut push_item: impl FnMut(&mut Self, &T) -> fmt::Result,
) -> fmt::Result
where
Self: Sized,
{
for (pos, item) in items.iter().enumerate() {
self.push_ascii_str(if pos == 0 { name } else { "," })?;
push_item(self, item)?;
}
Ok(())
}
fn push_duration(&mut self, duration: DurationParts) -> fmt::Result {
let DurationParts {
neg,
weeks,
days,
hours,
minutes,
seconds,
} = duration;
self.push_ascii(if neg { b"-P" } else { b"P" })?;
if weeks == 0 && days == 0 && hours == 0 && minutes == 0 && seconds == 0 {
return self.push_ascii(b"T0S");
}
for (value, unit) in [(weeks, b'W'), (days, b'D')] {
if value != 0 {
self.push_u64(value.into())?;
self.push_byte(unit)?;
}
}
if hours != 0 || minutes != 0 || seconds != 0 {
self.push_byte(b'T')?;
for (value, unit) in [(hours, b'H'), (minutes, b'M'), (seconds, b'S')] {
if value != 0 {
self.push_u64(value.into())?;
self.push_byte(unit)?;
}
}
}
Ok(())
}
}
pub(crate) struct BufferedWriter<'x, W: Write + ?Sized, const N: usize> {
out: &'x mut W,
len: usize,
buf: [u8; N],
}
impl<'x, W: Write + ?Sized, const N: usize> BufferedWriter<'x, W, N> {
#[inline(always)]
pub(crate) fn new(out: &'x mut W) -> Self {
Self {
out,
len: 0,
buf: [0; N],
}
}
pub(crate) fn flush(&mut self) -> fmt::Result {
if self.len > 0 {
let text = self
.buf
.get(..self.len)
.and_then(|bytes| from_utf8(bytes).ok())
.ok_or(fmt::Error)?;
self.out.write_str(text)?;
self.len = 0;
}
Ok(())
}
#[inline(always)]
fn push_bytes(&mut self, bytes: &[u8]) -> Option<()> {
if bytes.len() < DIRECT_WRITE_LEN
&& let Some(slot) = self.buf.get_mut(self.len..self.len + bytes.len())
{
slot.copy_short_from(bytes);
self.len += bytes.len();
Some(())
} else {
None
}
}
#[inline(never)]
fn write_str_after_flush(&mut self, text: &str) -> fmt::Result {
self.flush()?;
match self.buf.get_mut(..text.len()) {
Some(slot) if text.len() < DIRECT_WRITE_LEN => {
slot.copy_from_slice(text.as_bytes());
self.len = text.len();
Ok(())
}
_ => self.out.write_str(text),
}
}
#[inline(never)]
fn push_ascii_after_flush(&mut self, bytes: &[u8]) -> fmt::Result {
self.write_str_after_flush(str::from_utf8(bytes).map_err(|_| fmt::Error)?)
}
}
impl<W: Write + ?Sized, const N: usize> AsciiPush for BufferedWriter<'_, W, N> {
#[inline(always)]
fn push_ascii(&mut self, bytes: &[u8]) -> fmt::Result {
match self.push_bytes(bytes) {
Some(()) => Ok(()),
None => self.push_ascii_after_flush(bytes),
}
}
}
impl<W: Write + ?Sized, const N: usize> Write for BufferedWriter<'_, W, N> {
#[inline(always)]
fn write_str(&mut self, text: &str) -> fmt::Result {
match self.push_bytes(text.as_bytes()) {
Some(()) => Ok(()),
None => self.write_str_after_flush(text),
}
}
}
impl AsciiPush for String {
fn push_ascii(&mut self, bytes: &[u8]) -> fmt::Result {
self.push_str(str::from_utf8(bytes).map_err(|_| fmt::Error)?);
Ok(())
}
}
#[derive(Clone, Copy)]
pub(crate) struct DurationParts {
pub neg: bool,
pub weeks: u32,
pub days: u32,
pub hours: u32,
pub minutes: u32,
pub seconds: u32,
}
pub(crate) trait BufferedWrite: Write {
fn write_buffered<const N: usize>(
&mut self,
push: impl FnOnce(&mut BufferedWriter<'_, Self, N>) -> fmt::Result,
) -> fmt::Result {
let mut buf = BufferedWriter::new(self);
push(&mut buf)?;
buf.flush()
}
}
impl<W: Write + ?Sized> BufferedWrite for W {}
+101
View File
@@ -0,0 +1,101 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use crate::common::{IanaString, IanaType};
impl<I, O> IanaType<I, O> {
pub fn iana(&self) -> Option<&I> {
match self {
IanaType::Iana(iana) => Some(iana),
_ => None,
}
}
pub fn other(&self) -> Option<&O> {
match self {
IanaType::Other(s) => Some(s),
_ => None,
}
}
pub fn into_iana(self) -> Option<I> {
match self {
IanaType::Iana(iana) => Some(iana),
_ => None,
}
}
pub fn is_iana(&self) -> bool {
matches!(self, IanaType::Iana(_))
}
pub fn is_iana_and(&self, f: impl FnOnce(&I) -> bool) -> bool {
match &self {
IanaType::Iana(x) => f(x),
_ => false,
}
}
}
impl<I, O> AsRef<str> for IanaType<I, O>
where
I: IanaString,
O: AsRef<str>,
{
fn as_ref(&self) -> &str {
match self {
IanaType::Iana(iana) => iana.as_str(),
IanaType::Other(s) => s.as_ref(),
}
}
}
impl<I> IanaType<I, String>
where
I: IanaString,
{
pub fn into_string(self) -> Cow<'static, str> {
match self {
IanaType::Iana(iana) => Cow::Borrowed(iana.as_str()),
IanaType::Other(s) => Cow::Owned(s),
}
}
}
impl<I> From<I> for IanaType<I, String>
where
I: IanaString,
{
fn from(iana: I) -> Self {
IanaType::Iana(iana)
}
}
impl<I> From<String> for IanaType<I, String> {
fn from(s: String) -> Self {
IanaType::Other(s)
}
}
impl<I, O> Default for IanaType<I, O>
where
I: IanaString
+ std::fmt::Debug
+ Clone
+ PartialEq
+ Eq
+ PartialOrd
+ Ord
+ std::hash::Hash
+ Default,
O: std::fmt::Debug + Clone + PartialEq + Eq + PartialOrd + Ord + std::hash::Hash,
{
fn default() -> Self {
IanaType::Iana(I::default())
}
}
+79
View File
@@ -0,0 +1,79 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use jmap_tools::{Element, JsonPointer, JsonPointerItem, Property, Value};
use std::{io::Write, iter::repeat_n};
pub(crate) mod ordered;
pub(crate) mod text;
const KEY_CAPACITY: usize = 16;
macro_rules! property_names {
($property:ident, $($name:literal => $variant:ident,)*) => {
pub(crate) fn parse_name(s: &str) -> Option<Self> {
hashify::fnc_map!(s.as_bytes(),
$($name => Some($property::$variant),)*
"metadata" => Some($property::Metadata),
"privateMetadata" => Some($property::PrivateMetadata),
_ => None,
)
}
pub(crate) fn parse_nested_name(s: &str) -> Option<Self> {
hashify::fnc_map!(s.as_bytes(),
$($name => Some($property::$variant),)*
_ => None,
)
}
};
}
pub(crate) use property_names;
pub(crate) trait JSPropPointer<P: Property> {
fn parse_jsprop_value<E: Element<Property = P>>(
&self,
json: &str,
) -> Option<Value<'static, P, E>>;
}
impl<P: Property> JSPropPointer<P> for JsonPointer<P> {
fn parse_jsprop_value<E: Element<Property = P>>(
&self,
json: &str,
) -> Option<Value<'static, P, E>> {
let items = self.as_slice();
if items
.iter()
.any(|item| matches!(item, JsonPointerItem::Root | JsonPointerItem::Wildcard))
{
return Value::parse_json(json).ok().map(Value::into_owned);
}
let mut wrapped = Vec::with_capacity(json.len() + items.len() * KEY_CAPACITY);
for item in items {
wrapped.push(b'{');
match item {
JsonPointerItem::Key(key) => serde_json::to_writer(&mut wrapped, key).ok()?,
JsonPointerItem::Number(number) => write!(wrapped, "\"{number}\"").ok()?,
JsonPointerItem::Root | JsonPointerItem::Wildcard | JsonPointerItem::Invalid(_) => {
return None;
}
}
wrapped.push(b':');
}
wrapped.extend_from_slice(json.as_bytes());
wrapped.extend(repeat_n(b'}', items.len()));
let mut value = Value::<P, E>::parse_json(std::str::from_utf8(&wrapped).ok()?).ok()?;
for _ in items {
let [(_, inner)] = <[_; 1]>::try_from(value.into_object()?.into_vec()).ok()?;
value = inner;
}
Some(value.into_owned())
}
}
+174
View File
@@ -0,0 +1,174 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use ahash::{AHashMap, RandomState};
use std::{borrow::Borrow, hash::Hash, vec::IntoIter};
pub(crate) const INDEX_THRESHOLD: usize = 16;
#[derive(Debug)]
pub(crate) struct OrderedMap<K, V> {
entries: Vec<(K, V)>,
index: Option<Box<HashIndex>>,
}
#[derive(Debug)]
struct HashIndex {
state: RandomState,
positions: AHashMap<u64, usize>,
}
impl<K, V> Default for OrderedMap<K, V> {
fn default() -> Self {
Self {
entries: Vec::new(),
index: None,
}
}
}
impl<K: Hash + Eq, V> OrderedMap<K, V> {
pub(crate) fn len(&self) -> usize {
self.entries.len()
}
pub(crate) fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub(crate) fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
self.entries.iter().map(|(key, value)| (key, value))
}
pub(crate) fn contains_key<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self.position(key).is_some()
}
pub(crate) fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let position = self.position(key)?;
self.entries.get_mut(position).map(|(_, value)| value)
}
pub(crate) fn push(&mut self, key: K, value: V) {
if let Some(index) = &mut self.index {
index.insert(&key, self.entries.len());
}
self.entries.push((key, value));
if self.index.is_none() {
self.index = HashIndex::build(&self.entries);
}
}
pub(crate) fn insert(&mut self, key: K, value: V) {
match self.get_mut(&key) {
Some(existing) => *existing = value,
None => self.push(key, value),
}
}
pub(crate) fn insert_if_absent(&mut self, key: K, value: V) {
if !self.contains_key(&key) {
self.push(key, value);
}
}
pub(crate) fn upsert(
&mut self,
key: K,
value: impl FnOnce() -> V,
update: impl FnOnce(&mut V),
) {
match self.get_mut(&key) {
Some(existing) => update(existing),
None => {
let mut value = value();
update(&mut value);
self.push(key, value);
}
}
}
pub(crate) fn retain(&mut self, mut keep: impl FnMut(&K, &mut V) -> bool) {
let len = self.entries.len();
self.entries.retain_mut(|(key, value)| keep(key, value));
if self.entries.len() != len {
self.index = HashIndex::build(&self.entries);
}
}
fn position<Q>(&self, key: &Q) -> Option<usize>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let Some(index) = &self.index else {
return self.scan(key);
};
let position = index.get(key)?;
match self.entries.get(position) {
Some((item, _)) if item.borrow() == key => Some(position),
_ => self.scan(key),
}
}
fn scan<Q>(&self, key: &Q) -> Option<usize>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self.entries
.iter()
.position(|(item, _)| item.borrow() == key)
}
}
impl<K, V> IntoIterator for OrderedMap<K, V> {
type Item = (K, V);
type IntoIter = IntoIter<(K, V)>;
fn into_iter(self) -> Self::IntoIter {
self.entries.into_iter()
}
}
impl HashIndex {
#[inline]
fn build<K: Hash, V>(entries: &[(K, V)]) -> Option<Box<Self>> {
(entries.len() > INDEX_THRESHOLD).then(|| Self::index(entries))
}
#[cold]
#[inline(never)]
fn index<K: Hash, V>(entries: &[(K, V)]) -> Box<Self> {
let mut index = Box::new(HashIndex {
state: RandomState::new(),
positions: AHashMap::with_capacity(entries.len()),
});
for (position, (key, _)) in entries.iter().enumerate() {
index.insert(key, position);
}
index
}
fn insert<Q: Hash + ?Sized>(&mut self, key: &Q, position: usize) {
self.positions
.entry(self.state.hash_one(key))
.or_insert(position);
}
fn get<Q: Hash + ?Sized>(&self, key: &Q) -> Option<usize> {
self.positions.get(&self.state.hash_one(key)).copied()
}
}
+287
View File
@@ -0,0 +1,287 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::common::format::AsciiPush;
use jmap_tools::{JsonPointer, JsonPointerItem, Key, Property};
use std::{
borrow::Cow,
fmt::{self, Display, Write},
};
const POINTER_CAPACITY: usize = 48;
const CONVERTED_KEYS_CAPACITY: usize = 4;
pub(crate) trait PointerText: Write {
fn reserve_escapes(&mut self, _escapes: usize) {}
fn push_pointer_token(&mut self, token: &str) -> fmt::Result {
let escapes = token
.bytes()
.filter(|byte| matches!(byte, b'~' | b'/'))
.count();
if escapes == 0 {
return self.write_str(token);
}
self.reserve_escapes(escapes);
for piece in token.split_inclusive(['~', '/']) {
match piece.strip_suffix('~') {
Some(head) => {
self.write_str(head)?;
self.write_str("~0")?;
}
None => match piece.strip_suffix('/') {
Some(head) => {
self.write_str(head)?;
self.write_str("~1")?;
}
None => self.write_str(piece)?,
},
}
}
Ok(())
}
}
pub(crate) trait PointerString: Sized {
fn from_pointer<'x>(parts: impl IntoIterator<Item = &'x str> + Clone) -> Self;
}
pub(crate) trait PointerTextExt<P: Property> {
fn to_text(&self) -> String;
fn text_eq(&self, text: &str) -> bool;
}
pub(crate) trait DisplayEq {
fn display_eq(&self, text: &str) -> bool;
}
pub(crate) trait IdReferenceText {
fn to_id_reference(&self) -> String;
}
pub(crate) trait IntoAsciiLowercase {
fn into_ascii_lowercase(self) -> String;
}
pub(crate) trait PointerProperty: Property {
fn into_pointer(self) -> Result<JsonPointer<Self>, Self>;
}
pub(crate) trait ConvertedKeys<P: Property> {
fn into_converted_keys(self) -> Vec<Key<'static, P>>;
}
pub(crate) struct Expect<'x>(&'x [u8]);
pub(crate) struct AsciiString(String);
impl PointerText for String {
fn reserve_escapes(&mut self, escapes: usize) {
self.reserve_exact(self.capacity() - self.len() + escapes);
}
}
impl PointerString for String {
fn from_pointer<'x>(parts: impl IntoIterator<Item = &'x str> + Clone) -> Self {
let len = parts
.clone()
.into_iter()
.map(|part| part.len() + 1)
.sum::<usize>();
let mut text = String::with_capacity(len.saturating_sub(1));
for (position, part) in parts.into_iter().enumerate() {
if position > 0 {
text.push('/');
}
let _ = text.push_pointer_token(part);
}
text
}
}
impl<P: Property> PointerTextExt<P> for JsonPointer<P> {
fn to_text(&self) -> String {
let mut text = String::with_capacity(POINTER_CAPACITY);
let _ = write!(text, "{self}");
text
}
fn text_eq(&self, text: &str) -> bool {
if text.bytes().any(|byte| matches!(byte, b'/' | b'~')) {
return self.display_eq(text);
}
match self.as_slice() {
[] | [JsonPointerItem::Root] => text.is_empty(),
[JsonPointerItem::Wildcard] => text == "*",
[JsonPointerItem::Invalid(invalid)] => invalid == text,
[JsonPointerItem::Key(key)] => *key == text,
[JsonPointerItem::Number(number)] => number.display_eq(text),
_ => false,
}
}
}
impl<T: Display + ?Sized> DisplayEq for T {
fn display_eq(&self, text: &str) -> bool {
let mut expect = Expect::new(text);
write!(expect, "{self}").is_ok() && expect.is_complete()
}
}
impl IdReferenceText for str {
fn to_id_reference(&self) -> String {
let mut text = String::with_capacity(self.len() + 1);
text.push('#');
text.push_str(self);
text
}
}
impl IntoAsciiLowercase for Cow<'_, str> {
fn into_ascii_lowercase(self) -> String {
let mut text = self.into_owned();
text.make_ascii_lowercase();
text
}
}
impl AsciiString {
pub(crate) fn with_capacity(capacity: usize) -> Self {
AsciiString(String::with_capacity(capacity))
}
pub(crate) fn into_string(self) -> String {
self.0
}
}
impl From<String> for AsciiString {
fn from(text: String) -> Self {
AsciiString(text)
}
}
impl AsciiPush for AsciiString {
#[inline(always)]
fn push_ascii(&mut self, bytes: &[u8]) -> fmt::Result {
self.0.extend(bytes.iter().copied().map(char::from));
Ok(())
}
}
impl<'x> Expect<'x> {
pub(crate) fn new(text: &'x str) -> Self {
Expect(text.as_bytes())
}
pub(crate) fn is_complete(&self) -> bool {
self.0.is_empty()
}
}
impl AsciiPush for Expect<'_> {
#[inline(always)]
fn push_ascii(&mut self, bytes: &[u8]) -> fmt::Result {
self.0 = self.0.strip_prefix(bytes).ok_or(fmt::Error)?;
Ok(())
}
}
impl Write for Expect<'_> {
#[inline(always)]
fn write_str(&mut self, text: &str) -> fmt::Result {
self.push_ascii(text.as_bytes())
}
}
impl<P: PointerProperty> ConvertedKeys<P> for Key<'_, P> {
fn into_converted_keys(self) -> Vec<Key<'static, P>> {
match self {
Key::Property(property) => match property.into_pointer() {
Ok(pointer) => pointer.into_converted_keys(),
Err(property) => property.to_cow().as_ref().into_converted_keys(),
},
Key::Borrowed(text) => text.into_converted_keys(),
Key::Owned(text) => text.as_str().into_converted_keys(),
}
}
}
impl<P: PointerProperty> ConvertedKeys<P> for JsonPointer<P> {
fn into_converted_keys(self) -> Vec<Key<'static, P>> {
let mut keys = Vec::with_capacity(self.len());
for item in self.into_iter() {
match item {
JsonPointerItem::Key(key) => {
match key.as_string_key() {
Some(text) if text.contains('/') => {
keys.extend(JsonPointer::<P>::parse(text).into_iter().filter_map(
|item| match item {
JsonPointerItem::Key(key) => Some(key),
JsonPointerItem::Number(number) => {
Some(Key::Owned(number.to_string()))
}
JsonPointerItem::Root
| JsonPointerItem::Wildcard
| JsonPointerItem::Invalid(_) => None,
},
));
}
_ => keys.push(key),
}
}
JsonPointerItem::Number(number) => keys.push(Key::Owned(number.to_string())),
JsonPointerItem::Root | JsonPointerItem::Wildcard | JsonPointerItem::Invalid(_) => {
}
}
}
keys
}
}
impl<P: PointerProperty> ConvertedKeys<P> for &str {
fn into_converted_keys(self) -> Vec<Key<'static, P>> {
if self.contains('~') {
return JsonPointer::<P>::parse(self).into_converted_keys();
}
let mut keys: Vec<Key<'static, P>> = Vec::with_capacity(CONVERTED_KEYS_CAPACITY);
let mut parent_is_key = false;
let first = usize::from(self.starts_with('/'));
let mut segments = self.split('/').enumerate();
for (position, segment) in segments.by_ref() {
let parent = if parent_is_key { keys.last() } else { None };
let (key, is_key) = match segment {
"" if position == 0 => continue,
"" => (Key::from(""), true),
"*" => {
parent_is_key = false;
continue;
}
_ => match P::try_parse(parent, segment) {
Some(property) => (Key::Property(property), true),
None => (
Key::Owned(segment.to_string()),
!segment.bytes().all(|byte| byte.is_ascii_digit())
|| (segment != "0" && segment.starts_with('0'))
|| segment.parse::<u64>().is_err(),
),
},
};
let opaque = position == first
&& key
.as_property()
.is_some_and(|property| property.is_opaque());
keys.push(key);
parent_is_key = is_key;
if opaque {
break;
}
}
keys.extend(segments.map(|(_, segment)| Key::Owned(segment.to_string())));
keys
}
}
+296
View File
@@ -0,0 +1,296 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use jiff::{civil, tz::Offset};
use mail_parser::DateTime;
#[cfg(feature = "rkyv")]
pub mod archive;
#[cfg(feature = "jmap")]
pub mod blob;
pub(crate) mod decode;
#[cfg(feature = "jmap")]
pub(crate) mod elements;
pub mod embedded;
#[cfg(feature = "jmap")]
pub mod export;
pub(crate) mod format;
pub mod iana;
#[cfg(feature = "jmap")]
pub(crate) mod jsprop;
pub mod parser;
pub(crate) mod scan;
pub(crate) mod stack;
pub mod timezone;
pub mod tokenizer;
pub mod types;
mod tzdb;
pub mod writer;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum IanaType<I, O> {
Iana(I),
Other(O),
}
pub trait IanaString {
fn as_str(&self) -> &'static str;
}
pub trait IanaParse: Sized {
fn parse(value: &[u8]) -> Option<Self>;
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub struct PartialDateTime {
pub year: Option<u16>,
pub month: Option<u8>,
pub day: Option<u8>,
pub hour: Option<u8>,
pub minute: Option<u8>,
pub second: Option<u8>,
pub tz_hour: Option<u8>,
pub tz_minute: Option<u8>,
pub tz_minus: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum CalendarScale {
#[default]
Gregorian,
Chinese,
IslamicCivil,
Hebrew,
Ethiopic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Encoding {
QuotedPrintable,
Base64,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub struct Data {
pub content_type: Option<String>,
pub data: Vec<u8>,
}
#[cfg(feature = "rkyv")]
impl PartialEq<Box<Data>> for ArchivedData {
fn eq(&self, other: &Box<Data>) -> bool {
self == other.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DateTimeResult {
pub date_time: civil::DateTime,
pub offset: Option<Offset>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum LinkRelation {
About,
Acl,
Alternate,
Amphtml,
ApiCatalog,
Appendix,
AppleTouchIcon,
AppleTouchStartupImage,
Archives,
Author,
BlockedBy,
Bookmark,
C2paManifest,
Canonical,
Chapter,
CiteAs,
Collection,
CompressionDictionary,
Contents,
Convertedfrom,
Copyright,
CreateForm,
Current,
Deprecation,
Describedby,
Describes,
Disclosure,
DnsPrefetch,
Duplicate,
Edit,
EditForm,
EditMedia,
Enclosure,
External,
First,
Geofeed,
Glossary,
Help,
Hosts,
Hub,
IceServer,
Icon,
Index,
Intervalafter,
Intervalbefore,
Intervalcontains,
Intervaldisjoint,
Intervalduring,
Intervalequals,
Intervalfinishedby,
Intervalfinishes,
Intervalin,
Intervalmeets,
Intervalmetby,
Intervaloverlappedby,
Intervaloverlaps,
Intervalstartedby,
Intervalstarts,
Item,
Last,
LatestVersion,
License,
Linkset,
Lrdd,
Manifest,
MaskIcon,
Me,
MediaFeed,
Memento,
Micropub,
Modulepreload,
Monitor,
MonitorGroup,
Next,
NextArchive,
Nofollow,
Noopener,
Noreferrer,
Opener,
Openid2LocalId,
Openid2Provider,
Original,
P3pv1,
Payment,
Pingback,
Preconnect,
PredecessorVersion,
Prefetch,
Preload,
Prerender,
Prev,
Preview,
Previous,
PrevArchive,
PrivacyPolicy,
Profile,
Publication,
RdapActive,
RdapBottom,
RdapDown,
RdapTop,
RdapUp,
Related,
Restconf,
Replies,
Ruleinput,
Search,
Section,
Self_,
Service,
ServiceDesc,
ServiceDoc,
ServiceMeta,
SipTrunkingCapability,
Sponsored,
Start,
Status,
Stylesheet,
Subsection,
SuccessorVersion,
Sunset,
Tag,
TermsOfService,
Timegate,
Timemap,
Type,
Ugc,
Up,
VersionHistory,
Via,
Webmention,
WorkingCopy,
WorkingCopyOf,
}
#[cfg(feature = "jmap")]
pub(crate) enum IdReference<T> {
Value(T),
Reference(String),
Error,
}
#[cfg(feature = "jmap")]
impl<T: std::str::FromStr> IdReference<T> {
pub fn parse(s: &str) -> Self {
if let Some(reference) = s.strip_prefix('#') {
IdReference::Reference(reference.to_string())
} else {
T::from_str(s)
.map(IdReference::Value)
.unwrap_or(IdReference::Error)
}
}
}
+534
View File
@@ -0,0 +1,534 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{Data, Encoding, PartialDateTime, tokenizer::Token};
use crate::{
Parser,
common::{IanaParse, IanaString, IanaType},
icalendar::Uri,
};
use encodify::hex;
use mail_parser::DateTime;
use std::{borrow::Cow, iter::Peekable, slice::Iter, str::FromStr};
impl<'x> Parser<'x> {
pub(crate) fn raw_token(&mut self) -> Option<Cow<'x, str>> {
let (first, last) = (self.token_buf.first()?, self.token_buf.last()?);
self.source.get(first.start..=last.end).map(unfold)
}
pub(crate) fn buf_parse_one<T: From<Token<'x>>>(&mut self) -> Option<T> {
let result = self.token_buf.pop().map(T::from);
self.token_buf.clear();
result
}
}
impl Token<'_> {
pub(crate) fn into_uri_bytes(self) -> std::result::Result<Data, String> {
match Data::try_parse(self.text.as_ref()) {
Some(data) => Ok(data),
None => Err(self.into_string()),
}
}
pub(crate) fn into_timestamp(
self,
require_time: bool,
) -> std::result::Result<PartialDateTime, String> {
match PartialDateTime::from_timestamp_bytes(&self.text, require_time) {
(dt, true) => Ok(dt),
(_, false) => Err(self.into_string()),
}
}
pub(crate) fn into_offset(self) -> std::result::Result<PartialDateTime, String> {
let mut dt = PartialDateTime::default();
dt.parse_zone(&mut self.text.iter().peekable());
if !dt.is_null() {
Ok(dt)
} else {
Err(self.into_string())
}
}
pub(crate) fn into_float(self) -> std::result::Result<f64, String> {
if let Ok(float) = self.text.as_str().parse::<f64>()
&& float.is_finite()
{
return Ok(float);
}
Err(self.into_string())
}
pub(crate) fn into_integer(self) -> std::result::Result<i64, String> {
if let Ok(integer) = self.text.as_str().parse::<i64>() {
return Ok(integer);
}
Err(self.into_string())
}
pub(crate) fn into_boolean(self) -> bool {
self.text.as_ref().eq_ignore_ascii_case(b"true")
}
}
impl Data {
pub fn try_parse(text: &[u8]) -> Option<Self> {
const BASE64: &[u8] = b"base64";
let (scheme, text) = text.split_at_checked(5)?;
if !scheme.eq_ignore_ascii_case(b"data:") {
return None;
}
let mut parts = text.splitn(2, |ch| *ch == b',');
let header = parts.next()?;
let data = parts.next().filter(|data| !data.is_empty())?;
let header = header.strip_suffix(b"\\").unwrap_or(header);
let mut segments = header.rsplitn(2, |ch| *ch == b';');
let (media_type, is_base64) = match (segments.next(), segments.next()) {
(Some(last), media_type) if last.eq_ignore_ascii_case(BASE64) => {
(media_type.unwrap_or_default(), true)
}
_ => (header, false),
};
Some(Data {
content_type: std::str::from_utf8(media_type)
.ok()
.filter(|media_type| !media_type.is_empty())
.map(str::to_string),
data: if is_base64 {
Encoding::Base64.decode(data)?.into_owned()
} else {
hex::PERCENT.decode(data).ok()?.into_owned()
},
})
}
}
impl PartialDateTime {
pub(crate) fn from_timestamp_bytes(bytes: &[u8], require_time: bool) -> (Self, bool) {
if let Some(dt) = Self::from_basic_format(bytes) {
let valid = !require_time || dt.has_time();
(dt, valid)
} else {
let mut dt = Self::default();
let valid = dt.parse_timestamp(&mut bytes.iter().peekable(), require_time);
(dt, valid)
}
}
fn from_basic_format(bytes: &[u8]) -> Option<Self> {
let (date, time) = bytes.split_first_chunk::<8>()?;
let mut dt = Self::from_basic_date(date)?;
match time {
[] => {}
[b'T' | b't', h0, h1, m0, m1, s0, s1, zone @ ..] => {
dt.hour = Some(Self::two_digits(*h0, *h1)?);
dt.minute = Some(Self::two_digits(*m0, *m1)?);
dt.second = Some(Self::two_digits(*s0, *s1)?);
match zone {
[] => {}
[b'Z' | b'z'] => {
dt.tz_hour = Some(0);
dt.tz_minute = Some(0);
}
_ => return None,
}
}
_ => return None,
}
Some(dt)
}
pub(crate) fn from_basic_date(&[y0, y1, y2, y3, m0, m1, d0, d1]: &[u8; 8]) -> Option<Self> {
Some(PartialDateTime {
year: Some(
u16::from(Self::two_digits(y0, y1)?) * 100 + u16::from(Self::two_digits(y2, y3)?),
),
month: Some(Self::two_digits(m0, m1)?),
day: Some(Self::two_digits(d0, d1)?),
..Default::default()
})
}
#[inline(always)]
fn two_digits(tens: u8, ones: u8) -> Option<u8> {
let (tens, ones) = (tens.wrapping_sub(b'0'), ones.wrapping_sub(b'0'));
(tens < 10 && ones < 10).then(|| tens * 10 + ones)
}
pub fn parse_timestamp(
&mut self,
iter: &mut Peekable<Iter<'_, u8>>,
require_time: bool,
) -> bool {
let mut idx = 0;
for ch in iter {
match ch {
b'0'..=b'9' => {
let value = match idx {
0..=3 => {
if let Some(value) = &mut self.year {
*value =
value.saturating_mul(10).saturating_add((ch - b'0') as u16);
} else {
self.year = Some((ch - b'0') as u16);
}
idx += 1;
continue;
}
4..=5 => &mut self.month,
6..=7 => &mut self.day,
9..=10 => &mut self.hour,
11..=12 => &mut self.minute,
13..=14 => &mut self.second,
16..=17 => &mut self.tz_hour,
18..=19 => &mut self.tz_minute,
_ => return false,
};
if let Some(value) = value {
*value = value.saturating_mul(10).saturating_add(ch - b'0');
} else {
*value = Some(ch - b'0');
}
}
b'T' | b't' if idx == 8 => {}
b'+' if idx == 15 => {}
b'Z' | b'z' if idx == 15 => {
self.tz_hour = Some(0);
self.tz_minute = Some(0);
}
b'-' if idx == 15 => {
self.tz_minus = true;
}
b' ' | b'\t' | b'\r' | b'\n' => {
continue;
}
_ => break,
}
idx += 1;
}
if self.hour.is_some() {
self.minute.get_or_insert(0);
self.second.get_or_insert(0);
}
self.has_date() && (!require_time || self.has_time())
}
pub(crate) fn parse_zone(&mut self, iter: &mut Peekable<Iter<'_, u8>>) -> bool {
self.tz_minus = match iter.peek() {
Some(b'-') => true,
Some(b'+') => false,
Some(b'Z') | Some(b'z') => {
self.tz_hour = Some(0);
self.tz_minute = Some(0);
iter.next();
return true;
}
_ => return false,
};
iter.next();
let mut idx = 0;
for ch in iter {
match ch {
b'0'..=b'9' => {
idx += 1;
let value = match idx {
1 | 2 => &mut self.tz_hour,
3 | 4 => &mut self.tz_minute,
_ => return false,
};
if let Some(value) = value {
*value = value.saturating_mul(10).saturating_add(ch - b'0');
} else {
*value = Some(ch - b'0');
}
}
_ => {
if !ch.is_ascii_whitespace() {
return false;
}
}
}
}
self.tz_hour.is_some()
}
pub fn is_null(&self) -> bool {
self.year.is_none()
&& self.month.is_none()
&& self.day.is_none()
&& self.hour.is_none()
&& self.minute.is_none()
&& self.second.is_none()
&& self.tz_hour.is_none()
&& self.tz_minute.is_none()
}
#[inline(always)]
pub fn has_date(&self) -> bool {
self.year.is_some() && self.month.is_some() && self.day.is_some()
}
#[inline(always)]
pub fn has_time(&self) -> bool {
self.hour.is_some() && self.minute.is_some()
}
#[inline(always)]
pub fn has_zone(&self) -> bool {
self.tz_hour.is_some()
}
#[inline(always)]
pub fn has_date_and_time(&self) -> bool {
self.has_date() && self.has_time()
}
fn to_datetime(&self) -> Option<DateTime> {
if self.has_date() && self.has_time() {
DateTime {
year: self.year.unwrap(),
month: self.month.unwrap(),
day: self.day.unwrap(),
hour: self.hour.unwrap(),
minute: self.minute.unwrap(),
second: self.second.unwrap_or_default(),
tz_before_gmt: self.tz_minus,
tz_hour: self.tz_hour.unwrap_or_default(),
tz_minute: self.tz_minute.unwrap_or_default(),
}
.into()
} else {
None
}
}
pub fn to_rfc3339(&self) -> Option<String> {
self.to_datetime().map(|dt| dt.to_rfc3339())
}
pub fn to_timestamp(&self) -> Option<i64> {
self.to_datetime().map(|dt| dt.to_timestamp())
}
}
pub(crate) fn parse_digits(
iter: &mut Peekable<Iter<'_, u8>>,
target: &mut Option<u16>,
num: usize,
nullable: bool,
) -> bool {
let mut idx = 0;
while let Some(ch) = iter.peek() {
match ch {
b'0'..=b'9' => {
let ch = (*ch - b'0') as u16;
idx += 1;
iter.next();
if let Some(target) = target {
*target = target.saturating_mul(10).saturating_add(ch);
if idx == num {
return true;
}
} else {
*target = Some(ch);
}
}
b'-' if nullable => {
idx += 1;
iter.next();
if idx == num / 2 {
return true;
}
}
_ => {
if !ch.is_ascii_whitespace() {
return false;
} else {
iter.next();
}
}
};
}
false
}
pub(crate) fn parse_small_digits(
iter: &mut Peekable<Iter<'_, u8>>,
target: &mut Option<u8>,
num: usize,
nullable: bool,
) -> bool {
let mut idx = 0;
while let Some(ch) = iter.peek() {
match ch {
b'0'..=b'9' => {
let ch = *ch - b'0';
idx += 1;
iter.next();
if let Some(target) = target {
*target = target.saturating_mul(10).saturating_add(ch);
if idx == num {
return true;
}
} else {
*target = Some(ch);
}
}
b'-' if nullable => {
idx += 1;
iter.next();
if idx == num / 2 {
return true;
}
}
_ => {
if !ch.is_ascii_whitespace() {
return false;
} else {
iter.next();
}
}
};
}
false
}
pub(crate) fn unfold(text: &str) -> Cow<'_, str> {
if !text.as_bytes().contains(&b'\n') {
return Cow::Borrowed(text);
}
let mut unfolded = String::with_capacity(text.len());
for (pos, chunk) in text.split('\n').enumerate() {
let chunk = if pos == 0 {
chunk
} else {
chunk.strip_prefix([' ', '\t']).unwrap_or(chunk)
};
unfolded.push_str(chunk.strip_suffix('\r').unwrap_or(chunk));
}
Cow::Owned(unfolded)
}
#[derive(Default)]
pub(crate) struct Timestamp(pub i64);
impl FromStr for Timestamp {
type Err = ();
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Timestamp::parse(s.as_bytes()).ok_or(())
}
}
impl IanaParse for Timestamp {
fn parse(value: &[u8]) -> Option<Self> {
PartialDateTime::from_timestamp_bytes(value, true)
.0
.to_timestamp()
.map(Timestamp)
}
}
pub(crate) struct Integer(pub i64);
impl IanaParse for Integer {
fn parse(value: &[u8]) -> Option<Self> {
let mut num: i64 = 0;
let mut is_negative = false;
for (pos, ch) in value.iter().enumerate() {
match ch {
b'-' if pos == 0 => {
is_negative = true;
}
b'+' if pos == 0 => {}
b'0'..=b'9' => {
num = num.saturating_mul(10).saturating_add((*ch - b'0') as i64);
}
_ => {
if !ch.is_ascii_whitespace() {
return None;
}
}
}
}
Some(Integer(if is_negative { -num } else { num }))
}
}
pub(crate) struct Boolean(pub bool);
impl IanaParse for Boolean {
fn parse(value: &[u8]) -> Option<Self> {
if value.eq_ignore_ascii_case(b"true") {
Some(Boolean(true))
} else if value.eq_ignore_ascii_case(b"false") {
Some(Boolean(false))
} else {
None
}
}
}
impl<I> From<Token<'_>> for IanaType<I, String>
where
I: IanaParse,
{
fn from(value: Token<'_>) -> Self {
I::parse(value.text.as_ref())
.map(IanaType::Iana)
.unwrap_or_else(|| IanaType::Other(value.into_string()))
}
}
impl<I> From<Token<'_>> for IanaType<I, Uri>
where
I: IanaString
+ IanaParse
+ std::fmt::Debug
+ Clone
+ PartialEq
+ Eq
+ PartialOrd
+ Ord
+ std::hash::Hash,
{
fn from(value: Token<'_>) -> Self {
I::parse(value.text.as_ref())
.map(IanaType::Iana)
.unwrap_or_else(|| {
IanaType::Other(
value
.into_uri_bytes()
.map(Uri::from)
.unwrap_or_else(Uri::Location),
)
})
}
}
+108
View File
@@ -0,0 +1,108 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use memchr::{memchr2, memchr3};
pub(crate) const EOL: u16 = 1;
pub(crate) const WHITESPACE: u16 = 1 << 1;
pub(crate) const BACKSLASH: u16 = 1 << 2;
pub(crate) const QUOTE: u16 = 1 << 3;
pub(crate) const COLON: u16 = 1 << 4;
pub(crate) const SEMICOLON: u16 = 1 << 5;
pub(crate) const COMMA: u16 = 1 << 6;
pub(crate) const EQUAL: u16 = 1 << 7;
pub(crate) const DOT: u16 = 1 << 8;
pub(crate) const CONTROL: u16 = 1 << 9;
pub(crate) const CARET: u16 = 1 << 10;
const LINE_ESCAPES: u16 = EOL | BACKSLASH;
static CLASS: [u16; 256] = class_table();
const fn class_table() -> [u16; 256] {
let mut table = [0u16; 256];
let mut byte = 0;
while byte < table.len() {
table[byte] = match byte as u8 {
b'\r' | b'\n' => EOL,
b' ' | b'\t' => WHITESPACE,
b'\\' => BACKSLASH,
b'"' => QUOTE,
b':' => COLON,
b';' => SEMICOLON,
b',' => COMMA,
b'=' => EQUAL,
b'.' => DOT,
b'^' => CARET,
0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F | 0x7F => CONTROL,
_ => 0,
};
byte += 1;
}
table
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Mask(u16);
impl Mask {
#[inline(always)]
pub(crate) const fn new(classes: u16) -> Self {
Mask(classes | EOL)
}
#[inline(always)]
pub(crate) fn class_of(self, byte: u8) -> u16 {
CLASS[byte as usize] & self.0
}
#[inline(always)]
pub(crate) fn plain_len(self, bytes: &[u8]) -> usize {
match bytes.first() {
Some(&byte) if self.class_of(byte) == 0 => {
if self.0 & !LINE_ESCAPES != 0 {
self.class_scan(bytes)
} else if self.0 & BACKSLASH != 0 {
memchr3(b'\r', b'\n', b'\\', bytes).unwrap_or(bytes.len())
} else {
memchr2(b'\r', b'\n', bytes).unwrap_or(bytes.len())
}
}
_ => 0,
}
}
#[inline(always)]
fn class_scan(self, bytes: &[u8]) -> usize {
let (chunks, tail) = bytes.as_chunks::<4>();
for (chunk_idx, &[a, b, c, d]) in chunks.iter().enumerate() {
let (a, b, c, d) = (
self.class_of(a),
self.class_of(b),
self.class_of(c),
self.class_of(d),
);
if a | b | c | d != 0 {
return chunk_idx * 4
+ if a != 0 {
0
} else if b != 0 {
1
} else if c != 0 {
2
} else {
3
};
}
}
chunks.len() * 4
+ tail
.iter()
.position(|&byte| self.class_of(byte) != 0)
.unwrap_or(tail.len())
}
}
+43
View File
@@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
const INLINE_DEPTH: usize = 8;
pub(crate) struct ComponentStack<T> {
inline: [Option<T>; INLINE_DEPTH],
len: usize,
spilled: Vec<T>,
}
impl<T> ComponentStack<T> {
pub(crate) fn new() -> Self {
Self {
inline: [const { None }; INLINE_DEPTH],
len: 0,
spilled: Vec::new(),
}
}
pub(crate) fn push(&mut self, item: T) {
match self.inline.get_mut(self.len) {
Some(slot) => *slot = Some(item),
None => self.spilled.push(item),
}
self.len += 1;
}
pub(crate) fn pop(&mut self) -> Option<T> {
self.len = self.len.checked_sub(1)?;
match self.inline.get_mut(self.len) {
Some(slot) => slot.take(),
None => self.spilled.pop(),
}
}
pub(crate) fn is_empty(&self) -> bool {
self.len == 0
}
}
File diff suppressed because it is too large. Load diff
+784
View File
@@ -0,0 +1,784 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Parser,
common::{
Encoding, IanaParse,
scan::{self, Mask},
},
};
use encodify::base64;
use std::ops::{Deref, Range};
const SPARE_BYTES: usize = 64;
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Token<'x> {
pub(crate) text: TokenText<'x>,
pub(crate) start: usize,
pub(crate) end: usize,
pub(crate) stop_char: StopChar,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum TokenText<'x> {
Borrowed(&'x str),
Owned(String),
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum StopChar {
Colon,
Semicolon,
Comma,
Equal,
Dot,
Lf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Mode(u16);
impl Mode {
pub(crate) const STOP_COLON: Mode = Mode(scan::COLON);
pub(crate) const STOP_SEMICOLON: Mode = Mode(scan::SEMICOLON);
pub(crate) const STOP_COMMA: Mode = Mode(scan::COMMA);
pub(crate) const STOP_EQUAL: Mode = Mode(scan::EQUAL);
pub(crate) const STOP_DOT: Mode = Mode(scan::DOT);
pub(crate) const UNFOLD_QP: Mode = Mode(1 << 11);
pub(crate) const UNFOLD_B64: Mode = Mode(1 << 12);
pub(crate) const UNQUOTE: Mode = Mode(scan::QUOTE);
pub(crate) const SKIP_WS: Mode = Mode(scan::WHITESPACE);
pub(crate) const STRIP_CTL: Mode = Mode(scan::CONTROL);
pub(crate) const UNESCAPE_CARET: Mode = Mode(scan::CARET);
pub(crate) const UNESCAPE_BACKSLASH: Mode = Mode(scan::BACKSLASH);
pub(crate) const INITIAL: Mode = Mode(
Mode::STOP_COLON.0
| Mode::STOP_SEMICOLON.0
| Mode::STOP_COMMA.0
| Mode::STOP_EQUAL.0
| Mode::UNQUOTE.0
| Mode::UNESCAPE_BACKSLASH.0,
);
const IANA_TOKEN: Mode = Mode(
Mode::STOP_COLON.0
| Mode::STOP_SEMICOLON.0
| Mode::STOP_COMMA.0
| Mode::STOP_EQUAL.0
| Mode::UNQUOTE.0
| Mode::SKIP_WS.0
| Mode::UNESCAPE_BACKSLASH.0,
);
const SINGLE_VALUE: Mode = Mode::UNESCAPE_BACKSLASH;
const MULTI_VALUE_COMMA: Mode =
Mode(Mode::STOP_COMMA.0 | Mode::SKIP_WS.0 | Mode::UNESCAPE_BACKSLASH.0);
const MULTI_VALUE_SEMICOLON: Mode = Mode(Mode::STOP_SEMICOLON.0 | Mode::UNESCAPE_BACKSLASH.0);
const MULTI_VALUE_SEMICOLON_AND_COMMA: Mode =
Mode(Mode::STOP_SEMICOLON.0 | Mode::STOP_COMMA.0 | Mode::UNESCAPE_BACKSLASH.0);
const PARAM_VALUE: Mode = Mode(
Mode::STOP_COLON.0
| Mode::STOP_SEMICOLON.0
| Mode::STOP_COMMA.0
| Mode::UNQUOTE.0
| Mode::SKIP_WS.0
| Mode::STRIP_CTL.0
| Mode::UNESCAPE_CARET.0
| Mode::UNESCAPE_BACKSLASH.0,
);
const RRULE_VALUE: Mode = Mode(
Mode::STOP_COLON.0
| Mode::STOP_COMMA.0
| Mode::STOP_EQUAL.0
| Mode::STOP_SEMICOLON.0
| Mode::SKIP_WS.0
| Mode::UNESCAPE_BACKSLASH.0,
);
const PLAIN_VALUE: Mode = Mode(Mode::UNESCAPE_BACKSLASH.0 | Mode::UNFOLD_B64.0);
const UNFOLD: u16 = Self::UNFOLD_QP.0 | Self::UNFOLD_B64.0;
const UNQUOTED_ONLY: u16 =
scan::WHITESPACE | scan::COLON | scan::SEMICOLON | scan::COMMA | scan::EQUAL | scan::DOT;
#[inline(always)]
pub(crate) const fn contains(self, flag: Mode) -> bool {
self.0 & flag.0 == flag.0
}
#[inline(always)]
pub(crate) fn set(&mut self, flag: Mode, enabled: bool) {
if enabled {
self.0 |= flag.0;
} else {
self.0 &= !flag.0;
}
}
#[inline(always)]
const fn unquoted_mask(self) -> Mask {
Mask::new(self.0 & !Self::UNFOLD)
}
#[inline(always)]
const fn quoted_mask(self) -> Mask {
Mask::new(self.0 & !Self::UNFOLD & !Self::UNQUOTED_ONLY)
}
}
impl<'x> Parser<'x> {
pub(crate) fn expect_iana_token(&mut self) {
self.mode = Mode::IANA_TOKEN;
}
pub(crate) fn expect_single_value(&mut self) {
self.mode = Mode::SINGLE_VALUE;
}
pub(crate) fn expect_multi_value_comma(&mut self) {
self.mode = Mode::MULTI_VALUE_COMMA;
}
pub(crate) fn expect_multi_value_semicolon(&mut self) {
self.mode = Mode::MULTI_VALUE_SEMICOLON;
}
pub(crate) fn expect_multi_value_semicolon_and_comma(&mut self) {
self.mode = Mode::MULTI_VALUE_SEMICOLON_AND_COMMA;
}
pub(crate) fn expect_param_value(&mut self) {
self.mode = Mode::PARAM_VALUE;
}
pub(crate) fn expect_rrule_value(&mut self) {
self.mode = Mode::RRULE_VALUE;
}
fn caret_escape(&self, idx: usize) -> Option<(usize, char)> {
let mut bytes = self.input.get(idx + 1..)?.iter().zip(idx + 1..);
while let Some((ch, pos)) = bytes.next() {
match ch {
b'\r' if matches!(self.input.get(pos + 1), Some(b'\n')) => {}
b'\n' => {
bytes.next().filter(|(ch, _)| matches!(ch, b' ' | b'\t'))?;
}
b'^' => return Some((pos, '^')),
b'n' => return Some((pos, '\n')),
b'\'' => return Some((pos, '"')),
_ => return None,
}
}
None
}
fn is_base64_continuation(&self, idx: usize) -> bool {
let mut has_data = false;
for ch in self.input.get(idx + 1..).unwrap_or_default() {
match ch {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'/' | b'=' => has_data = true,
b'\r' => {}
b'\n' => break,
_ => return false,
}
}
has_data
}
#[inline(always)]
fn borrowed(&self, start: usize, end: usize) -> TokenText<'x> {
TokenText::Borrowed(self.source.get(start..end).unwrap_or_default())
}
pub(crate) fn token(&mut self) -> Option<Token<'x>> {
let input = self.input;
let start = self.pos;
let mask = self.mode.unquoted_mask();
let rest = input.get(start..).unwrap_or_default();
let run = mask.plain_len(rest);
let end = start + run;
let Some(&ch) = rest.get(run) else {
if run == 0 {
return None;
}
self.pos = end;
self.last_token_end = end - 1;
return Some(Token {
text: self.borrowed(start, end),
start,
end: end - 1,
stop_char: StopChar::Lf,
});
};
let (stop_char, next) = match mask.class_of(ch) {
scan::COLON => (StopChar::Colon, end + 1),
scan::SEMICOLON => (StopChar::Semicolon, end + 1),
scan::COMMA => (StopChar::Comma, end + 1),
scan::EQUAL => (StopChar::Equal, end + 1),
scan::DOT => (StopChar::Dot, end + 1),
scan::EOL => {
let lf = if ch == b'\n' {
end
} else if input.get(end + 1) == Some(&b'\n') {
end + 1
} else {
return self.token_slow(start, run);
};
if matches!(input.get(lf + 1), Some(b' ' | b'\t'))
|| (run != 0
&& self.mode.contains(Mode::UNFOLD_QP)
&& input.get(end - 1) == Some(&b'='))
|| self.mode.contains(Mode::UNFOLD_B64)
{
return self.token_slow(start, run);
}
(StopChar::Lf, lf + 1)
}
scan::QUOTE if run == 0 => return self.quoted_token(start),
_ => return self.token_slow(start, run),
};
self.pos = next;
Some(if run != 0 {
self.last_token_end = end - 1;
Token {
text: self.borrowed(start, end),
start,
end: end - 1,
stop_char,
}
} else {
Token {
text: TokenText::Borrowed(""),
start: next - 1,
end: next - 1,
stop_char,
}
})
}
#[inline(always)]
fn quoted_token(&mut self, quote: usize) -> Option<Token<'x>> {
let input = self.input;
let inner_start = quote + 1;
let inner = input.get(inner_start..).unwrap_or_default();
let inner_len = self.mode.quoted_mask().plain_len(inner);
let close = inner_start + inner_len;
let stop_char = match input.get(close..close + 2) {
Some(&[b'"', stop]) => match self.mode.unquoted_mask().class_of(stop) {
scan::COLON => StopChar::Colon,
scan::SEMICOLON => StopChar::Semicolon,
scan::COMMA => StopChar::Comma,
scan::EQUAL => StopChar::Equal,
scan::DOT => StopChar::Dot,
_ => return self.token_slow(quote, 0),
},
_ => return self.token_slow(quote, 0),
};
self.pos = close + 2;
Some(if inner_len != 0 {
self.last_token_end = close - 1;
Token {
text: self.borrowed(inner_start, close),
start: inner_start,
end: close - 1,
stop_char,
}
} else {
Token {
text: TokenText::Borrowed(""),
start: close + 1,
end: close + 1,
stop_char,
}
})
}
#[inline(never)]
fn token_slow(&mut self, start: usize, run: usize) -> Option<Token<'x>> {
let input = self.input;
let source = self.source;
let unquoted_mask = self.mode.unquoted_mask();
let quoted_mask = self.mode.quoted_mask();
let mut mask = unquoted_mask;
let mut pos = start + run;
let mut offset_start = if run != 0 { start } else { usize::MAX };
let mut offset_end = if run != 0 {
start + run - 1
} else {
usize::MAX
};
let mut last_idx = 0;
let mut in_quote = false;
let mut buf = OwnedText::default();
let mut skipped_ws = usize::MAX;
let stop_char = 'outer: loop {
let Some(&ch) = input.get(pos) else {
if offset_start != usize::MAX {
break StopChar::Lf;
}
self.pos = pos;
return None;
};
let idx = pos;
pos += 1;
last_idx = idx;
'special: {
let content = match mask.class_of(ch) {
scan::EOL => {
let idx = if ch == b'\n' {
idx
} else if input.get(pos) == Some(&b'\n') {
last_idx = pos;
pos += 1;
last_idx
} else {
break 'special;
};
if self.mode.contains(Mode::UNFOLD_QP)
&& buf.last().or_else(|| input.get(offset_end).copied()) == Some(b'=')
{
offset_end = idx;
if !buf.is_empty() {
buf.push('\n');
}
} else if let Some(b' ' | b'\t') = input.get(pos) {
pos += 1;
if buf.is_empty() && offset_start != usize::MAX {
buf.start(source.get(offset_start..=offset_end));
}
if skipped_ws != usize::MAX && !buf.is_empty() {
buf.restore_whitespace(input.get(skipped_ws..idx));
}
skipped_ws = usize::MAX;
} else if self.mode.contains(Mode::UNFOLD_B64)
&& self.is_base64_continuation(idx)
{
if buf.is_empty() && offset_start != usize::MAX {
buf.start(source.get(offset_start..=offset_end));
}
skipped_ws = usize::MAX;
} else {
break 'outer StopChar::Lf;
}
break 'special;
}
scan::WHITESPACE => {
if !buf.last().is_some_and(|last| last != ch) {
if skipped_ws == usize::MAX {
skipped_ws = idx;
}
break 'special;
}
if ch == b'\t' { '\t' } else { ' ' }
}
scan::BACKSLASH => {
let mut next_ch = b'\\';
let mut next_offset_end = idx;
while let Some(&escaped) = input.get(pos) {
let escaped_idx = pos;
pos += 1;
match escaped {
b'\t' | b'\r' => {}
b'\n' => {
if let Some(b' ' | b'\t') = input.get(pos) {
pos += 1;
if let Some(&folded) = input.get(pos) {
next_ch = folded;
next_offset_end = pos;
pos += 1;
break;
}
} else {
offset_end = escaped_idx - 1;
break 'outer StopChar::Lf;
}
}
_ => {
next_ch = escaped;
next_offset_end = escaped_idx;
break;
}
}
}
if offset_start != usize::MAX {
if buf.is_empty() {
buf.start(source.get(offset_start..=offset_end));
}
if skipped_ws != usize::MAX {
buf.restore_whitespace(input.get(skipped_ws..idx));
}
} else {
offset_start = next_offset_end;
}
skipped_ws = usize::MAX;
match next_ch {
b'n' | b'N' => buf.push('\n'),
b'r' | b'R' => buf.push('\r'),
ascii if ascii.is_ascii() => buf.push(char::from(ascii)),
_ => {
if let Some(escaped) = source
.get(next_offset_end..)
.and_then(|rest| rest.chars().next())
{
buf.push(escaped);
pos = next_offset_end + escaped.len_utf8();
next_offset_end = pos - 1;
}
}
}
offset_end = next_offset_end;
break 'special;
}
scan::QUOTE => {
in_quote = !in_quote;
mask = if in_quote { quoted_mask } else { unquoted_mask };
break 'special;
}
scan::COLON => break 'outer StopChar::Colon,
scan::SEMICOLON => break 'outer StopChar::Semicolon,
scan::COMMA => break 'outer StopChar::Comma,
scan::EQUAL => break 'outer StopChar::Equal,
scan::DOT => break 'outer StopChar::Dot,
scan::CONTROL => {
if buf.is_empty() && offset_start != usize::MAX {
buf.start(source.get(offset_start..=offset_end));
}
break 'special;
}
scan::CARET => {
if let Some((escape_idx, decoded)) = self.caret_escape(idx) {
if offset_start != usize::MAX {
if buf.is_empty() {
buf.start(source.get(offset_start..=offset_end));
}
if skipped_ws != usize::MAX {
buf.restore_whitespace(input.get(skipped_ws..idx));
}
} else {
offset_start = escape_idx;
}
skipped_ws = usize::MAX;
buf.push(decoded);
offset_end = escape_idx;
pos = escape_idx + 1;
break 'special;
}
'^'
}
_ => {
pos = idx;
break 'special;
}
};
if offset_start == usize::MAX {
offset_start = idx;
}
offset_end = idx;
skipped_ws = usize::MAX;
if !buf.is_empty() {
buf.push(content);
}
}
let run = mask.plain_len(input.get(pos..).unwrap_or_default());
if run != 0 {
if offset_start == usize::MAX {
offset_start = pos;
}
if !buf.is_empty() {
buf.push_str(source.get(pos..pos + run));
}
pos += run;
offset_end = pos - 1;
skipped_ws = usize::MAX;
}
};
self.pos = pos;
if offset_start != usize::MAX {
self.last_token_end = offset_end;
}
let (text, start, end) = if !buf.is_empty() {
(buf.into_text(), offset_start, offset_end)
} else if offset_start != usize::MAX {
(
TokenText::Borrowed(source.get(offset_start..=offset_end).unwrap_or_default()),
offset_start,
offset_end,
)
} else {
(TokenText::Borrowed(""), last_idx, last_idx)
};
Some(Token {
text,
start,
end,
stop_char,
})
}
#[inline]
pub(crate) fn token_until_lf(&mut self, last_stop_char: &mut StopChar) -> Option<Token<'x>> {
if last_stop_char != &StopChar::Lf {
self.token()
.inspect(|token| *last_stop_char = token.stop_char)
} else {
None
}
}
#[inline]
pub(crate) fn parse_value_until_lf<T>(
&mut self,
separator: StopChar,
last_stop_char: &mut StopChar,
) -> Option<Option<T>>
where
T: IanaParse + 'static,
{
if *last_stop_char != separator {
self.token_until_lf(last_stop_char)
.map(|token| T::parse(token.text.as_ref()))
} else {
None
}
}
#[inline(always)]
pub(crate) fn value_token(
&mut self,
encoding: Option<Encoding>,
payload: &mut Option<Vec<u8>>,
) -> Option<Token<'x>> {
if encoding == Some(Encoding::Base64) {
self.base64_token(payload)
} else {
self.token()
}
}
#[inline(never)]
fn base64_token(&mut self, payload: &mut Option<Vec<u8>>) -> Option<Token<'x>> {
if let Some((token, next, bytes)) = self.folded_base64() {
self.pos = next;
self.last_token_end = token.end;
*payload = Some(bytes);
return Some(token);
}
self.token()
}
fn folded_base64(&self) -> Option<(Token<'x>, usize, Vec<u8>)> {
if self.mode.0 & !Mode::PLAIN_VALUE.0 != 0 {
return None;
}
let start = self.pos;
let value = self.input.get(start..)?;
let mut bytes = Vec::new();
let folded = base64::STANDARD.decode_folded(value, &mut bytes).ok()?;
if folded.next != folded.end
&& self.mode.contains(Mode::UNFOLD_B64)
&& self.is_base64_continuation(start + folded.next - 1)
{
return None;
}
let span = FoldedContent(value.get(..folded.end)?).span();
if span.is_empty() {
return None;
}
if bytes.capacity() > 2 * bytes.len() + SPARE_BYTES {
bytes.shrink_to_fit();
}
let (first, end) = (start + span.start, start + span.end);
Some((
Token {
text: TokenText::Borrowed(self.source.get(first..end)?),
start: first,
end: end - 1,
stop_char: StopChar::Lf,
},
start + folded.next,
bytes,
))
}
pub(crate) fn seek_lf(&mut self) -> bool {
loop {
match self.token() {
Some(Token {
stop_char: StopChar::Lf,
..
}) => return true,
None => return false,
_ => {}
}
}
}
pub(crate) fn seek_value_or_eol(&mut self) -> StopChar {
loop {
match self.token() {
Some(Token {
stop_char: StopChar::Colon,
..
}) => return StopChar::Colon,
Some(Token {
stop_char: StopChar::Lf,
..
})
| None => return StopChar::Lf,
_ => {}
}
}
}
pub(crate) fn seek_param_value_or_eol(&mut self) -> StopChar {
loop {
match self.token() {
Some(Token {
stop_char: stop_char @ (StopChar::Colon | StopChar::Semicolon | StopChar::Equal),
..
}) => return stop_char,
Some(Token {
stop_char: StopChar::Lf,
..
})
| None => return StopChar::Lf,
_ => {}
}
}
}
}
struct FoldedContent<'x>(&'x [u8]);
impl FoldedContent<'_> {
fn span(&self) -> Range<usize> {
let mut rest = self.0;
loop {
rest = match rest {
[b'\r', b'\n', b' ' | b'\t', tail @ ..] | [b'\n', b' ' | b'\t', tail @ ..] => tail,
_ => break,
};
}
let first = self.0.len() - rest.len();
loop {
rest = match rest {
[head @ .., b'\r', b'\n', b' ' | b'\t'] => head,
[head @ .., b'\n', b' ' | b'\t'] => head,
_ => return first..first + rest.len(),
};
}
}
}
#[derive(Default)]
struct OwnedText(String);
impl OwnedText {
#[inline(always)]
fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[inline(always)]
fn last(&self) -> Option<u8> {
self.0.as_bytes().last().copied()
}
#[inline(always)]
fn push(&mut self, ch: char) {
self.0.push(ch);
}
#[inline(always)]
fn push_str(&mut self, run: Option<&str>) {
self.0.push_str(run.unwrap_or_default());
}
#[inline(always)]
fn start(&mut self, prefix: Option<&str>) {
self.push_str(prefix);
}
#[inline(always)]
fn into_text<'x>(self) -> TokenText<'x> {
TokenText::Owned(self.0)
}
#[inline(always)]
fn restore_whitespace(&mut self, skipped: Option<&[u8]>) {
self.0.extend(
skipped
.unwrap_or_default()
.iter()
.filter_map(|ch| match ch {
b' ' => Some(' '),
b'\t' => Some('\t'),
_ => None,
}),
);
}
}
impl TokenText<'_> {
#[inline(always)]
pub(crate) fn as_str(&self) -> &str {
match self {
TokenText::Borrowed(text) => text,
TokenText::Owned(text) => text,
}
}
#[inline(always)]
pub(crate) fn into_string(self) -> String {
match self {
TokenText::Borrowed(text) => text.to_owned(),
TokenText::Owned(text) => text,
}
}
}
impl Deref for TokenText<'_> {
type Target = [u8];
#[inline(always)]
fn deref(&self) -> &[u8] {
self.as_str().as_bytes()
}
}
impl AsRef<[u8]> for TokenText<'_> {
#[inline(always)]
fn as_ref(&self) -> &[u8] {
self
}
}
impl From<String> for TokenText<'_> {
fn from(text: String) -> Self {
TokenText::Owned(text)
}
}
impl<'x> Token<'x> {
pub fn into_string(self) -> String {
self.text.into_string()
}
}
+467
View File
@@ -0,0 +1,467 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::*;
use jiff::Timestamp;
impl IanaString for CalendarScale {
fn as_str(&self) -> &'static str {
match self {
CalendarScale::Gregorian => "GREGORIAN",
CalendarScale::Chinese => "CHINESE",
CalendarScale::IslamicCivil => "ISLAMIC-CIVIL",
CalendarScale::Hebrew => "HEBREW",
CalendarScale::Ethiopic => "ETHIOPIC",
}
}
}
#[cfg(feature = "jmap")]
impl CalendarScale {
pub fn as_js_str(&self) -> &'static str {
match self {
CalendarScale::Gregorian => "gregorian",
CalendarScale::Chinese => "chinese",
CalendarScale::IslamicCivil => "islamic-civil",
CalendarScale::Hebrew => "hebrew",
CalendarScale::Ethiopic => "ethiopic",
}
}
}
#[cfg(feature = "rkyv")]
impl IanaString for ArchivedCalendarScale {
fn as_str(&self) -> &'static str {
match self {
ArchivedCalendarScale::Gregorian => "GREGORIAN",
ArchivedCalendarScale::Chinese => "CHINESE",
ArchivedCalendarScale::IslamicCivil => "ISLAMIC-CIVIL",
ArchivedCalendarScale::Hebrew => "HEBREW",
ArchivedCalendarScale::Ethiopic => "ETHIOPIC",
}
}
}
impl IanaParse for CalendarScale {
fn parse(value: &[u8]) -> Option<Self> {
hashify::fnc_map_ignore_case!(value,
"gregorian" => Some(CalendarScale::Gregorian),
"chinese" => Some(CalendarScale::Chinese),
"islamic-civil" => Some(CalendarScale::IslamicCivil),
"hebrew" => Some(CalendarScale::Hebrew),
"ethiopic" => Some(CalendarScale::Ethiopic),
_ => None,
)
}
}
impl Encoding {
pub fn parse(value: &[u8]) -> Option<Self> {
hashify::map_ignore_case!(value, Encoding,
b"QUOTED-PRINTABLE" => Encoding::QuotedPrintable,
b"BASE64" => Encoding::Base64,
b"Q" => Encoding::QuotedPrintable,
b"B" => Encoding::Base64,
)
.copied()
}
}
impl PartialDateTime {
pub fn now() -> Self {
Self::from_utc_timestamp(Timestamp::now().as_second())
}
pub fn from_utc_timestamp(value: i64) -> Self {
let dt = DateTime::from_timestamp(value);
PartialDateTime {
year: dt.year.into(),
month: dt.month.into(),
day: dt.day.into(),
hour: dt.hour.into(),
minute: dt.minute.into(),
second: dt.second.into(),
tz_hour: dt.tz_hour.into(),
tz_minute: dt.tz_minute.into(),
tz_minus: false,
}
}
pub fn from_date_timestamp(value: i64) -> Self {
let dt = DateTime::from_timestamp(value);
PartialDateTime {
year: dt.year.into(),
month: dt.month.into(),
day: dt.day.into(),
..Default::default()
}
}
pub fn from_naive_timestamp(value: i64) -> Self {
let dt = DateTime::from_timestamp(value);
PartialDateTime {
year: dt.year.into(),
month: dt.month.into(),
day: dt.day.into(),
hour: dt.hour.into(),
minute: dt.minute.into(),
second: dt.second.into(),
..Default::default()
}
}
pub fn to_date_time(&self) -> Option<DateTimeResult> {
let mut dt = DateTimeResult {
date_time: civil::DateTime::new(
i16::try_from(self.year?).ok()?,
i8::try_from(self.month?).ok()?,
i8::try_from(self.day?).ok()?,
i8::try_from(self.hour.unwrap_or(0)).ok()?,
i8::try_from(self.minute.unwrap_or(0)).ok()?,
i8::try_from(self.second.unwrap_or(0)).ok()?,
0,
)
.ok()?,
offset: None,
};
if let Some(tz_hour) = self.tz_hour {
let secs = (tz_hour as i32 * 3600) + (self.tz_minute.unwrap_or(0) as i32 * 60);
if i64::from(secs) >= timezone::SECONDS_PER_DAY {
return None;
}
dt.offset = Some(Offset::from_seconds(if self.tz_minus { -secs } else { secs }).ok()?);
}
Some(dt)
}
}
#[cfg(feature = "rkyv")]
impl From<&ArchivedPartialDateTime> for PartialDateTime {
fn from(date: &ArchivedPartialDateTime) -> Self {
PartialDateTime {
year: date.year.as_ref().map(|year| year.to_native()),
month: date.month.as_ref().copied(),
day: date.day.as_ref().copied(),
hour: date.hour.as_ref().copied(),
minute: date.minute.as_ref().copied(),
second: date.second.as_ref().copied(),
tz_hour: date.tz_hour.as_ref().copied(),
tz_minute: date.tz_minute.as_ref().copied(),
tz_minus: date.tz_minus,
}
}
}
#[cfg(feature = "rkyv")]
impl ArchivedPartialDateTime {
pub fn to_date_time(&self) -> Option<DateTimeResult> {
let mut dt = DateTimeResult {
date_time: civil::DateTime::new(
i16::try_from(self.year.as_ref()?.to_native()).ok()?,
i8::try_from(*self.month.as_ref()?).ok()?,
i8::try_from(*self.day.as_ref()?).ok()?,
i8::try_from(self.hour.unwrap_or(0)).ok()?,
i8::try_from(self.minute.unwrap_or(0)).ok()?,
i8::try_from(self.second.unwrap_or(0)).ok()?,
0,
)
.ok()?,
offset: None,
};
if let Some(tz_hour) = self.tz_hour.as_ref() {
let secs = (*tz_hour as i32 * 3600) + (self.tz_minute.unwrap_or(0) as i32 * 60);
if i64::from(secs) >= timezone::SECONDS_PER_DAY {
return None;
}
dt.offset = Some(Offset::from_seconds(if self.tz_minus { -secs } else { secs }).ok()?);
}
Some(dt)
}
}
impl IanaParse for LinkRelation {
fn parse(s: &[u8]) -> Option<Self> {
hashify::map_ignore_case!(s, LinkRelation,
"about" => LinkRelation::About,
"acl" => LinkRelation::Acl,
"alternate" => LinkRelation::Alternate,
"amphtml" => LinkRelation::Amphtml,
"api-catalog" => LinkRelation::ApiCatalog,
"appendix" => LinkRelation::Appendix,
"apple-touch-icon" => LinkRelation::AppleTouchIcon,
"apple-touch-startup-image" => LinkRelation::AppleTouchStartupImage,
"archives" => LinkRelation::Archives,
"author" => LinkRelation::Author,
"blocked-by" => LinkRelation::BlockedBy,
"bookmark" => LinkRelation::Bookmark,
"c2pa-manifest" => LinkRelation::C2paManifest,
"canonical" => LinkRelation::Canonical,
"chapter" => LinkRelation::Chapter,
"cite-as" => LinkRelation::CiteAs,
"collection" => LinkRelation::Collection,
"compression-dictionary" => LinkRelation::CompressionDictionary,
"contents" => LinkRelation::Contents,
"convertedfrom" => LinkRelation::Convertedfrom,
"copyright" => LinkRelation::Copyright,
"create-form" => LinkRelation::CreateForm,
"current" => LinkRelation::Current,
"deprecation" => LinkRelation::Deprecation,
"describedby" => LinkRelation::Describedby,
"describes" => LinkRelation::Describes,
"disclosure" => LinkRelation::Disclosure,
"dns-prefetch" => LinkRelation::DnsPrefetch,
"duplicate" => LinkRelation::Duplicate,
"edit" => LinkRelation::Edit,
"edit-form" => LinkRelation::EditForm,
"edit-media" => LinkRelation::EditMedia,
"enclosure" => LinkRelation::Enclosure,
"external" => LinkRelation::External,
"first" => LinkRelation::First,
"geofeed" => LinkRelation::Geofeed,
"glossary" => LinkRelation::Glossary,
"help" => LinkRelation::Help,
"hosts" => LinkRelation::Hosts,
"hub" => LinkRelation::Hub,
"ice-server" => LinkRelation::IceServer,
"icon" => LinkRelation::Icon,
"index" => LinkRelation::Index,
"intervalafter" => LinkRelation::Intervalafter,
"intervalbefore" => LinkRelation::Intervalbefore,
"intervalcontains" => LinkRelation::Intervalcontains,
"intervaldisjoint" => LinkRelation::Intervaldisjoint,
"intervalduring" => LinkRelation::Intervalduring,
"intervalequals" => LinkRelation::Intervalequals,
"intervalfinishedby" => LinkRelation::Intervalfinishedby,
"intervalfinishes" => LinkRelation::Intervalfinishes,
"intervalin" => LinkRelation::Intervalin,
"intervalmeets" => LinkRelation::Intervalmeets,
"intervalmetby" => LinkRelation::Intervalmetby,
"intervaloverlappedby" => LinkRelation::Intervaloverlappedby,
"intervaloverlaps" => LinkRelation::Intervaloverlaps,
"intervalstartedby" => LinkRelation::Intervalstartedby,
"intervalstarts" => LinkRelation::Intervalstarts,
"item" => LinkRelation::Item,
"last" => LinkRelation::Last,
"latest-version" => LinkRelation::LatestVersion,
"license" => LinkRelation::License,
"linkset" => LinkRelation::Linkset,
"lrdd" => LinkRelation::Lrdd,
"manifest" => LinkRelation::Manifest,
"mask-icon" => LinkRelation::MaskIcon,
"me" => LinkRelation::Me,
"media-feed" => LinkRelation::MediaFeed,
"memento" => LinkRelation::Memento,
"micropub" => LinkRelation::Micropub,
"modulepreload" => LinkRelation::Modulepreload,
"monitor" => LinkRelation::Monitor,
"monitor-group" => LinkRelation::MonitorGroup,
"next" => LinkRelation::Next,
"next-archive" => LinkRelation::NextArchive,
"nofollow" => LinkRelation::Nofollow,
"noopener" => LinkRelation::Noopener,
"noreferrer" => LinkRelation::Noreferrer,
"opener" => LinkRelation::Opener,
"openid2.local_id" => LinkRelation::Openid2LocalId,
"openid2.provider" => LinkRelation::Openid2Provider,
"original" => LinkRelation::Original,
"p3pv1" => LinkRelation::P3pv1,
"payment" => LinkRelation::Payment,
"pingback" => LinkRelation::Pingback,
"preconnect" => LinkRelation::Preconnect,
"predecessor-version" => LinkRelation::PredecessorVersion,
"prefetch" => LinkRelation::Prefetch,
"preload" => LinkRelation::Preload,
"prerender" => LinkRelation::Prerender,
"prev" => LinkRelation::Prev,
"preview" => LinkRelation::Preview,
"previous" => LinkRelation::Previous,
"prev-archive" => LinkRelation::PrevArchive,
"privacy-policy" => LinkRelation::PrivacyPolicy,
"profile" => LinkRelation::Profile,
"publication" => LinkRelation::Publication,
"rdap-active" => LinkRelation::RdapActive,
"rdap-bottom" => LinkRelation::RdapBottom,
"rdap-down" => LinkRelation::RdapDown,
"rdap-top" => LinkRelation::RdapTop,
"rdap-up" => LinkRelation::RdapUp,
"related" => LinkRelation::Related,
"restconf" => LinkRelation::Restconf,
"replies" => LinkRelation::Replies,
"ruleinput" => LinkRelation::Ruleinput,
"search" => LinkRelation::Search,
"section" => LinkRelation::Section,
"self" => LinkRelation::Self_,
"service" => LinkRelation::Service,
"service-desc" => LinkRelation::ServiceDesc,
"service-doc" => LinkRelation::ServiceDoc,
"service-meta" => LinkRelation::ServiceMeta,
"sip-trunking-capability" => LinkRelation::SipTrunkingCapability,
"sponsored" => LinkRelation::Sponsored,
"start" => LinkRelation::Start,
"status" => LinkRelation::Status,
"stylesheet" => LinkRelation::Stylesheet,
"subsection" => LinkRelation::Subsection,
"successor-version" => LinkRelation::SuccessorVersion,
"sunset" => LinkRelation::Sunset,
"tag" => LinkRelation::Tag,
"terms-of-service" => LinkRelation::TermsOfService,
"timegate" => LinkRelation::Timegate,
"timemap" => LinkRelation::Timemap,
"type" => LinkRelation::Type,
"ugc" => LinkRelation::Ugc,
"up" => LinkRelation::Up,
"version-history" => LinkRelation::VersionHistory,
"via" => LinkRelation::Via,
"webmention" => LinkRelation::Webmention,
"working-copy" => LinkRelation::WorkingCopy,
"working-copy-of" => LinkRelation::WorkingCopyOf,
)
.copied()
}
}
impl IanaString for LinkRelation {
fn as_str(&self) -> &'static str {
match self {
LinkRelation::About => "about",
LinkRelation::Acl => "acl",
LinkRelation::Alternate => "alternate",
LinkRelation::Amphtml => "amphtml",
LinkRelation::ApiCatalog => "api-catalog",
LinkRelation::Appendix => "appendix",
LinkRelation::AppleTouchIcon => "apple-touch-icon",
LinkRelation::AppleTouchStartupImage => "apple-touch-startup-image",
LinkRelation::Archives => "archives",
LinkRelation::Author => "author",
LinkRelation::BlockedBy => "blocked-by",
LinkRelation::Bookmark => "bookmark",
LinkRelation::C2paManifest => "c2pa-manifest",
LinkRelation::Canonical => "canonical",
LinkRelation::Chapter => "chapter",
LinkRelation::CiteAs => "cite-as",
LinkRelation::Collection => "collection",
LinkRelation::CompressionDictionary => "compression-dictionary",
LinkRelation::Contents => "contents",
LinkRelation::Convertedfrom => "convertedfrom",
LinkRelation::Copyright => "copyright",
LinkRelation::CreateForm => "create-form",
LinkRelation::Current => "current",
LinkRelation::Deprecation => "deprecation",
LinkRelation::Describedby => "describedby",
LinkRelation::Describes => "describes",
LinkRelation::Disclosure => "disclosure",
LinkRelation::DnsPrefetch => "dns-prefetch",
LinkRelation::Duplicate => "duplicate",
LinkRelation::Edit => "edit",
LinkRelation::EditForm => "edit-form",
LinkRelation::EditMedia => "edit-media",
LinkRelation::Enclosure => "enclosure",
LinkRelation::External => "external",
LinkRelation::First => "first",
LinkRelation::Geofeed => "geofeed",
LinkRelation::Glossary => "glossary",
LinkRelation::Help => "help",
LinkRelation::Hosts => "hosts",
LinkRelation::Hub => "hub",
LinkRelation::IceServer => "ice-server",
LinkRelation::Icon => "icon",
LinkRelation::Index => "index",
LinkRelation::Intervalafter => "intervalafter",
LinkRelation::Intervalbefore => "intervalbefore",
LinkRelation::Intervalcontains => "intervalcontains",
LinkRelation::Intervaldisjoint => "intervaldisjoint",
LinkRelation::Intervalduring => "intervalduring",
LinkRelation::Intervalequals => "intervalequals",
LinkRelation::Intervalfinishedby => "intervalfinishedby",
LinkRelation::Intervalfinishes => "intervalfinishes",
LinkRelation::Intervalin => "intervalin",
LinkRelation::Intervalmeets => "intervalmeets",
LinkRelation::Intervalmetby => "intervalmetby",
LinkRelation::Intervaloverlappedby => "intervaloverlappedby",
LinkRelation::Intervaloverlaps => "intervaloverlaps",
LinkRelation::Intervalstartedby => "intervalstartedby",
LinkRelation::Intervalstarts => "intervalstarts",
LinkRelation::Item => "item",
LinkRelation::Last => "last",
LinkRelation::LatestVersion => "latest-version",
LinkRelation::License => "license",
LinkRelation::Linkset => "linkset",
LinkRelation::Lrdd => "lrdd",
LinkRelation::Manifest => "manifest",
LinkRelation::MaskIcon => "mask-icon",
LinkRelation::Me => "me",
LinkRelation::MediaFeed => "media-feed",
LinkRelation::Memento => "memento",
LinkRelation::Micropub => "micropub",
LinkRelation::Modulepreload => "modulepreload",
LinkRelation::Monitor => "monitor",
LinkRelation::MonitorGroup => "monitor-group",
LinkRelation::Next => "next",
LinkRelation::NextArchive => "next-archive",
LinkRelation::Nofollow => "nofollow",
LinkRelation::Noopener => "noopener",
LinkRelation::Noreferrer => "noreferrer",
LinkRelation::Opener => "opener",
LinkRelation::Openid2LocalId => "openid2.local_id",
LinkRelation::Openid2Provider => "openid2.provider",
LinkRelation::Original => "original",
LinkRelation::P3pv1 => "p3pv1",
LinkRelation::Payment => "payment",
LinkRelation::Pingback => "pingback",
LinkRelation::Preconnect => "preconnect",
LinkRelation::PredecessorVersion => "predecessor-version",
LinkRelation::Prefetch => "prefetch",
LinkRelation::Preload => "preload",
LinkRelation::Prerender => "prerender",
LinkRelation::Prev => "prev",
LinkRelation::Preview => "preview",
LinkRelation::Previous => "previous",
LinkRelation::PrevArchive => "prev-archive",
LinkRelation::PrivacyPolicy => "privacy-policy",
LinkRelation::Profile => "profile",
LinkRelation::Publication => "publication",
LinkRelation::RdapActive => "rdap-active",
LinkRelation::RdapBottom => "rdap-bottom",
LinkRelation::RdapDown => "rdap-down",
LinkRelation::RdapTop => "rdap-top",
LinkRelation::RdapUp => "rdap-up",
LinkRelation::Related => "related",
LinkRelation::Restconf => "restconf",
LinkRelation::Replies => "replies",
LinkRelation::Ruleinput => "ruleinput",
LinkRelation::Search => "search",
LinkRelation::Section => "section",
LinkRelation::Self_ => "self",
LinkRelation::Service => "service",
LinkRelation::ServiceDesc => "service-desc",
LinkRelation::ServiceDoc => "service-doc",
LinkRelation::ServiceMeta => "service-meta",
LinkRelation::SipTrunkingCapability => "sip-trunking-capability",
LinkRelation::Sponsored => "sponsored",
LinkRelation::Start => "start",
LinkRelation::Status => "status",
LinkRelation::Stylesheet => "stylesheet",
LinkRelation::Subsection => "subsection",
LinkRelation::SuccessorVersion => "successor-version",
LinkRelation::Sunset => "sunset",
LinkRelation::Tag => "tag",
LinkRelation::TermsOfService => "terms-of-service",
LinkRelation::Timegate => "timegate",
LinkRelation::Timemap => "timemap",
LinkRelation::Type => "type",
LinkRelation::Ugc => "ugc",
LinkRelation::Up => "up",
LinkRelation::VersionHistory => "version-history",
LinkRelation::Via => "via",
LinkRelation::Webmention => "webmention",
LinkRelation::WorkingCopy => "working-copy",
LinkRelation::WorkingCopyOf => "working-copy-of",
}
}
}
File diff suppressed because it is too large. Load diff
+589
View File
@@ -0,0 +1,589 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use super::{
format::{AsciiPush, BufferedWrite},
parser::Timestamp,
};
use crate::vcard::Jscomp;
use encodify::{Fold, base64};
use mail_parser::DateTime;
use std::fmt::{self, Display, Write};
pub(crate) const MAX_LINE_LEN: usize = 75;
const FOLD: &str = "\r\n ";
pub(crate) const DOCUMENT_BUFFER: usize = 4096;
pub(crate) const ENTRY_BUFFER: usize = 512;
const TEXT: u8 = 1;
const TEXT_SEMICOLON: u8 = 1 << 1;
const TEXT_COMMA: u8 = 1 << 2;
const URI: u8 = 1 << 3;
const PARAM: u8 = 1 << 4;
const PARAM_CARET: u8 = 1 << 5;
const JSCOMP: u8 = 1 << 6;
const QUOTE: u8 = 1 << 7;
const fn byte_class(ch: u8) -> u8 {
let mut class = 0;
if matches!(ch, b'\r' | b'\n' | b'\\') {
class |= TEXT;
}
if ch == b';' {
class |= TEXT_SEMICOLON;
}
if ch == b',' {
class |= TEXT_COMMA;
}
if matches!(ch, b'\r' | b'\n') {
class |= URI;
}
if ch == b'\\' || !matches!(ch, 0x09 | 0x20 | 0x21 | 0x23..=0x7E | 0x80..=0xFF) {
class |= PARAM;
}
if ch == b'^' {
class |= PARAM_CARET;
}
if matches!(ch, b'\\' | b',' | b':' | b'=' | b';' | b'"' | b'\r' | b'\n') {
class |= JSCOMP;
}
if matches!(ch, b',' | b':' | b'=' | b' ' | b';' | b'"') {
class |= QUOTE;
}
class
}
static BYTE_CLASS: [u8; 256] = {
let mut table = [0; 256];
let mut ch = 0;
while ch < table.len() {
table[ch] = byte_class(ch as u8);
ch += 1;
}
table
};
const LANE_ONES: u64 = 0x0101_0101_0101_0101;
const LANE_HIGHS: u64 = 0x8080_8080_8080_8080;
#[inline(always)]
const fn lanes_equal(word: u64, byte: u8) -> u64 {
let lanes = word ^ (LANE_ONES * byte as u64);
lanes.wrapping_sub(LANE_ONES) & !lanes & LANE_HIGHS
}
#[inline(always)]
const fn lanes_below(word: u64, byte: u8) -> u64 {
word.wrapping_sub(LANE_ONES * byte as u64) & !word & LANE_HIGHS
}
#[inline(always)]
const fn candidate_lanes<const MASK: u8>(word: u64) -> u64 {
let mut lanes = 0;
if MASK & (TEXT | URI | JSCOMP) != 0 {
lanes |= lanes_equal(word, b'\r') | lanes_equal(word, b'\n');
}
if MASK & (TEXT | PARAM | JSCOMP) != 0 {
lanes |= lanes_equal(word, b'\\');
}
if MASK & (TEXT_SEMICOLON | JSCOMP | QUOTE) != 0 {
lanes |= lanes_equal(word, b';');
}
if MASK & (TEXT_COMMA | JSCOMP | QUOTE) != 0 {
lanes |= lanes_equal(word, b',');
}
if MASK & (PARAM | JSCOMP | QUOTE) != 0 {
lanes |= lanes_equal(word, b'"');
}
if MASK & (JSCOMP | QUOTE) != 0 {
lanes |= lanes_equal(word, b':') | lanes_equal(word, b'=');
}
if MASK & QUOTE != 0 {
lanes |= lanes_equal(word, b' ');
}
if MASK & PARAM != 0 {
lanes |= lanes_below(word, 0x20) | lanes_equal(word, 0x7F);
}
if MASK & PARAM_CARET != 0 {
lanes |= lanes_equal(word, b'^');
}
lanes
}
#[inline(always)]
fn find_class<const MASK: u8>(bytes: &[u8]) -> Option<usize> {
let (words, tail) = bytes.as_chunks::<8>();
let mut offset = 0;
for word in words {
let mut lanes = candidate_lanes::<MASK>(u64::from_le_bytes(*word));
while lanes != 0 {
let pos = offset + (lanes.trailing_zeros() / 8) as usize;
if MASK & PARAM == 0
|| bytes
.get(pos)
.is_some_and(|&ch| BYTE_CLASS[usize::from(ch)] & MASK != 0)
{
return Some(pos);
}
lanes &= lanes - 1;
}
offset += 8;
}
tail.iter()
.position(|&ch| BYTE_CLASS[usize::from(ch)] & MASK != 0)
.map(|pos| offset + pos)
}
#[inline(always)]
fn split_at_class<const MASK: u8>(text: &str) -> Option<(&str, u8, &str)> {
let pos = find_class::<MASK>(text.as_bytes())?;
let (run, rest) = text.split_at_checked(pos)?;
let special = *rest.as_bytes().first()?;
Some((run, special, rest.get(1..)?))
}
pub(crate) trait LineWriter: Write + AsciiPush {
fn write_atomic(&mut self, text: &str) -> fmt::Result;
fn write_base64(&mut self, data: &[u8]) -> fmt::Result;
}
pub(crate) struct FoldingWriter<'x, W: Write + AsciiPush + ?Sized> {
out: &'x mut W,
line_len: usize,
}
impl<'x, W: Write + AsciiPush + ?Sized> FoldingWriter<'x, W> {
pub(crate) fn new(out: &'x mut W) -> Self {
Self { out, line_len: 0 }
}
pub(crate) fn end_line(&mut self) -> fmt::Result {
self.line_len = 0;
self.out.push_ascii(b"\r\n")
}
pub(crate) fn write_boundary(&mut self, keyword: &str, name: &str) -> fmt::Result {
self.write_atomic(keyword)?;
self.write_atomic(name)?;
self.end_line()
}
pub(crate) fn write_raw(&mut self, text: &str) -> fmt::Result {
self.out.write_str(text)
}
#[inline(never)]
fn write_folded(&mut self, text: &str) -> fmt::Result {
let mut rest = text;
loop {
let available = MAX_LINE_LEN.saturating_sub(self.line_len);
if rest.len() <= available {
self.line_len += rest.len();
return self.out.write_str(rest);
}
match rest.split_at_checked(rest.floor_char_boundary(available)) {
Some((line, tail)) if !line.is_empty() => {
self.out.write_str(line)?;
self.line_len += line.len();
rest = tail;
}
_ => {
self.out.push_ascii(FOLD.as_bytes())?;
self.line_len = 1;
}
}
}
}
#[inline(never)]
fn push_ascii_folded(&mut self, bytes: &[u8]) -> fmt::Result {
let mut rest = bytes;
while !rest.is_empty() {
let available = MAX_LINE_LEN.saturating_sub(self.line_len);
if available == 0 {
self.out.push_ascii(FOLD.as_bytes())?;
self.line_len = 1;
continue;
}
let (line, tail) = rest
.split_at_checked(available.min(rest.len()))
.unwrap_or((rest, &[]));
self.out.push_ascii(line)?;
self.line_len += line.len();
rest = tail;
}
Ok(())
}
}
impl<W: Write + AsciiPush + ?Sized> AsciiPush for FoldingWriter<'_, W> {
#[inline(always)]
fn push_ascii(&mut self, bytes: &[u8]) -> fmt::Result {
if bytes.len() <= MAX_LINE_LEN.saturating_sub(self.line_len) {
self.line_len += bytes.len();
self.out.push_ascii(bytes)
} else {
self.push_ascii_folded(bytes)
}
}
}
impl<W: Write + AsciiPush + ?Sized> LineWriter for FoldingWriter<'_, W> {
#[inline(always)]
fn write_atomic(&mut self, text: &str) -> fmt::Result {
let len = text.len();
if len > MAX_LINE_LEN - 1 {
return self.write_str(text);
}
if self.line_len + len > MAX_LINE_LEN {
self.out.push_ascii(FOLD.as_bytes())?;
self.line_len = 1;
}
self.line_len += len;
self.out.write_str(text)
}
fn write_base64(&mut self, data: &[u8]) -> fmt::Result {
let mut result = Ok(());
let out = &mut *self.out;
base64::STANDARD.encode_folded(data, &mut self.line_len, Fold::CONTENT_LINE, |piece| {
if result.is_ok() {
result = out.push_ascii(piece);
}
});
result
}
}
impl LineWriter for String {
fn write_atomic(&mut self, text: &str) -> fmt::Result {
self.push_str(text);
Ok(())
}
fn write_base64(&mut self, data: &[u8]) -> fmt::Result {
base64::STANDARD.encode_append(data, self);
Ok(())
}
}
impl<W: Write + AsciiPush + ?Sized> Write for FoldingWriter<'_, W> {
#[inline]
fn write_str(&mut self, text: &str) -> fmt::Result {
if text.len() <= MAX_LINE_LEN.saturating_sub(self.line_len) {
self.line_len += text.len();
self.out.write_str(text)
} else {
self.write_folded(text)
}
}
fn write_char(&mut self, ch: char) -> fmt::Result {
let ch_len = ch.len_utf8();
if self.line_len + ch_len > MAX_LINE_LEN {
self.out.push_ascii(FOLD.as_bytes())?;
self.line_len = 1;
}
self.line_len += ch_len;
self.out.write_char(ch)
}
}
const INLINE_COMPONENTS: usize = 256;
pub(crate) struct VisitedComponents {
inline: [u64; INLINE_COMPONENTS / 64],
spilled: Vec<u64>,
}
impl VisitedComponents {
pub(crate) fn new(len: usize) -> Self {
Self {
inline: [0; INLINE_COMPONENTS / 64],
spilled: if len > INLINE_COMPONENTS {
vec![0; len.div_ceil(64)]
} else {
Vec::new()
},
}
}
#[inline]
pub(crate) fn insert(&mut self, id: usize) -> bool {
let words = if self.spilled.is_empty() {
self.inline.as_mut_slice()
} else {
self.spilled.as_mut_slice()
};
match words.get_mut(id / 64) {
Some(word) => {
let bit = 1 << (id % 64);
let fresh = *word & bit == 0;
*word |= bit;
fresh
}
None => false,
}
}
}
pub(crate) fn write_text(
out: &mut impl LineWriter,
value: &str,
escape_semicolon: bool,
escape_comma: bool,
) -> fmt::Result {
match (escape_semicolon, escape_comma) {
(true, true) => write_text_class::<{ TEXT | TEXT_SEMICOLON | TEXT_COMMA }>(out, value),
(true, false) => write_text_class::<{ TEXT | TEXT_SEMICOLON }>(out, value),
(false, true) => write_text_class::<{ TEXT | TEXT_COMMA }>(out, value),
(false, false) => write_text_class::<TEXT>(out, value),
}
}
fn write_text_class<const MASK: u8>(out: &mut impl LineWriter, value: &str) -> fmt::Result {
let mut rest = value;
while let Some((run, special, tail)) = split_at_class::<MASK>(rest) {
if !run.is_empty() {
out.write_str(run)?;
}
out.write_atomic(match special {
b'\r' => "\\r",
b'\n' => "\\n",
b'\\' => "\\\\",
b';' => "\\;",
_ => "\\,",
})?;
rest = tail;
}
if rest.is_empty() {
Ok(())
} else {
out.write_str(rest)
}
}
pub(crate) fn write_uri(out: &mut impl LineWriter, value: &str) -> fmt::Result {
let mut rest = value;
while let Some((run, special, tail)) = split_at_class::<URI>(rest) {
if !run.is_empty() {
out.write_str(run)?;
}
out.write_atomic(if special == b'\r' { "\\r" } else { "\\n" })?;
rest = tail;
}
if rest.is_empty() {
Ok(())
} else {
out.write_str(rest)
}
}
pub(crate) trait NeedsQuotes {
fn needs_quotes(&self) -> bool;
}
impl<T: AsRef<[u8]>> NeedsQuotes for T {
fn needs_quotes(&self) -> bool {
find_class::<QUOTE>(self.as_ref()).is_some()
}
}
pub(crate) fn write_param_value(
out: &mut impl LineWriter,
value: &str,
caret_escape: bool,
) -> fmt::Result {
if value.needs_quotes() {
write_quoted_param_value(out, value, caret_escape)
} else {
write_param_text(out, value, caret_escape)
}
}
pub(crate) fn write_quoted_param_value(
out: &mut impl LineWriter,
value: &str,
caret_escape: bool,
) -> fmt::Result {
out.write_atomic("\"")?;
write_param_text(out, value, caret_escape)?;
out.write_atomic("\"")
}
pub(crate) fn write_param_text(
out: &mut impl LineWriter,
value: &str,
caret_escape: bool,
) -> fmt::Result {
if caret_escape {
write_param_text_class::<{ PARAM | PARAM_CARET }>(out, value)
} else {
write_param_text_class::<PARAM>(out, value)
}
}
fn write_param_text_class<const MASK: u8>(out: &mut impl LineWriter, value: &str) -> fmt::Result {
let caret_escape = MASK & PARAM_CARET != 0;
let mut rest = value;
while let Some((run, special, tail)) = split_at_class::<MASK>(rest) {
if !run.is_empty() {
out.write_str(run)?;
}
let escaped = match special {
b'\r' if tail.starts_with('\n') => None,
b'\n' | b'\r' if caret_escape => Some("^n"),
b'\n' | b'\r' => Some("\\n"),
b'^' => Some("^^"),
b'"' if caret_escape => Some("^'"),
b'"' => Some("\\\""),
b'\\' => Some("\\\\"),
_ => None,
};
if let Some(escaped) = escaped {
out.write_atomic(escaped)?;
}
rest = tail;
}
if rest.is_empty() {
Ok(())
} else {
out.write_str(rest)
}
}
pub(crate) enum JscompRef<'x> {
Entry { position: u32, value: u32 },
Separator(&'x str),
}
impl<'x> From<&'x Jscomp> for JscompRef<'x> {
fn from(jscomp: &'x Jscomp) -> Self {
match jscomp {
Jscomp::Entry { position, value } => JscompRef::Entry {
position: *position,
value: *value,
},
Jscomp::Separator(separator) => JscompRef::Separator(separator),
}
}
}
impl JscompRef<'_> {
fn write_to(self, out: &mut impl LineWriter) -> fmt::Result {
match self {
JscompRef::Entry { position, value } => {
out.push_u64(position.into())?;
if value > 0 {
out.push_byte(b',')?;
out.push_u64(value.into())?;
}
Ok(())
}
JscompRef::Separator("") => Ok(()),
JscompRef::Separator(separator) => {
out.write_atomic("s,")?;
let mut rest = separator;
while let Some((run, special, tail)) = split_at_class::<JSCOMP>(rest) {
if !run.is_empty() {
out.write_str(run)?;
}
let escaped = match special {
b'\\' => "\\\\",
b',' => "\\,",
b':' => "\\:",
b'=' => "\\=",
b';' => "\\;",
b'"' => "\\\"",
_ => "",
};
if !escaped.is_empty() {
out.write_atomic(escaped)?;
}
rest = tail;
}
if rest.is_empty() {
Ok(())
} else {
out.write_str(rest)
}
}
}
}
}
pub(crate) fn write_jscomps<'x, J: 'x>(out: &mut impl LineWriter, values: &'x [J]) -> fmt::Result
where
&'x J: Into<JscompRef<'x>>,
{
for (pos, item) in values.iter().enumerate() {
if pos > 0 {
out.write_atomic(";")?;
}
item.into().write_to(out)?;
}
Ok(())
}
impl Timestamp {
pub(crate) fn push_to(&self, buf: &mut impl AsciiPush) -> fmt::Result {
let dt = DateTime::from_timestamp(self.0);
buf.push_4_digits(dt.year)?;
buf.push_2_digits(dt.month)?;
buf.push_2_digits(dt.day)?;
buf.push_byte(b'T')?;
buf.push_2_digits(dt.hour)?;
buf.push_2_digits(dt.minute)?;
buf.push_2_digits(dt.second)?;
buf.push_byte(b'Z')
}
}
#[cfg(all(test, inbuxa_tests))]
pub(crate) fn assert_fold_width(text: &str, context: &str) {
let mut lines = text.split("\r\n").peekable();
let is_v21 = text.contains("\r\nVERSION:2.1\r\n");
let mut ends_v21_base64 = false;
while let Some(line) = lines.next() {
if !line.is_empty() && !line.starts_with(' ') {
ends_v21_base64 = is_v21 && line.contains(";ENCODING=BASE64");
}
assert!(
line.len() <= MAX_LINE_LEN,
"physical line of {} octets exceeds the {MAX_LINE_LEN} octet fold width in {context}: {line:?}",
line.len()
);
assert!(
!line.is_empty() || lines.peek().is_none() || std::mem::take(&mut ends_v21_base64),
"empty physical line in {context}: {text:?}"
);
assert!(
line != " ",
"continuation line holding nothing but the fold character in {context}: {text:?}"
);
}
}
impl Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_buffered::<32>(|buf| self.push_to(buf))
}
}
+117
View File
@@ -0,0 +1,117 @@
/*
* SPDX-FileCopyrightText: 2021 Fredrik Meringdal, Ralph Bisschops <https://github.com/fmeringdal/rust-rrule>
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*/
use crate::{
common::{CalendarScale, IanaString},
icalendar::ICalendarFrequency,
};
use std::fmt::{Display, Formatter};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize)
)]
pub enum RRuleError {
ValidationError(ValidationError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize)
)]
pub enum ValidationError {
BySetPosWithoutByRule,
InvalidFieldValue {
field: String,
value: String,
},
InvalidFieldValueRange {
field: String,
value: String,
start_idx: String,
end_idx: String,
},
InvalidFieldValueRangeWithFreq {
field: String,
value: String,
freq: ICalendarFrequency,
start_idx: String,
end_idx: String,
},
InvalidByRuleAndFrequency {
by_rule: String,
freq: ICalendarFrequency,
},
TooBigInterval(u16),
/// The rule is written in a calendar other than the Gregorian one.
///
/// RFC 7529 scales place instances on entirely different dates, so a rule
/// naming one cannot be expanded by treating the scale as absent.
UnsupportedCalendarScale(CalendarScale),
}
impl From<ValidationError> for RRuleError {
fn from(err: ValidationError) -> Self {
Self::ValidationError(err)
}
}
impl Display for RRuleError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
RRuleError::ValidationError(err) => write!(f, "{err}"),
}
}
}
impl Display for ValidationError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ValidationError::BySetPosWithoutByRule => {
write!(f, "BYSETPOS without BYxxx rule is not allowed")
}
ValidationError::InvalidFieldValue { field, value } => {
write!(f, "Invalid value `{value}` for field `{field}`")
}
ValidationError::InvalidFieldValueRange {
field,
value,
start_idx,
end_idx,
} => write!(
f,
"Invalid value `{value}` for field `{field}`, start index `{start_idx}` and end index `{end_idx}`"
),
ValidationError::InvalidFieldValueRangeWithFreq {
field,
value,
freq,
start_idx,
end_idx,
} => write!(
f,
"Invalid value `{value}` for field `{field}`, frequency `{}`, start index `{start_idx}` and end index `{end_idx}`",
freq.as_str()
),
ValidationError::InvalidByRuleAndFrequency { by_rule, freq } => write!(
f,
"Invalid BY rule `{by_rule}` with frequency `{}`",
freq.as_str()
),
ValidationError::TooBigInterval(interval) => write!(
f,
"Interval of {interval} is too big. The maximum interval is 32767."
),
ValidationError::UnsupportedCalendarScale(rscale) => write!(
f,
"Recurrence rules in the {} calendar are not supported",
rscale.as_str()
),
}
}
}
+20
View File
@@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
//! Recurrence rule expansion.
//!
//! The engine in [`recurrence`] is a port of the one in
//! [`bttf`](https://github.com/BurntSushi/bttf) by Andrew Gallant; see that
//! module for what was changed. [`rrule`] adapts a parsed `RRULE` property
//! onto it.
pub mod error;
pub mod recurrence;
pub mod rrule;
mod weekdate;
pub use recurrence::{MAX_ITER_LOOP, MAX_UNPRODUCTIVE_WORK, RecurrenceIter};
pub use rrule::RRule;
File diff suppressed because it is too large. Load diff
+152
View File
@@ -0,0 +1,152 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*/
//! Turns a parsed `RRULE` property into an expandable recurrence rule.
use super::{
error::{RRuleError, ValidationError},
recurrence::{ByWeekday, RecurrenceIter, RecurrenceRule},
};
use crate::{
common::{
CalendarScale,
timezone::{Tz, ZonedDateTime},
},
icalendar::{ICalendarFrequency, ICalendarRecurrenceRule},
};
use jiff::{Unit, civil::Time};
const END_OF_DAY: Time = Time::constant(23, 59, 0, 0);
/// A recurrence rule bound to the start of its series.
#[derive(Clone, Debug)]
pub struct RRule {
rule: RecurrenceRule,
}
impl RRule {
/// Builds a rule from a parsed `RRULE` and the start of its series.
pub fn from_ical(
rule: &ICalendarRecurrenceRule,
dt_start: ZonedDateTime,
is_date: bool,
) -> Result<Self, RRuleError> {
// A non-Gregorian calendar scale puts instances on entirely different
// dates, so expanding the rule as though the scale were absent would
// silently produce the wrong ones.
if let Some(rscale) = rule
.rscale
.as_ref()
.filter(|rscale| **rscale != CalendarScale::Gregorian)
{
return Err(ValidationError::UnsupportedCalendarScale(rscale.clone()).into());
}
let mut builder = RecurrenceRule::builder(rule.freq, dt_start);
if let Some(interval) = rule.interval.filter(|interval| *interval > 0) {
builder.interval(i32::from(interval));
}
if let Some(count) = rule.count {
builder.count(count);
}
if let Some(until) = rule.until.as_ref().and_then(|until| until.to_date_time()) {
let tz = dt_start.timezone();
let until = if is_date && until.offset.is_some() {
let written = until.date_time;
let last_day = if written.time() >= END_OF_DAY {
Some(written.date())
} else {
written.round(Unit::Day).ok().map(|day| day.date())
};
last_day.and_then(|day| tz.from_local(day.to_datetime(Time::MAX)))
} else {
until.to_date_time_with_tz(tz)
};
if let Some(until) = until {
builder.until(until);
}
}
if let Some(wkst) = rule.wkst {
builder.week_start(wkst.into());
}
builder.by_month(
rule.bymonth
.iter()
.map(|month| month.month() as i8)
.collect::<Vec<_>>(),
);
let by_week: &[i8] = if rule.freq == ICalendarFrequency::Yearly {
&rule.byweekno
} else {
&[]
};
let by_year_day: &[i16] = if matches!(
rule.freq,
ICalendarFrequency::Monthly | ICalendarFrequency::Weekly | ICalendarFrequency::Daily
) {
&[]
} else {
&rule.byyearday
};
let by_month_day: &[i8] = if rule.freq == ICalendarFrequency::Weekly {
&[]
} else {
&rule.bymonthday
};
let numbered_weekdays = rule.freq == ICalendarFrequency::Monthly
|| (rule.freq == ICalendarFrequency::Yearly && by_week.is_empty());
builder.by_week(by_week);
builder.by_year_day(by_year_day);
builder.by_month_day(by_month_day);
builder.by_week_day(
rule.byday
.iter()
.map(|day| match day.ordwk.filter(|_| numbered_weekdays) {
Some(nth) => ByWeekday::Numbered {
nth: i8::try_from(nth).unwrap_or(i8::MAX),
weekday: day.weekday.into(),
},
None => ByWeekday::Any(day.weekday.into()),
})
.collect::<Vec<_>>(),
);
builder.by_hour(rule.byhour.iter().map(|v| *v as i8).collect::<Vec<_>>());
builder.by_minute(rule.byminute.iter().map(|v| *v as i8).collect::<Vec<_>>());
builder.by_second(rule.bysecond.iter().map(|v| *v as i8).collect::<Vec<_>>());
let set_position_alone = rule.bymonth.is_empty()
&& by_week.is_empty()
&& by_year_day.is_empty()
&& by_month_day.is_empty()
&& rule.byday.is_empty()
&& rule.byhour.is_empty()
&& rule.byminute.is_empty()
&& rule.bysecond.is_empty();
if !set_position_alone {
builder.by_set_position(rule.bysetpos.as_slice());
} else if !rule.bysetpos.is_empty()
&& !rule.bysetpos.iter().any(|position| position.abs() == 1)
{
builder.count(0);
}
Ok(RRule {
rule: builder.build()?,
})
}
/// Returns an iterator over the instances this rule generates.
pub fn iter(&self) -> RecurrenceIter<'_> {
self.rule.iter()
}
/// Returns the time zone instances are generated in.
pub fn tz(&self) -> Tz {
self.rule.tz()
}
}
+171
View File
@@ -0,0 +1,171 @@
/*
* SPDX-FileCopyrightText: 2025 Andrew Gallant <[email protected]> (https://github.com/BurntSushi/bttf)
*
* SPDX-License-Identifier: Unlicense OR MIT
*/
//! Week dates under an arbitrary week start.
//!
//! RFC 5545 lets a rule choose the day a week starts on (WKST), so BYWEEKNO
//! cannot use [`jiff::civil::ISOWeekDate`], which always starts on Monday.
//!
//! Ported from `bttf`, with the `anyhow` errors replaced by `Option`: every
//! failure here means a date outside the supported range, which a recurrence
//! rule skips rather than reports.
use jiff::{
ToSpan,
civil::{Date, Weekday},
};
#[derive(Clone, Copy, Debug)]
pub(crate) struct WeekDate {
// The weekday on which this week date calendar starts weeks. This is the
// key difference from `jiff::civil::ISOWeekDate`, which always starts on
// Monday.
start: Weekday,
year: i16,
week: i8,
weekday: Weekday,
}
impl WeekDate {
/// Create a new week date.
///
/// `year` must be in the range `-9999..=9999`. `week` must be in the range
/// `1..=53`, although `53` is only valid for "long" years.
///
/// `start` corresponds to how the week numbering scheme determines the
/// start of a week.
pub(crate) fn new(start: Weekday, year: i16, week: i8, weekday: Weekday) -> Option<WeekDate> {
if !(1..=53).contains(&week) || (week == 53 && !is_long_year(start, year)) {
return None;
}
if year == -9999 || year == 9999 {
let start_of_year = week_start_of_year(start, year)?;
let mut days = i32::from(week - 1) * 7;
days += i32::from(weekday.since(start));
if start_of_year.checked_add(days.days()).is_err() {
return None;
}
}
Some(WeekDate {
start,
year,
week,
weekday,
})
}
/// Returns a new week date for the given Gregorian date.
///
/// The week date uses a week numbering scheme where the given weekday
/// is the first day in the week. That is, the first week date of a year
/// starts on the given weekday and is the first week whose majority of
/// days (>= 4) falls in the same Gregorian year.
#[cfg(all(test, inbuxa_tests))]
pub(crate) fn from_date(start: Weekday, date: Date) -> Option<WeekDate> {
let mut start_of_year = week_start_of_year(start, date.year())?;
if date < start_of_year {
start_of_year = week_start_of_year(start, date.year() - 1)?;
} else {
let next = date.year() + 1;
// This fails when `date` is in year 9999, which is Jiff's maximum.
// But by the same token, in that case, `date` will never be
// greater than or equal to the start of that year. So we can just
// ignore it.
if let Some(next_start_of_year) = week_start_of_year(start, next)
&& date >= next_start_of_year
{
start_of_year = next_start_of_year;
}
}
debug_assert!(date >= start_of_year);
let diff_days = start_of_year.until(date).ok()?.get_days();
// +1 because weeks are one-indexed. `date` cannot be more than 53
// weeks after `start_of_year`, so the conversion always fits.
let week = i8::try_from(diff_days / 7).ok()? + 1;
// The week date year is guaranteed to match the Gregorian year 4
// days after the start of the first day of the week date year.
let year = start_of_year.checked_add(4.days()).ok()?.year();
let weekday = date.weekday();
Some(WeekDate {
start,
year,
week,
weekday,
})
}
/// Converts this week date to its corresponding Gregorian date.
pub(crate) fn date(self) -> Option<Date> {
let start_of_year = week_start_of_year(self.start, self.year)?;
let mut days = i32::from(self.week - 1) * 7;
days += i32::from(self.weekday.since(self.start));
start_of_year.checked_add(days.days()).ok()
}
}
pub(crate) fn weeks_in_year(start: Weekday, year: i16) -> i8 {
if is_long_year(start, year) { 53 } else { 52 }
}
/// Returns the start of the week that the given date resides in.
///
/// The starting point of the week is determined by `start`.
pub(crate) fn first_of_week(start: Weekday, date: Date) -> Option<Date> {
let wd = date.weekday();
if start == wd {
Some(date)
} else {
date.nth_weekday(-1, start).ok()
}
}
/// Returns the end of the week that the given date resides in.
///
/// The starting point of the week is determined by `start`.
pub(crate) fn last_of_week(start: Weekday, date: Date) -> Option<Date> {
let last = start.wrapping_sub(1);
let wd = date.weekday();
if last == wd {
Some(date)
} else {
date.nth_weekday(1, last).ok()
}
}
/// Returns true if the given week year (with weeks starting on `start`) is a
/// "long" year or not.
///
/// A "long" year is a year with 53 weeks. Otherwise, it's a "short" year with
/// 52 weeks.
fn is_long_year(start: Weekday, year: i16) -> bool {
// Inspired by: https://en.wikipedia.org/wiki/ISO_week_date#Weeks_per_year
let Ok(last) = Date::new(year, 12, 31) else {
return false;
};
let weekday = last.weekday();
weekday == start.wrapping_add(3) || (last.in_leap_year() && weekday == start.wrapping_add(4))
}
/// Returns the first date in the first week of the given year.
///
/// The date returned is guaranteed to have a weekday equivalent to `start`.
fn week_start_of_year(start: Weekday, year: i16) -> Option<Date> {
// RFC 5545 says:
//
// > A week is defined as a seven day period, starting on the day of the
// > week defined to be the week start (see WKST). Week number one of the
// > calendar year is the first week that contains at least four (4) days
// > in that calendar year.
//
// Which means that Jan 4 *must* be in the first week of the year.
let date_in_first_week = Date::new(year, 1, 4).ok()?;
// Now find the number of days since the start of the week from a date that
// we know is in the first week of `year`.
let diff_from_start = date_in_first_week.weekday().since(start);
date_in_first_week.checked_sub(diff_from_start.days()).ok()
}
+760
View File
@@ -0,0 +1,760 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{
Data, IanaType, PartialDateTime,
parser::{Boolean, Integer},
},
icalendar::*,
};
use ahash::{AHashMap, AHashSet};
use smallvec::{SmallVec, smallvec};
impl ICalendar {
#[must_use]
pub fn remove_component_ids(&mut self, component_ids: &[u32]) -> bool {
let max_component_id = u32::try_from(self.components.len()).unwrap_or(u32::MAX);
let mut remove_component_ids: AHashSet<u32> = component_ids
.iter()
.copied()
.filter(|id| *id < max_component_id)
.collect();
let mut pending = remove_component_ids.iter().copied().collect::<Vec<_>>();
while let Some(component_id) = pending.pop() {
let Some(component) = self.components.get(component_id as usize) else {
continue;
};
for child_id in &component.component_ids {
if *child_id < max_component_id && remove_component_ids.insert(*child_id) {
pending.push(*child_id);
}
}
}
if remove_component_ids.is_empty() {
return true;
}
let removes_all_calendar_components = self.has_calendar_components()
&& self.calendar_root().is_some_and(|root| {
!root.component_ids.iter().any(|id| {
*id != 0 && *id < max_component_id && !remove_component_ids.contains(id)
})
});
if remove_component_ids.contains(&0) || removes_all_calendar_components {
return false;
}
let id_mappings = (0..max_component_id)
.filter(|i| !remove_component_ids.contains(i))
.zip(0u32..)
.collect::<AHashMap<_, _>>();
let components =
std::mem::replace(&mut self.components, Vec::with_capacity(id_mappings.len()));
self.components.extend(
(0..max_component_id)
.zip(components)
.filter(|(component_id, _)| !remove_component_ids.contains(component_id))
.map(|(_, mut component)| {
component.component_ids = component
.component_ids
.iter()
.filter_map(|id| id_mappings.get(id).copied())
.collect();
component
}),
);
true
}
pub fn copy_timezones(&mut self, other: &ICalendar) {
if self.calendar_root().is_none() {
return;
}
for component in other
.components
.iter()
.filter(|component| component.component_type == ICalendarComponentType::VTimezone)
{
let items = component
.component_ids
.iter()
.filter_map(|component_id| other.component_by_id(*component_id));
let has_observances = items.clone().any(|item| {
matches!(
item.component_type,
ICalendarComponentType::Standard | ICalendarComponentType::Daylight
)
});
let item_count = items.clone().count();
let Some((tz_component_id, first_item_id, last_item_id)) =
u32::try_from(self.components.len())
.ok()
.filter(|_| has_observances)
.and_then(|tz_component_id| {
let first_item_id = tz_component_id.checked_add(1)?;
let last_item_id =
first_item_id.checked_add(u32::try_from(item_count).ok()?)?;
Some((tz_component_id, first_item_id, last_item_id))
})
else {
continue;
};
self.components.reserve(item_count + 1);
self.components.push(ICalendarComponent {
component_type: ICalendarComponentType::VTimezone,
entries: component.entries.clone(),
component_ids: (first_item_id..last_item_id).collect(),
});
self.components.extend(items.map(|item| ICalendarComponent {
component_type: item.component_type.clone(),
entries: item.entries.clone(),
component_ids: vec![],
}));
if let Some(root) = self.components.first_mut() {
let insert_at = root.component_ids.len().min(1);
root.component_ids.insert(insert_at, tz_component_id);
}
}
}
}
impl ICalendarComponent {
pub fn new(component_type: ICalendarComponentType) -> Self {
Self {
component_type,
entries: Vec::new(),
component_ids: Vec::new(),
}
}
pub fn add_dtstamp(&mut self, dt_stamp: PartialDateTime) {
self.entries.push(ICalendarEntry {
name: ICalendarProperty::Dtstamp,
params: vec![],
values: smallvec![ICalendarValue::PartialDateTime(dt_stamp)],
});
}
pub fn add_sequence(&mut self, sequence: i64) {
self.entries.push(ICalendarEntry {
name: ICalendarProperty::Sequence,
params: vec![],
values: smallvec![ICalendarValue::Integer(sequence)],
});
}
pub fn add_uid(&mut self, uid: &str) {
self.entries.push(ICalendarEntry {
name: ICalendarProperty::Uid,
params: vec![],
values: smallvec![ICalendarValue::Text(uid.to_string())],
});
}
pub fn add_property(&mut self, name: ICalendarProperty, value: impl Into<ICalendarValue>) {
self.entries.push(ICalendarEntry {
name,
params: vec![],
values: smallvec![value.into()],
});
}
pub fn add_property_with_params(
&mut self,
name: ICalendarProperty,
params: impl IntoIterator<Item = ICalendarParameter>,
value: impl Into<ICalendarValue>,
) {
self.entries.push(ICalendarEntry {
name,
params: params.into_iter().collect(),
values: smallvec![value.into()],
});
}
}
impl ICalendarEntry {
pub fn new(name: ICalendarProperty) -> Self {
Self {
name,
params: vec![],
values: SmallVec::new(),
}
}
pub fn with_params(mut self, params: Vec<ICalendarParameter>) -> Self {
self.params = params;
self
}
pub fn with_value(mut self, value: impl Into<ICalendarValue>) -> Self {
self.values.push(value.into());
self
}
pub fn with_values(mut self, values: impl IntoIterator<Item = ICalendarValue>) -> Self {
self.values = values.into_iter().collect();
self
}
pub fn with_param(mut self, param: impl Into<ICalendarParameter>) -> Self {
self.params.push(param.into());
self
}
pub fn add_param(&mut self, param: impl Into<ICalendarParameter>) {
self.params.push(param.into());
}
pub fn with_param_opt(mut self, param: Option<impl Into<ICalendarParameter>>) -> Self {
if let Some(param) = param {
self.params.push(param.into());
}
self
}
pub fn is_type(&self, typ: &ICalendarValueType) -> bool {
self.parameters(&ICalendarParameterName::Value)
.any(|p| matches!(p, ICalendarParameterValue::Value(v) if v == typ))
}
}
impl ICalendarParameter {
pub fn new(name: ICalendarParameterName, value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name,
value: value.into(),
}
}
pub fn altrep(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Altrep,
value: value.into(),
}
}
pub fn cn(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Cn,
value: value.into(),
}
}
pub fn cutype(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Cutype,
value: value.into(),
}
}
pub fn delegated_from(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::DelegatedFrom,
value: value.into(),
}
}
pub fn delegated_to(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::DelegatedTo,
value: value.into(),
}
}
pub fn dir(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Dir,
value: value.into(),
}
}
pub fn fmttype(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Fmttype,
value: value.into(),
}
}
pub fn fbtype(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Fbtype,
value: value.into(),
}
}
pub fn language(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Language,
value: value.into(),
}
}
pub fn member(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Member,
value: value.into(),
}
}
pub fn partstat(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Partstat,
value: value.into(),
}
}
pub fn range(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Range,
value: value.into(),
}
}
pub fn related(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Related,
value: value.into(),
}
}
pub fn reltype(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Reltype,
value: value.into(),
}
}
pub fn role(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Role,
value: value.into(),
}
}
pub fn rsvp(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Rsvp,
value: value.into(),
}
}
pub fn schedule_agent(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::ScheduleAgent,
value: value.into(),
}
}
pub fn schedule_force_send(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::ScheduleForceSend,
value: value.into(),
}
}
pub fn schedule_status(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::ScheduleStatus,
value: value.into(),
}
}
pub fn sent_by(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::SentBy,
value: value.into(),
}
}
pub fn tzid(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Tzid,
value: value.into(),
}
}
pub fn value(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Value,
value: value.into(),
}
}
pub fn display(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Display,
value: value.into(),
}
}
pub fn email(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Email,
value: value.into(),
}
}
pub fn feature(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Feature,
value: value.into(),
}
}
pub fn label(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Label,
value: value.into(),
}
}
pub fn size(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Size,
value: value.into(),
}
}
pub fn filename(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Filename,
value: value.into(),
}
}
pub fn managed_id(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::ManagedId,
value: value.into(),
}
}
pub fn order(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Order,
value: value.into(),
}
}
pub fn schema(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Schema,
value: value.into(),
}
}
pub fn derived(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Derived,
value: value.into(),
}
}
pub fn gap(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Gap,
value: value.into(),
}
}
pub fn linkrel(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Linkrel,
value: value.into(),
}
}
pub fn jsptr(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Jsptr,
value: value.into(),
}
}
pub fn jsid(value: impl Into<ICalendarParameterValue>) -> Self {
ICalendarParameter {
name: ICalendarParameterName::Jsid,
value: value.into(),
}
}
}
impl From<String> for ICalendarParameterValue {
fn from(text: String) -> Self {
ICalendarParameterValue::Text(text)
}
}
impl From<u64> for ICalendarParameterValue {
fn from(integer: u64) -> Self {
ICalendarParameterValue::Integer(integer)
}
}
impl From<Integer> for ICalendarParameterValue {
fn from(integer: Integer) -> Self {
ICalendarParameterValue::Integer(integer.0.unsigned_abs())
}
}
impl From<Boolean> for ICalendarParameterValue {
fn from(boolean: Boolean) -> Self {
ICalendarParameterValue::Bool(boolean.0)
}
}
impl From<bool> for ICalendarParameterValue {
fn from(boolean: bool) -> Self {
ICalendarParameterValue::Bool(boolean)
}
}
impl From<Uri> for ICalendarParameterValue {
fn from(uri: Uri) -> Self {
ICalendarParameterValue::Uri(uri)
}
}
impl From<ICalendarUserTypes> for ICalendarParameterValue {
fn from(cutype: ICalendarUserTypes) -> Self {
ICalendarParameterValue::Cutype(cutype)
}
}
impl From<ICalendarFreeBusyType> for ICalendarParameterValue {
fn from(fbtype: ICalendarFreeBusyType) -> Self {
ICalendarParameterValue::Fbtype(fbtype)
}
}
impl From<ICalendarParticipationStatus> for ICalendarParameterValue {
fn from(partstat: ICalendarParticipationStatus) -> Self {
ICalendarParameterValue::Partstat(partstat)
}
}
impl From<ICalendarRelated> for ICalendarParameterValue {
fn from(related: ICalendarRelated) -> Self {
ICalendarParameterValue::Related(related)
}
}
impl From<ICalendarRelationshipType> for ICalendarParameterValue {
fn from(reltype: ICalendarRelationshipType) -> Self {
ICalendarParameterValue::Reltype(reltype)
}
}
impl From<ICalendarParticipationRole> for ICalendarParameterValue {
fn from(role: ICalendarParticipationRole) -> Self {
ICalendarParameterValue::Role(role)
}
}
impl From<ICalendarScheduleAgentValue> for ICalendarParameterValue {
fn from(schedule_agent: ICalendarScheduleAgentValue) -> Self {
ICalendarParameterValue::ScheduleAgent(schedule_agent)
}
}
impl From<ICalendarScheduleForceSendValue> for ICalendarParameterValue {
fn from(schedule_force_send: ICalendarScheduleForceSendValue) -> Self {
ICalendarParameterValue::ScheduleForceSend(schedule_force_send)
}
}
impl From<ICalendarValueType> for ICalendarParameterValue {
fn from(value: ICalendarValueType) -> Self {
ICalendarParameterValue::Value(value)
}
}
impl From<ICalendarDisplayType> for ICalendarParameterValue {
fn from(display: ICalendarDisplayType) -> Self {
ICalendarParameterValue::Display(display)
}
}
impl From<ICalendarFeatureType> for ICalendarParameterValue {
fn from(feature: ICalendarFeatureType) -> Self {
ICalendarParameterValue::Feature(feature)
}
}
impl From<ICalendarDuration> for ICalendarParameterValue {
fn from(duration: ICalendarDuration) -> Self {
ICalendarParameterValue::Duration(duration)
}
}
impl From<LinkRelation> for ICalendarParameterValue {
fn from(linkrel: LinkRelation) -> Self {
ICalendarParameterValue::Linkrel(linkrel)
}
}
impl<T: Into<ICalendarParameterValue>> From<IanaType<T, String>> for ICalendarParameterValue {
fn from(value: IanaType<T, String>) -> Self {
match value {
IanaType::Iana(v) => v.into(),
IanaType::Other(s) => s.into(),
}
}
}
impl From<Vec<u8>> for ICalendarValue {
fn from(value: Vec<u8>) -> Self {
ICalendarValue::Binary(value)
}
}
impl From<bool> for ICalendarValue {
fn from(value: bool) -> Self {
ICalendarValue::Boolean(value)
}
}
impl From<Data> for Uri {
fn from(value: Data) -> Self {
Uri::Data(Box::new(value))
}
}
impl From<Uri> for ICalendarValue {
fn from(value: Uri) -> Self {
ICalendarValue::Uri(value)
}
}
impl From<PartialDateTime> for ICalendarValue {
fn from(value: PartialDateTime) -> Self {
ICalendarValue::PartialDateTime(value)
}
}
impl From<Box<PartialDateTime>> for ICalendarValue {
fn from(value: Box<PartialDateTime>) -> Self {
ICalendarValue::PartialDateTime(*value)
}
}
impl From<ICalendarDuration> for ICalendarValue {
fn from(value: ICalendarDuration) -> Self {
ICalendarValue::Duration(value)
}
}
impl From<ICalendarRecurrenceRule> for ICalendarValue {
fn from(value: ICalendarRecurrenceRule) -> Self {
ICalendarValue::RecurrenceRule(Box::new(value))
}
}
impl From<Box<ICalendarRecurrenceRule>> for ICalendarValue {
fn from(value: Box<ICalendarRecurrenceRule>) -> Self {
ICalendarValue::RecurrenceRule(value)
}
}
impl From<ICalendarPeriod> for ICalendarValue {
fn from(value: ICalendarPeriod) -> Self {
ICalendarValue::Period(Box::new(value))
}
}
impl From<f64> for ICalendarValue {
fn from(value: f64) -> Self {
ICalendarValue::Float(value)
}
}
impl From<i64> for ICalendarValue {
fn from(value: i64) -> Self {
ICalendarValue::Integer(value)
}
}
impl From<String> for ICalendarValue {
fn from(value: String) -> Self {
ICalendarValue::Text(value)
}
}
impl From<&str> for ICalendarValue {
fn from(value: &str) -> Self {
ICalendarValue::Text(value.to_string())
}
}
impl From<CalendarScale> for ICalendarValue {
fn from(value: CalendarScale) -> Self {
ICalendarValue::CalendarScale(value)
}
}
impl From<ICalendarMethod> for ICalendarValue {
fn from(value: ICalendarMethod) -> Self {
ICalendarValue::Method(value)
}
}
impl From<ICalendarClassification> for ICalendarValue {
fn from(value: ICalendarClassification) -> Self {
ICalendarValue::Classification(value)
}
}
impl From<ICalendarStatus> for ICalendarValue {
fn from(value: ICalendarStatus) -> Self {
ICalendarValue::Status(value)
}
}
impl From<ICalendarTransparency> for ICalendarValue {
fn from(value: ICalendarTransparency) -> Self {
ICalendarValue::Transparency(value)
}
}
impl From<ICalendarAction> for ICalendarValue {
fn from(value: ICalendarAction) -> Self {
ICalendarValue::Action(value)
}
}
impl From<ICalendarFreeBusyType> for ICalendarValue {
fn from(value: ICalendarFreeBusyType) -> Self {
ICalendarValue::BusyType(value)
}
}
impl From<ICalendarParticipantType> for ICalendarValue {
fn from(value: ICalendarParticipantType) -> Self {
ICalendarValue::ParticipantType(value)
}
}
impl From<ICalendarResourceType> for ICalendarValue {
fn from(value: ICalendarResourceType) -> Self {
ICalendarValue::ResourceType(value)
}
}
impl From<ICalendarProximityValue> for ICalendarValue {
fn from(value: ICalendarProximityValue) -> Self {
ICalendarValue::Proximity(value)
}
}
+863
View File
@@ -0,0 +1,863 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use super::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarPeriod, ICalendarProperty,
ICalendarRecurrenceRule, ICalendarValue, timezone::TzResolver,
};
use crate::{
common::{
DateTimeResult,
timezone::{NominalDuration, Tz, ZonedDateTime},
},
datecalc::{MAX_UNPRODUCTIVE_WORK, error::RRuleError, rrule::RRule},
icalendar::ICalendarParameterName,
};
use ahash::{AHashMap, AHashSet};
use jiff::{SignedDuration, civil};
use std::{
collections::hash_map::Entry,
fmt::{Display, Formatter},
hash::Hash,
};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize)
)]
pub struct CalendarExpand {
pub events: Vec<CalendarEvent>,
pub errors: Vec<CalendarError>,
}
/// One instance of a calendar component, with its end already resolved.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize)
)]
pub struct CalendarEvent {
pub comp_id: u32,
pub start: ZonedDateTime,
pub end: ZonedDateTime,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize)
)]
pub struct CalendarError {
pub comp_id: u32,
pub error: CalendarErrorType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize)
)]
pub enum CalendarErrorType {
MissingDtStart,
InvalidDtStart,
InvalidDtEnd,
InvalidDuration,
RRule(RRuleError),
ExpansionLimitReached,
}
impl ICalendar {
pub fn expand_dates(&self, default_tz: impl Into<Tz>, mut limit: usize) -> CalendarExpand {
let tz_resolver = self.build_tz_resolver().with_default(default_tz);
let mut expand = CalendarExpand::default();
let mut recurrences = Vec::new();
let mut overridden = OverriddenInstances::default();
for (comp_id, comp) in self.components.iter().enumerate() {
if comp.component_type.has_time_ranges() {
match comp.build_calendar_date(comp_id as u32, &tz_resolver) {
Ok(Some(mut event)) => {
if event.rid.is_some() {
expand
.events
.extend(event.rdates.drain(..).map(|rdate| rdate.event));
overridden.insert(comp_id as u32, event);
} else if event.rrule.is_some()
|| !event.rdates.is_empty()
|| !event.exdates.is_empty()
{
recurrences.push((comp_id as u32, event));
} else if let Some(cal_event) = event.event {
expand.events.push(cal_event);
}
}
Ok(None) => {}
Err(error) => {
expand.errors.push(CalendarError {
comp_id: comp_id as u32,
error,
});
}
}
}
}
// Expand recurrences
let mut unproductive_budget = MAX_UNPRODUCTIVE_WORK;
for (series_id, event) in &mut recurrences {
let mut comp_id = *series_id;
let length = event.length();
let exdates = event
.exdates
.iter()
.filter_map(|(tz_id, dt)| {
dt.to_date_time_with_tz(tz_id.map_or(event.start_tz, |tz_id| {
tz_resolver.resolve_or_default(Some(tz_id))
}))
})
.collect::<AHashSet<_>>();
let start_instance = event
.rrule
.is_none()
.then(|| event.event.take())
.flatten()
.map(|start| RecurrenceInstance {
event: start,
length,
});
for instance in std::mem::take(&mut event.rdates)
.into_iter()
.chain(start_instance)
{
match overridden.take(event, &instance.event.start) {
Some((_, overridden_event)) => expand.events.extend(
overridden_event
.replacing(instance.event.start, instance.length)
.map(|(replacement, _)| replacement),
),
None if !exdates.contains(&instance.event.start) => {
expand.events.push(instance.event)
}
None => {}
}
}
let Some(rrule) = event.rrule.take() else {
continue;
};
let rrule = match RRule::from_ical(rrule, event.dt_start_zoned, event.is_date) {
Ok(rrule) => rrule,
Err(err) => {
expand.errors.push(CalendarError {
comp_id,
error: CalendarErrorType::RRule(err),
});
continue;
}
};
let mut override_offset = None;
let mut override_duration = None;
let mut instances = rrule.iter().with_unproductive_budget(unproductive_budget);
while limit != 0 {
let Some(date) = instances.next() else {
break;
};
limit -= 1;
if event.rdate_starts.contains(&date) {
continue;
}
match overridden.take(event, &date) {
Some((new_comp_id, overridden_event)) => {
if let Some((new_event, new_duration)) =
overridden_event.replacing(date, length)
{
if overridden_event.rid_this_and_future {
comp_id = new_comp_id;
override_offset =
Some(DefaultDuration::shift(date, new_event.start));
override_duration = Some(new_duration);
}
expand.events.push(new_event);
}
}
None if !exdates.contains(&date) => {
let start = match override_offset {
Some(offset) => match offset.after(date) {
Some(start) => start,
None => continue,
},
None => date,
};
let duration = override_duration.unwrap_or(event.default_duration);
if let Some(end) = duration.after(start) {
expand.events.push(CalendarEvent {
start,
end,
comp_id,
});
}
}
None => {}
}
}
unproductive_budget = instances.unproductive_budget();
if instances.is_exhausted() {
expand.errors.push(CalendarError {
comp_id,
error: CalendarErrorType::ExpansionLimitReached,
});
}
}
// Add missing overridden events (this should not occur unless the iCalendar is malformed)
expand.events.extend(overridden.into_events(&recurrences));
expand
}
}
/// How long an instance of a component lasts.
#[derive(Debug, Clone, Copy)]
enum DefaultDuration {
Exact(SignedDuration),
Nominal(NominalDuration),
}
impl DefaultDuration {
/// Returns the duration between the start and end of a series, as
/// RFC 5545 section 3.8.5.3 says every instance of it should last.
fn between(start: ZonedDateTime, end: ZonedDateTime, has_time: bool) -> Self {
if has_time {
Self::Exact(SignedDuration::from_secs(
end.timestamp() - start.timestamp(),
))
} else {
Self::Nominal(NominalDuration::new(end.days_since(start), 0))
}
}
/// Returns the end of an instance that starts at `start`.
fn shift(from: ZonedDateTime, to: ZonedDateTime) -> Self {
if from.timezone() == to.timezone() {
Self::Nominal(NominalDuration::between(from, to))
} else {
Self::Exact(to.signed_duration_since(from))
}
}
fn after(&self, start: ZonedDateTime) -> Option<ZonedDateTime> {
match self {
Self::Exact(duration) => start.checked_add(*duration),
Self::Nominal(duration) => start.checked_add_nominal(*duration),
}
}
}
struct CalendarEventBuilder<'x> {
event: Option<CalendarEvent>,
component_type: &'x ICalendarComponentType,
start_tz: Tz,
default_duration: DefaultDuration,
inherits_start: bool,
instance_end: InstanceEnd,
dt_start_zoned: ZonedDateTime,
is_date: bool,
rrule: Option<&'x ICalendarRecurrenceRule>,
uid: Option<&'x str>,
sequence: i64,
rdates: Vec<RecurrenceInstance>,
rdate_starts: AHashSet<ZonedDateTime>,
exdates: Vec<(Option<&'x str>, DateTimeResult)>,
rid: Option<ZonedDateTime>,
rid_this_and_future: bool,
}
#[derive(Debug, Clone, Copy)]
struct RecurrenceInstance {
event: CalendarEvent,
length: InstanceLength,
}
#[derive(Debug, Clone, Copy)]
struct InstanceLength {
duration: DefaultDuration,
is_date: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InstanceEnd {
Inherited,
Relative,
Fixed,
}
impl InstanceEnd {
fn of(tz_id: Option<&str>, end: &DateTimeResult) -> Self {
if tz_id.is_some() || end.tz().is_some() {
InstanceEnd::Fixed
} else {
InstanceEnd::Relative
}
}
}
impl CalendarEventBuilder<'_> {
fn length(&self) -> InstanceLength {
InstanceLength {
duration: self.default_duration,
is_date: self.is_date,
}
}
fn anchor(&self, recurrence_id: ZonedDateTime) -> Option<ZonedDateTime> {
if recurrence_id.timezone().is_floating() {
self.start_tz.from_local(recurrence_id.naive_local())
} else {
Some(recurrence_id.with_timezone(self.start_tz))
}
}
fn replacing(
&self,
start: ZonedDateTime,
replaced: InstanceLength,
) -> Option<(CalendarEvent, DefaultDuration)> {
let event = self.event?;
if !self.inherits_start || self.is_date != replaced.is_date {
return Some((event, self.default_duration));
}
let (end, duration) = match self.instance_end {
InstanceEnd::Fixed => (
event.end,
DefaultDuration::Exact(event.end.signed_duration_since(start)),
),
InstanceEnd::Relative => (
self.default_duration.after(start).unwrap_or(event.end),
self.default_duration,
),
InstanceEnd::Inherited => (
replaced.duration.after(start).unwrap_or(event.end),
replaced.duration,
),
};
Some((
CalendarEvent {
start,
end,
..event
},
duration,
))
}
}
type OverriddenInstance<'x> = (u32, CalendarEventBuilder<'x>);
type OverrideKey<'x, T> = (Option<&'x str>, &'x ICalendarComponentType, T);
#[derive(Default)]
struct OverriddenInstances<'x> {
zoned: AHashMap<OverrideKey<'x, ZonedDateTime>, OverriddenInstance<'x>>,
floating: AHashMap<OverrideKey<'x, civil::DateTime>, OverriddenInstance<'x>>,
}
impl<'x> OverriddenInstances<'x> {
fn insert(&mut self, comp_id: u32, instance: CalendarEventBuilder<'x>) {
match instance.rid {
Some(rid) if rid.timezone().is_floating() => {
Self::insert_latest(
&mut self.floating,
(instance.uid, instance.component_type, rid.naive_local()),
(comp_id, instance),
);
}
Some(rid) => {
Self::insert_latest(
&mut self.zoned,
(instance.uid, instance.component_type, rid),
(comp_id, instance),
);
}
None => {}
}
}
fn insert_latest<K: Eq + Hash>(
instances: &mut AHashMap<K, OverriddenInstance<'x>>,
key: K,
instance: OverriddenInstance<'x>,
) {
match instances.entry(key) {
Entry::Occupied(current) if current.get().1.sequence > instance.1.sequence => {}
Entry::Occupied(mut current) => {
current.insert(instance);
}
Entry::Vacant(slot) => {
slot.insert(instance);
}
}
}
fn take(
&mut self,
series: &CalendarEventBuilder<'x>,
start: &ZonedDateTime,
) -> Option<OverriddenInstance<'x>> {
(!self.zoned.is_empty())
.then(|| {
self.zoned
.remove(&(series.uid, series.component_type, *start))
})
.flatten()
.or_else(|| {
(!self.floating.is_empty())
.then(|| {
self.floating.remove(&(
series.uid,
series.component_type,
start.naive_local(),
))
})
.flatten()
})
}
fn into_events<'y>(
self,
recurrences: &'y [(u32, CalendarEventBuilder<'x>)],
) -> impl Iterator<Item = CalendarEvent> + use<'x, 'y> {
let series = if self.zoned.is_empty() && self.floating.is_empty() {
AHashMap::new()
} else {
recurrences
.iter()
.rev()
.map(|(_, series)| ((series.uid, series.component_type), series))
.collect::<AHashMap<_, _>>()
};
self.zoned
.into_values()
.chain(self.floating.into_values())
.filter_map(move |(_, instance)| {
series
.get(&(instance.uid, instance.component_type))
.and_then(|series| {
instance.replacing(series.anchor(instance.rid?)?, series.length())
})
.map(|(replacement, _)| replacement)
.or(instance.event)
})
}
}
/// How an `RDATE` of PERIOD value type names the end of its instance.
enum PeriodEnd {
Time(DateTimeResult),
Duration(NominalDuration),
}
impl ICalendarComponent {
fn build_calendar_date(
&self,
comp_id: u32,
tz_resolver: &TzResolver<&'_ str>,
) -> Result<Option<CalendarEventBuilder<'_>>, CalendarErrorType> {
let mut dt_start = None;
let mut dt_start_tzid = None;
let mut dt_start_has_time = false;
let mut dt_end: Option<DateTimeResult> = None;
let mut dt_end_tzid = None;
let mut todo_dates = vec![];
let mut rid: Option<DateTimeResult> = None;
let mut rid_tzid = None;
let mut rid_has_time = false;
let mut rid_this_and_future = false;
let mut duration = None;
let mut rrule = None;
let mut uid = None;
let mut sequence = 0;
let mut rdates = vec![];
let mut rdates_periods = vec![];
let mut exdates = vec![];
for entry in &self.entries {
match (&entry.name, entry.values.first()) {
(ICalendarProperty::Dtstart, Some(ICalendarValue::PartialDateTime(dt))) => {
dt_start = dt.to_date_time();
dt_start_tzid = entry.tz_id();
dt_start_has_time = dt.has_time();
}
(ICalendarProperty::Dtend, Some(ICalendarValue::PartialDateTime(dt))) => {
if let Some(dt) = dt.to_date_time() {
dt_end = Some(dt);
dt_end_tzid = entry.tz_id();
}
}
(
ICalendarProperty::Due
| ICalendarProperty::Completed
| ICalendarProperty::Created,
Some(ICalendarValue::PartialDateTime(dt)),
) if self.component_type == ICalendarComponentType::VTodo => {
todo_dates.push((&entry.name, dt.to_date_time(), entry.tz_id()));
}
(ICalendarProperty::RecurrenceId, Some(ICalendarValue::PartialDateTime(value))) => {
if let Some(dt) = value.to_date_time() {
for param in &entry.params {
match &param.name {
ICalendarParameterName::Tzid => {
rid_tzid = param.value.as_text();
}
ICalendarParameterName::Range => {
rid_this_and_future = true;
}
_ => (),
}
}
rid_has_time = value.has_time();
rid = Some(dt);
}
}
(ICalendarProperty::Duration, Some(ICalendarValue::Duration(dur))) => {
duration = Some(dur);
}
(ICalendarProperty::Rrule, Some(ICalendarValue::RecurrenceRule(rule))) => {
rrule = Some(rule);
}
(ICalendarProperty::Uid, Some(ICalendarValue::Text(value))) => {
uid = Some(value.as_str());
}
(ICalendarProperty::Sequence, Some(ICalendarValue::Integer(value))) => {
sequence = *value;
}
(ICalendarProperty::Rdate, _) => {
let tz_id = entry.tz_id();
for value in &entry.values {
match value {
ICalendarValue::PartialDateTime(dt) => {
if let Some(dt) = dt.to_date_time() {
rdates.push((tz_id, dt));
}
}
ICalendarValue::Period(period) => match period.as_ref() {
ICalendarPeriod::Range { start, end } => {
if let (Some(start), Some(end)) =
(start.to_date_time(), end.to_date_time())
{
rdates_periods.push((tz_id, start, PeriodEnd::Time(end)));
}
}
ICalendarPeriod::Duration { start, duration } => {
if let (Some(start), Some(duration)) =
(start.to_date_time(), duration.to_nominal())
{
rdates_periods.push((
tz_id,
start,
PeriodEnd::Duration(duration),
));
}
}
},
_ => (),
}
}
}
(ICalendarProperty::Exdate, _) => {
let tz_id = entry.tz_id();
for value in &entry.values {
if let ICalendarValue::PartialDateTime(dt) = value
&& let Some(dt) = dt.to_date_time()
{
exdates.push((tz_id, dt));
}
}
}
_ => (),
}
}
let has_start = dt_start.is_some();
let dt_start = match dt_start {
Some(dt_start) => dt_start,
None => match &rid {
Some(rid) => {
dt_start_tzid = rid_tzid;
dt_start_has_time = rid_has_time;
rid.clone()
}
None => match self.component_type {
ICalendarComponentType::VEvent => {
return Err(CalendarErrorType::MissingDtStart);
}
ICalendarComponentType::VTodo => {
let mut due_idx = None;
let mut completed_idx = None;
let mut created_idx = None;
for (idx, (prop, dt, _)) in todo_dates.iter().enumerate() {
if dt.is_some() {
match prop {
ICalendarProperty::Due => due_idx = Some(idx),
ICalendarProperty::Completed => completed_idx = Some(idx),
ICalendarProperty::Created => created_idx = Some(idx),
_ => (),
}
}
}
match (due_idx, completed_idx, created_idx) {
(Some(due_idx), _, _) => {
let due = &mut todo_dates[due_idx];
dt_start_tzid = due.2;
dt_start_has_time = true;
due.1.take().unwrap()
}
(_, Some(completed_idx), Some(created_idx)) => {
let completed = &mut todo_dates[completed_idx];
dt_end = completed.1.take();
dt_end_tzid = completed.2;
let created = &mut todo_dates[created_idx];
dt_start_tzid = created.2;
created.1.take().unwrap()
}
(_, Some(date_idx), _) | (_, _, Some(date_idx)) => {
let date = &mut todo_dates[date_idx];
dt_start_tzid = date.2;
dt_start_has_time = true;
date.1.take().unwrap()
}
_ => {
return Ok(None);
}
}
}
_ => {
return Ok(None);
}
},
},
};
let mut event = None;
let is_instance = rid.is_some();
let start_tz = dt_start
.tz()
.unwrap_or_else(|| tz_resolver.resolve_or_default(dt_start_tzid));
let value_tz = |tz_id: Option<&str>| {
tz_id.map_or(start_tz, |tz_id| {
tz_resolver.resolve_or_default(Some(tz_id))
})
};
let dt_start_tz = dt_start
.to_date_time_with_tz(start_tz)
.ok_or(CalendarErrorType::InvalidDtStart)?;
let mut instance_end = InstanceEnd::Relative;
let default_duration = if let Some(dt_end) = dt_end {
let end = dt_end
.to_date_time_with_tz(tz_resolver.resolve_or_default(dt_end_tzid.or(dt_start_tzid)))
.ok_or(CalendarErrorType::InvalidDtEnd)?;
if rrule.is_none() || is_instance {
event = Some(CalendarEvent {
start: dt_start_tz,
end,
comp_id,
});
}
instance_end = InstanceEnd::of(dt_end_tzid, &dt_end);
DefaultDuration::between(dt_start_tz, end, dt_start_has_time)
} else if let Some(duration) = duration {
let duration = duration
.to_nominal()
.ok_or(CalendarErrorType::InvalidDuration)?;
let duration = DefaultDuration::Nominal(duration);
if rrule.is_none() || is_instance {
event = Some(CalendarEvent {
start: dt_start_tz,
end: duration
.after(dt_start_tz)
.ok_or(CalendarErrorType::InvalidDuration)?,
comp_id,
});
}
duration
} else if let Some((due, due_tzid)) = todo_dates
.into_iter()
.filter_map(|(prop, dt, tz_id)| {
if prop == &ICalendarProperty::Due {
dt.map(|dt| (dt, tz_id))
} else {
None
}
})
.next()
{
let end = due
.to_date_time_with_tz(tz_resolver.resolve_or_default(due_tzid.or(dt_start_tzid)))
.ok_or(CalendarErrorType::InvalidDtEnd)?;
if rrule.is_none() || is_instance {
event = Some(CalendarEvent {
start: dt_start_tz,
end,
comp_id,
});
}
instance_end = InstanceEnd::of(due_tzid, &due);
DefaultDuration::between(dt_start_tz, end, dt_start_has_time)
} else {
/*
For cases where a "VEVENT" calendar component
specifies a "DTSTART" property with a DATE value type but no
"DTEND" nor "DURATION" property, the event's duration is taken to
be one day. For cases where a "VEVENT" calendar component
specifies a "DTSTART" property with a DATE-TIME value type but no
"DTEND" property, the event ends on the same calendar date and
time of day specified by the "DTSTART" property.
*/
instance_end = InstanceEnd::Inherited;
let duration =
if dt_start_has_time || self.component_type == ICalendarComponentType::VTodo {
DefaultDuration::Exact(SignedDuration::ZERO)
} else {
DefaultDuration::Nominal(NominalDuration::DAY)
};
if rrule.is_none() || is_instance {
event = Some(CalendarEvent {
start: dt_start_tz,
end: duration
.after(dt_start_tz)
.ok_or(CalendarErrorType::InvalidDuration)?,
comp_id,
});
}
duration
};
let rid = rid.and_then(|rid| {
rid.to_date_time_with_tz(rid_tzid.map_or(Tz::Floating, |tz_id| {
tz_resolver.resolve_or_default(Some(tz_id))
}))
});
// Add rdates
let is_date = !dt_start_has_time;
let series_length = InstanceLength {
duration: default_duration,
is_date,
};
let mut rdate_events = rdates
.into_iter()
.filter_map(|(tz_id, rdate)| {
let start = rdate.to_date_time_with_tz(value_tz(tz_id))?;
Some(RecurrenceInstance {
event: CalendarEvent {
start,
end: default_duration.after(start)?,
comp_id,
},
length: series_length,
})
})
.chain(
rdates_periods
.into_iter()
.filter_map(|(tz_id, start, end)| {
let tz = value_tz(tz_id);
let start = start.to_date_time_with_tz(tz)?;
let (end, duration) = match end {
PeriodEnd::Time(end) => {
let end = end.to_date_time_with_tz(tz)?;
(
end,
DefaultDuration::Exact(end.signed_duration_since(start)),
)
}
PeriodEnd::Duration(duration) => {
let duration = DefaultDuration::Nominal(duration);
(duration.after(start)?, duration)
}
};
Some(RecurrenceInstance {
event: CalendarEvent {
start,
end,
comp_id,
},
length: InstanceLength {
duration,
is_date: false,
},
})
}),
)
.collect::<Vec<_>>();
let mut rdate_starts = AHashSet::new();
if !rdate_events.is_empty() {
rdate_events.retain(|rdate| rdate_starts.insert(rdate.event.start));
if event
.as_ref()
.is_some_and(|event| rdate_starts.contains(&event.start))
{
event = None;
}
}
Ok(Some(CalendarEventBuilder {
event,
component_type: &self.component_type,
default_duration,
inherits_start: is_instance && !has_start,
instance_end,
dt_start_zoned: dt_start_tz,
is_date,
uid,
sequence,
rrule: rrule.map(|rule| &**rule),
rdates: rdate_events,
rdate_starts,
exdates,
start_tz,
rid,
rid_this_and_future,
}))
}
}
impl CalendarEvent {
/// Returns the instant the instance starts and the instant it ends.
pub fn timestamps(&self) -> (i64, i64) {
(self.start.timestamp(), self.end.timestamp())
}
/// Returns the wall clock readings of the start and end, as seconds since
/// the Unix epoch, for callers that index instances by displayed time.
pub fn naive_timestamps(&self) -> (i64, i64) {
(self.start.naive_timestamp(), self.end.naive_timestamp())
}
}
impl Display for CalendarErrorType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
CalendarErrorType::MissingDtStart => write!(f, "Missing DTSTART property"),
CalendarErrorType::InvalidDtStart => write!(f, "Invalid DTSTART property"),
CalendarErrorType::InvalidDtEnd => write!(f, "Invalid DTEND property"),
CalendarErrorType::InvalidDuration => write!(f, "Invalid DURATION property"),
CalendarErrorType::RRule(err) => write!(f, "RRule error: {err}"),
CalendarErrorType::ExpansionLimitReached => {
write!(f, "Recurrence expansion gave up before the rule finished")
}
}
}
}
+980
View File
@@ -0,0 +1,980 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
#[cfg(feature = "rkyv")]
use crate::common::archive::{InlineVec, Unboxed};
use crate::common::{
CalendarScale, Data, IanaParse, IanaString, IanaType, LinkRelation, PartialDateTime,
};
use smallvec::SmallVec;
use std::hash::{Hash, Hasher};
pub mod builder;
pub mod dates;
pub mod parser;
pub mod timezone;
pub mod types;
pub mod utils;
pub mod writer;
#[cfg(feature = "rkyv")]
pub mod rkyv_timezone;
#[cfg(feature = "rkyv")]
pub mod rkyv_types;
#[cfg(feature = "rkyv")]
pub mod rkyv_utils;
#[cfg(feature = "rkyv")]
pub mod rkyv_writer;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))]
pub struct ICalendar {
pub components: Vec<ICalendarComponent>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))]
pub struct ICalendarComponent {
pub component_type: ICalendarComponentType,
pub entries: Vec<ICalendarEntry>,
pub component_ids: Vec<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))]
pub struct ICalendarEntry {
pub name: ICalendarProperty,
pub params: Vec<ICalendarParameter>,
#[cfg_attr(feature = "rkyv", rkyv(with = InlineVec))]
pub values: SmallVec<[ICalendarValue; 1]>,
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))]
pub enum ICalendarValue {
Binary(Vec<u8>),
Boolean(bool),
Uri(Uri),
PartialDateTime(PartialDateTime),
Duration(ICalendarDuration),
RecurrenceRule(Box<ICalendarRecurrenceRule>),
Period(#[cfg_attr(feature = "rkyv", rkyv(with = Unboxed))] Box<ICalendarPeriod>),
Float(f64),
Integer(i64),
Text(String),
CalendarScale(CalendarScale),
Method(ICalendarMethod),
Classification(ICalendarClassification),
Status(ICalendarStatus),
Transparency(ICalendarTransparency),
Action(ICalendarAction),
BusyType(ICalendarFreeBusyType),
ParticipantType(ICalendarParticipantType),
ResourceType(ICalendarResourceType),
Proximity(ICalendarProximityValue),
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub struct ICalendarRecurrenceRule {
pub freq: ICalendarFrequency,
pub until: Option<PartialDateTime>,
pub count: Option<u32>,
pub interval: Option<u16>,
pub bysecond: Vec<u8>,
pub byminute: Vec<u8>,
pub byhour: Vec<u8>,
pub byday: Vec<ICalendarDay>,
pub bymonthday: Vec<i8>,
pub byyearday: Vec<i16>,
pub byweekno: Vec<i8>,
pub bymonth: Vec<ICalendarMonth>,
pub bysetpos: Vec<i32>,
pub wkst: Option<ICalendarWeekday>,
pub rscale: Option<CalendarScale>,
pub skip: Option<ICalendarSkip>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
#[repr(u8)]
pub enum ICalendarSkip {
Omit,
Backward,
Forward,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub struct ICalendarDay {
pub ordwk: Option<i16>,
pub weekday: ICalendarWeekday,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
#[repr(transparent)]
pub struct ICalendarMonth(i8);
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
#[repr(u8)]
pub enum ICalendarFrequency {
Yearly = 0,
Monthly = 1,
Weekly = 2,
#[default]
Daily = 3,
Hourly = 4,
Minutely = 5,
Secondly = 6,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarWeekday {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarPeriod {
Range {
start: PartialDateTime,
end: PartialDateTime,
},
Duration {
start: PartialDateTime,
duration: ICalendarDuration,
},
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarAction {
Audio, // [RFC5545, Section 3.8.6.1]
Display, // [RFC5545, Section 3.8.6.1]
Email, // [RFC5545, Section 3.8.6.1]
Procedure, // [RFC2445, Section 4.8.6.1]
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarUserTypes {
Individual, // [RFC5545, Section 3.2.3]
Group, // [RFC5545, Section 3.2.3]
Resource, // [RFC5545, Section 3.2.3]
Room, // [RFC5545, Section 3.2.3]
Unknown, // [RFC5545, Section 3.2.3]
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarClassification {
Public, // [RFC5545, Section 3.8.1.3]
Private, // [RFC5545, Section 3.8.1.3]
Confidential, // [RFC5545, Section 3.8.1.3]
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarComponentType {
Other(String),
#[default]
VCalendar, // [RFC5545, Section 3.4]
VEvent, // [RFC5545, Section 3.6.1]
VTodo, // [RFC5545, Section 3.6.2]
VJournal, // [RFC5545, Section 3.6.3]
VFreebusy, // [RFC5545, Section 3.6.4]
VTimezone, // [RFC5545, Section 3.6.5]
VAlarm, // [RFC5545, Section 3.6.6]
Standard, // [RFC5545, Section 3.6.5]
Daylight, // [RFC5545, Section 3.6.5]
VAvailability, // [RFC7953, Section 3.1]
Available, // [RFC7953, Section 3.1]
Participant, // [RFC9073, Section 7.1]
VLocation, // [RFC9073, Section 7.2] [RFC Errata 7381]
VResource, // [RFC9073, Section 7.3]
VStatus, // draft-ietf-calext-ical-tasks-17
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarDisplayType {
Badge, // [RFC7986, Section 6.1]
Graphic, // [RFC7986, Section 6.1]
Fullsize, // [RFC7986, Section 6.1]
Thumbnail, // [RFC7986, Section 6.1]
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarFeatureType {
Audio, // [RFC7986, Section 6.3]
Chat, // [RFC7986, Section 6.3]
Feed, // [RFC7986, Section 6.3]
Moderator, // [RFC7986, Section 6.3]
Phone, // [RFC7986, Section 6.3]
Screen, // [RFC7986, Section 6.3]
Video, // [RFC7986, Section 6.3]
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarFreeBusyType {
Free, // [RFC5545, Section 3.2.9]
Busy, // [RFC5545, Section 3.2.9]
BusyUnavailable, // [RFC5545, Section 3.2.9]
BusyTentative, // [RFC5545, Section 3.2.9]
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarMethod {
Publish, // [RFC5546]
Request, // [RFC5546]
Reply, // [RFC5546]
Add, // [RFC5546]
Cancel, // [RFC5546]
Refresh, // [RFC5546]
Counter, // [RFC5546]
Declinecounter, // [RFC5546]
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))]
pub struct ICalendarParameter {
pub name: ICalendarParameterName,
pub value: ICalendarParameterValue,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))]
pub enum ICalendarParameterValue {
Text(String),
Integer(u64),
Bool(bool),
Uri(Uri),
Cutype(ICalendarUserTypes),
Fbtype(ICalendarFreeBusyType),
Partstat(ICalendarParticipationStatus),
Related(ICalendarRelated),
Reltype(ICalendarRelationshipType),
Role(ICalendarParticipationRole),
ScheduleAgent(ICalendarScheduleAgentValue),
ScheduleForceSend(ICalendarScheduleForceSendValue),
Value(ICalendarValueType),
Display(ICalendarDisplayType),
Feature(ICalendarFeatureType),
Duration(ICalendarDuration),
Linkrel(LinkRelation),
Null,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(PartialEq)))]
pub enum ICalendarParameterName {
Other(String),
Altrep, // [RFC5545, Section 3.2.1]
Cn, // [RFC5545, Section 3.2.2]
Cutype, // [RFC5545, Section 3.2.3]
DelegatedFrom, // [RFC5545, Section 3.2.4]
DelegatedTo, // [RFC5545, Section 3.2.5]
Dir, // [RFC5545, Section 3.2.6]
Fmttype, // [RFC5545, Section 3.2.8]
Fbtype, // [RFC5545, Section 3.2.9]
Language, // [RFC5545, Section 3.2.10]
Member, // [RFC5545, Section 3.2.11]
Partstat, // [RFC5545, Section 3.2.12]
Range, // [RFC5545, Section 3.2.13]
Related, // [RFC5545, Section 3.2.14]
Reltype, // [RFC5545, Section 3.2.15]
Role, // [RFC5545, Section 3.2.16]
Rsvp, // [RFC5545, Section 3.2.17]
ScheduleAgent, // [RFC6638, Section 7.1]
ScheduleForceSend, // [RFC6638, Section 7.2]
ScheduleStatus, // [RFC6638, Section 7.3]
SentBy, // [RFC5545, Section 3.2.18]
Tzid, // [RFC5545, Section 3.2.19]
Value, // [RFC5545, Section 3.2.20]
Display, // [RFC7986, Section 6.1]
Email, // [RFC7986, Section 6.2]
Feature, // [RFC7986, Section 6.3]
Label, // [RFC7986, Section 6.4]
Size, // [RFC8607, Section 4.1]
Filename, // [RFC8607, Section 4.2]
ManagedId, // [RFC8607, Section 4.3]
Order, // [RFC9073, Section 5.1]
Schema, // [RFC9073, Section 5.2]
Derived, // [RFC9073, Section 5.3]
Gap, // [RFC9253, Section 6.2]
Linkrel, // [RFC9253, Section 6.1]
Jsptr, // draft-ietf-calext-jscalendar-icalendar
Jsid, // draft-ietf-calext-jscalendar-icalendar
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub struct ICalendarDuration {
pub neg: bool,
pub weeks: u32,
pub days: u32,
pub hours: u32,
pub minutes: u32,
pub seconds: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum Uri {
Data(#[cfg_attr(feature = "rkyv", rkyv(with = Unboxed))] Box<Data>),
Location(String),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarRelated {
Start,
End,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarParticipantType {
Active, // [RFC9073, Section 6.2]
Inactive, // [RFC9073, Section 6.2]
Sponsor, // [RFC9073, Section 6.2]
Contact, // [RFC9073, Section 6.2]
BookingContact, // [RFC9073, Section 6.2]
EmergencyContact, // [RFC9073, Section 6.2]
PublicityContact, // [RFC9073, Section 6.2]
PlannerContact, // [RFC9073, Section 6.2]
Performer, // [RFC9073, Section 6.2]
Speaker, // [RFC9073, Section 6.2]
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarParticipationRole {
Chair, // [RFC5545, Section 3.2.16]
ReqParticipant, // [RFC5545, Section 3.2.16]
OptParticipant, // [RFC5545, Section 3.2.16]
NonParticipant, // [RFC5545, Section 3.2.16]
Owner, // draft-ietf-calext-icalendar-jscalendar-extensions
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarStatus {
Tentative, // [RFC5545, Section 3.8.1]
Confirmed, // [RFC5545, Section 3.8.1]
Cancelled, // [RFC5545, Section 3.8.1]
NeedsAction, // [RFC5545, Section 3.8.1]
Completed, // [RFC5545, Section 3.8.1]
InProcess, // [RFC5545, Section 3.8.1]
Draft, // [RFC5545, Section 3.8.1]
Final, // [RFC5545, Section 3.8.1]
Failed, // draft-ietf-calext-ical-tasks
Pending, // draft-ietf-calext-ical-tasks
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarParticipationStatus {
NeedsAction, // [RFC5545, Section 3.2.12]
Accepted, // [RFC5545, Section 3.2.12]
Declined, // [RFC5545, Section 3.2.12]
Tentative, // [RFC5545, Section 3.2.12]
Delegated, // [RFC5545, Section 3.2.12]
Completed, // [RFC5545, Section 3.2.12]
InProcess, // [RFC5545, Section 3.2.12]
Failed, // draft-ietf-calext-ical-tasks
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarProperty {
Begin,
End,
Other(String),
Calscale, // [RFC5545, Section 3.7.1]
Method, // [RFC5545, Section 3.7.2]
Prodid, // [RFC5545, Section 3.7.3]
Version, // [RFC5545, Section 3.7.4]
Attach, // [RFC5545, Section 3.8.1.1]
Categories, // [RFC5545, Section 3.8.1.2] [RFC7986, Section 5.6]
Class, // [RFC5545, Section 3.8.1.3]
Comment, // [RFC5545, Section 3.8.1.4]
Description, // [RFC5545, Section 3.8.1.5] [RFC7986, Section 5.2]
Geo, // [RFC5545, Section 3.8.1.6]
Location, // [RFC5545, Section 3.8.1.7]
PercentComplete, // [RFC5545, Section 3.8.1.8]
Priority, // [RFC5545, Section 3.8.1.9]
Resources, // [RFC5545, Section 3.8.1.10]
Status, // [RFC5545, Section 3.8.1.11]
Summary, // [RFC5545, Section 3.8.1.12]
Completed, // [RFC5545, Section 3.8.2.1]
Dtend, // [RFC5545, Section 3.8.2.2]
Due, // [RFC5545, Section 3.8.2.3]
Dtstart, // [RFC5545, Section 3.8.2.4]
Duration, // [RFC5545, Section 3.8.2.5]
Freebusy, // [RFC5545, Section 3.8.2.6]
Transp, // [RFC5545, Section 3.8.2.7]
Tzid, // [RFC5545, Section 3.8.3.1]
Tzname, // [RFC5545, Section 3.8.3.2]
Tzoffsetfrom, // [RFC5545, Section 3.8.3.3]
Tzoffsetto, // [RFC5545, Section 3.8.3.4]
Tzurl, // [RFC5545, Section 3.8.3.5]
Attendee, // [RFC5545, Section 3.8.4.1]
Contact, // [RFC5545, Section 3.8.4.2]
Organizer, // [RFC5545, Section 3.8.4.3]
RecurrenceId, // [RFC5545, Section 3.8.4.4]
RelatedTo, // [RFC5545, Section 3.8.4.5] [RFC9253, Section 9.1]
Url, // [RFC5545, Section 3.8.4.6] [RFC7986, Section 5.5]
Uid, // [RFC5545, Section 3.8.4.7] [RFC7986, Section 5.3]
Exdate, // [RFC5545, Section 3.8.5.1]
Exrule, // [RFC2445, Section 4.8.5.2]
Rdate, // [RFC5545, Section 3.8.5.2]
Rrule, // [RFC5545, Section 3.8.5.3]
Action, // [RFC5545, Section 3.8.6.1]
Repeat, // [RFC5545, Section 3.8.6.2]
Trigger, // [RFC5545, Section 3.8.6.3]
Created, // [RFC5545, Section 3.8.7.1]
Dtstamp, // [RFC5545, Section 3.8.7.2]
LastModified, // [RFC5545, Section 3.8.7.3] [RFC7986, Section 5.4]
Sequence, // [RFC5545, Section 3.8.7.4]
RequestStatus, // [RFC5545, Section 3.8.8.3]
Xml, // [RFC6321, Section 4.2]
Tzuntil, // [RFC7808, Section 7.1]
TzidAliasOf, // [RFC7808, Section 7.2]
Busytype, // [RFC7953, Section 3.2]
Name, // [RFC7986, Section 5.1]
RefreshInterval, // [RFC7986, Section 5.7]
Source, // [RFC7986, Section 5.8]
Color, // [RFC7986, Section 5.9]
Image, // [RFC7986, Section 5.10]
Conference, // [RFC7986, Section 5.11]
CalendarAddress, // [RFC9073, Section 6.4]
LocationType, // [RFC9073, Section 6.1]
ParticipantType, // [RFC9073, Section 6.2]
ResourceType, // [RFC9073, Section 6.3]
StructuredData, // [RFC9073, Section 6.6]
StyledDescription, // [RFC9073, Section 6.5]
Acknowledged, // [RFC9074, Section 6.1]
Proximity, // [RFC9074, Section 8.1]
Concept, // [RFC9253, Section 8.1]
Link, // [RFC9253, Section 8.2]
Refid, // [RFC9253, Section 8.3]
Coordinates, // draft-ietf-calext-icalendar-jscalendar-extensions
ShowWithoutTime, // draft-ietf-calext-icalendar-jscalendar-extensions
Jsid, // draft-ietf-calext-jscalendar-icalendar
Jsprop, // draft-ietf-calext-jscalendar-icalendar
EstimatedDuration, // draft-ietf-calext-ical-tasks
Reason, // draft-ietf-calext-ical-tasks
Substate, // draft-ietf-calext-ical-tasks
TaskMode, // draft-ietf-calext-ical-tasks
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarProximityValue {
Arrive, // [RFC9074, Section 8.1]
Depart, // [RFC9074, Section 8.1]
Connect, // [RFC9074, Section 8.1]
Disconnect, // [RFC9074, Section 8.1]
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarRelationshipType {
Child, // [RFC5545, Section 3.2.15]
Parent, // [RFC5545, Section 3.2.15]
Sibling, // [RFC5545, Section 3.2.15]
Snooze, // [RFC9074, Section 7.1]
Concept, // [RFC9253, Section 5]
DependsOn, // [RFC9253, Section 5]
Finishtofinish, // [RFC9253, Section 4]
Finishtostart, // [RFC9253, Section 4]
First, // [RFC9253, Section 5]
Next, // [RFC9253, Section 5]
Refid, // [RFC9253, Section 5]
Starttofinish, // [RFC9253, Section 4]
Starttostart, // [RFC9253, Section 4]
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarResourceType {
Projector, // [RFC9073, Section 6.3]
Room, // [RFC9073, Section 6.3]
RemoteConferenceAudio, // [RFC9073, Section 6.3]
RemoteConferenceVideo, // [RFC9073, Section 6.3]
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarScheduleAgentValue {
Server, // [RFC6638, Section 7.1]
Client, // [RFC6638, Section 7.1]
None, // [RFC6638, Section 7.1]
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarScheduleForceSendValue {
Request, // [RFC6638, Section 7.2]
Reply, // [RFC6638, Section 7.2]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarValueType {
Binary, // [RFC5545, Section 3.3.1]
Boolean, // [RFC5545, Section 3.3.2]
CalAddress, // [RFC5545, Section 3.3.3]
Date, // [RFC5545, Section 3.3.4]
DateTime, // [RFC5545, Section 3.3.5]
Duration, // [RFC5545, Section 3.3.6]
Float, // [RFC5545, Section 3.3.7]
Integer, // [RFC5545, Section 3.3.8]
Period, // [RFC5545, Section 3.3.9]
Recur, // [RFC5545, Section 3.3.10]
Text, // [RFC5545, Section 3.3.11]
Time, // [RFC5545, Section 3.3.12]
Unknown, // [RFC7265, Section 5]
Uri, // [RFC5545, Section 3.3.13]
UtcOffset, // [RFC5545, Section 3.3.14]
XmlReference, // [RFC9253, Section 7]
Uid, // [RFC9253, Section 7]
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum ICalendarTransparency {
Opaque,
Transparent,
}
pub(crate) const VALUE_SIZE: usize = 56;
pub(crate) const PARAMETER_VALUE_SIZE: usize = 48;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ValueSeparator {
None,
Comma,
Semicolon,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ValueType {
Ical(ICalendarValueType),
CalendarScale,
Method,
Classification,
Status,
Transparency,
Action,
BusyType,
ParticipantType,
ResourceType,
Proximity,
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{
ArchivedICalendar, ArchivedICalendarComponent, ArchivedICalendarComponentType,
ArchivedICalendarEntry, ArchivedICalendarProperty, ArchivedICalendarValue,
};
use crate::{common::timezone::Tz, icalendar::ICalendarParameterName};
use std::str::FromStr;
impl ArchivedICalendar {
pub fn timezones(&self) -> impl Iterator<Item = &ArchivedICalendarComponent> {
self.components.iter().filter(|comp| {
matches!(
comp.component_type,
ArchivedICalendarComponentType::VTimezone
)
})
}
pub fn is_timezone(&self) -> bool {
self.timezones().count() == 1
}
}
impl ArchivedICalendarComponent {
pub fn timezone(&self) -> Option<(&str, Tz)> {
let mut tz_name = None;
let mut tz_id = None;
let mut tz_lic = None;
let mut tz_cdo_id = None;
for entry in self.entries.iter() {
match (&entry.name, entry.values.first()) {
(ArchivedICalendarProperty::Tzid, Some(ArchivedICalendarValue::Text(id))) => {
tz_id = Tz::from_str(id.as_str()).ok();
tz_name = Some(id.as_str());
}
(
ArchivedICalendarProperty::Other(value),
Some(ArchivedICalendarValue::Text(id)),
) => {
hashify::fnc_map!(value.as_bytes(),
"X-LIC-LOCATION" => {
tz_lic = Tz::from_str(id.strip_prefix("SystemV/").unwrap_or(id.as_str())).ok();
},
"X-MICROSOFT-CDO-TZID" => {
tz_cdo_id = Tz::from_ms_cdo_zone_id(id);
},
_ => {}
);
}
_ => (),
}
}
tz_name.zip(tz_id.or(tz_lic).or(tz_cdo_id))
}
}
impl ArchivedICalendarEntry {
pub fn tz_id(&self) -> Option<&str> {
self.parameters(&ICalendarParameterName::Tzid)
.find_map(|param| param.as_text())
}
}
+871
View File
@@ -0,0 +1,871 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::common::ArchivedLinkRelation;
use super::*;
impl ArchivedICalendarProperty {
// Returns the default value type and whether the property is multi-valued.
pub(crate) fn default_types(&self) -> (ArchivedValueType, ValueSeparator) {
match self {
ArchivedICalendarProperty::Calscale => {
(ArchivedValueType::CalendarScale, ValueSeparator::None)
}
ArchivedICalendarProperty::Method => (ArchivedValueType::Method, ValueSeparator::None),
ArchivedICalendarProperty::Prodid => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Version => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Attach => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Uri),
ValueSeparator::None,
),
ArchivedICalendarProperty::Categories => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::Comma,
),
ArchivedICalendarProperty::Class => {
(ArchivedValueType::Classification, ValueSeparator::None)
}
ArchivedICalendarProperty::Comment => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Description => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Geo => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Float),
ValueSeparator::Semicolon,
),
ArchivedICalendarProperty::Location => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::PercentComplete => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Integer),
ValueSeparator::None,
),
ArchivedICalendarProperty::Priority => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Integer),
ValueSeparator::None,
),
ArchivedICalendarProperty::Resources => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::Comma,
),
ArchivedICalendarProperty::Status => (ArchivedValueType::Status, ValueSeparator::None),
ArchivedICalendarProperty::Summary => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Completed => (
ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime),
ValueSeparator::None,
),
ArchivedICalendarProperty::Dtend => (
ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime),
ValueSeparator::None,
),
ArchivedICalendarProperty::Due => (
ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime),
ValueSeparator::None,
),
ArchivedICalendarProperty::Dtstart => (
ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime),
ValueSeparator::None,
),
ArchivedICalendarProperty::Duration => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Duration),
ValueSeparator::None,
),
ArchivedICalendarProperty::Freebusy => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Period),
ValueSeparator::Comma,
),
ArchivedICalendarProperty::Transp => {
(ArchivedValueType::Transparency, ValueSeparator::None)
}
ArchivedICalendarProperty::Tzid => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Tzname => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Tzoffsetfrom => (
ArchivedValueType::Ical(ArchivedICalendarValueType::UtcOffset),
ValueSeparator::None,
),
ArchivedICalendarProperty::Tzoffsetto => (
ArchivedValueType::Ical(ArchivedICalendarValueType::UtcOffset),
ValueSeparator::None,
),
ArchivedICalendarProperty::Tzurl => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Uri),
ValueSeparator::None,
),
ArchivedICalendarProperty::Attendee => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Uri),
ValueSeparator::None,
),
ArchivedICalendarProperty::Contact => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Organizer => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Uri),
ValueSeparator::None,
),
ArchivedICalendarProperty::RecurrenceId => (
ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime),
ValueSeparator::None,
),
ArchivedICalendarProperty::RelatedTo => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Url => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Uri),
ValueSeparator::None,
),
ArchivedICalendarProperty::Uid => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Exdate => (
ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime),
ValueSeparator::Comma,
),
ArchivedICalendarProperty::Exrule => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Recur),
ValueSeparator::None,
),
ArchivedICalendarProperty::Rdate => (
ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime),
ValueSeparator::Comma,
),
ArchivedICalendarProperty::Rrule => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Recur),
ValueSeparator::None,
),
ArchivedICalendarProperty::Action => (ArchivedValueType::Action, ValueSeparator::None),
ArchivedICalendarProperty::Repeat => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Integer),
ValueSeparator::None,
),
ArchivedICalendarProperty::Trigger => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Duration),
ValueSeparator::None,
),
ArchivedICalendarProperty::Created => (
ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime),
ValueSeparator::None,
),
ArchivedICalendarProperty::Dtstamp => (
ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime),
ValueSeparator::None,
),
ArchivedICalendarProperty::LastModified => (
ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime),
ValueSeparator::None,
),
ArchivedICalendarProperty::Sequence => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Integer),
ValueSeparator::None,
),
ArchivedICalendarProperty::RequestStatus => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::Semicolon,
),
ArchivedICalendarProperty::Xml => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Tzuntil => (
ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime),
ValueSeparator::None,
),
ArchivedICalendarProperty::TzidAliasOf => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Busytype => {
(ArchivedValueType::BusyType, ValueSeparator::None)
}
ArchivedICalendarProperty::Name => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::RefreshInterval => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Duration),
ValueSeparator::None,
),
ArchivedICalendarProperty::Source => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Uri),
ValueSeparator::None,
),
ArchivedICalendarProperty::Color => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Image => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Uri),
ValueSeparator::None,
),
ArchivedICalendarProperty::Conference => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Uri),
ValueSeparator::None,
),
ArchivedICalendarProperty::CalendarAddress => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Uri),
ValueSeparator::None,
),
ArchivedICalendarProperty::LocationType => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::Comma,
),
ArchivedICalendarProperty::ParticipantType => {
(ArchivedValueType::ParticipantType, ValueSeparator::None)
}
ArchivedICalendarProperty::ResourceType => {
(ArchivedValueType::ResourceType, ValueSeparator::None)
}
ArchivedICalendarProperty::StructuredData => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::StyledDescription => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Acknowledged => (
ArchivedValueType::Ical(ArchivedICalendarValueType::DateTime),
ValueSeparator::None,
),
ArchivedICalendarProperty::Proximity => {
(ArchivedValueType::Proximity, ValueSeparator::None)
}
ArchivedICalendarProperty::Concept => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Uri),
ValueSeparator::None,
),
ArchivedICalendarProperty::Link => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Uri),
ValueSeparator::None,
),
ArchivedICalendarProperty::Refid => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Begin => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::End => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::Other(_) => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::Semicolon,
),
ArchivedICalendarProperty::Coordinates => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Uri),
ValueSeparator::None,
),
ArchivedICalendarProperty::ShowWithoutTime => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Boolean),
ValueSeparator::None,
),
ArchivedICalendarProperty::Jsid | ArchivedICalendarProperty::Jsprop => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::EstimatedDuration => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Duration),
ValueSeparator::None,
),
ArchivedICalendarProperty::Reason => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Uri),
ValueSeparator::None,
),
ArchivedICalendarProperty::Substate => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
ArchivedICalendarProperty::TaskMode => (
ArchivedValueType::Ical(ArchivedICalendarValueType::Text),
ValueSeparator::None,
),
}
}
}
impl IanaString for ArchivedICalendarFrequency {
fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarFrequency::Secondly => "SECONDLY",
ArchivedICalendarFrequency::Minutely => "MINUTELY",
ArchivedICalendarFrequency::Hourly => "HOURLY",
ArchivedICalendarFrequency::Daily => "DAILY",
ArchivedICalendarFrequency::Weekly => "WEEKLY",
ArchivedICalendarFrequency::Monthly => "MONTHLY",
ArchivedICalendarFrequency::Yearly => "YEARLY",
}
}
}
impl ArchivedICalendarSkip {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarSkip::Omit => "OMIT",
ArchivedICalendarSkip::Backward => "BACKWARD",
ArchivedICalendarSkip::Forward => "FORWARD",
}
}
}
impl ArchivedICalendarWeekday {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarWeekday::Sunday => "SU",
ArchivedICalendarWeekday::Monday => "MO",
ArchivedICalendarWeekday::Tuesday => "TU",
ArchivedICalendarWeekday::Wednesday => "WE",
ArchivedICalendarWeekday::Thursday => "TH",
ArchivedICalendarWeekday::Friday => "FR",
ArchivedICalendarWeekday::Saturday => "SA",
}
}
}
impl ArchivedICalendarAction {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarAction::Audio => "AUDIO",
ArchivedICalendarAction::Display => "DISPLAY",
ArchivedICalendarAction::Email => "EMAIL",
ArchivedICalendarAction::Procedure => "PROCEDURE",
}
}
}
impl ArchivedICalendarUserTypes {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarUserTypes::Individual => "INDIVIDUAL",
ArchivedICalendarUserTypes::Group => "GROUP",
ArchivedICalendarUserTypes::Resource => "RESOURCE",
ArchivedICalendarUserTypes::Room => "ROOM",
ArchivedICalendarUserTypes::Unknown => "UNKNOWN",
}
}
}
impl ArchivedICalendarClassification {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarClassification::Public => "PUBLIC",
ArchivedICalendarClassification::Private => "PRIVATE",
ArchivedICalendarClassification::Confidential => "CONFIDENTIAL",
}
}
}
impl ArchivedICalendarComponentType {
pub fn as_str(&self) -> &str {
match self {
ArchivedICalendarComponentType::VCalendar => "VCALENDAR",
ArchivedICalendarComponentType::VEvent => "VEVENT",
ArchivedICalendarComponentType::VTodo => "VTODO",
ArchivedICalendarComponentType::VJournal => "VJOURNAL",
ArchivedICalendarComponentType::VFreebusy => "VFREEBUSY",
ArchivedICalendarComponentType::VTimezone => "VTIMEZONE",
ArchivedICalendarComponentType::VAlarm => "VALARM",
ArchivedICalendarComponentType::Standard => "STANDARD",
ArchivedICalendarComponentType::Daylight => "DAYLIGHT",
ArchivedICalendarComponentType::VAvailability => "VAVAILABILITY",
ArchivedICalendarComponentType::Available => "AVAILABLE",
ArchivedICalendarComponentType::Participant => "PARTICIPANT",
ArchivedICalendarComponentType::VLocation => "VLOCATION",
ArchivedICalendarComponentType::VResource => "VRESOURCE",
ArchivedICalendarComponentType::VStatus => "VSTATUS",
ArchivedICalendarComponentType::Other(name) => name.as_str(),
}
}
}
impl ArchivedICalendarDisplayType {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarDisplayType::Badge => "BADGE",
ArchivedICalendarDisplayType::Graphic => "GRAPHIC",
ArchivedICalendarDisplayType::Fullsize => "FULLSIZE",
ArchivedICalendarDisplayType::Thumbnail => "THUMBNAIL",
}
}
}
impl ArchivedICalendarFeatureType {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarFeatureType::Audio => "AUDIO",
ArchivedICalendarFeatureType::Chat => "CHAT",
ArchivedICalendarFeatureType::Feed => "FEED",
ArchivedICalendarFeatureType::Moderator => "MODERATOR",
ArchivedICalendarFeatureType::Phone => "PHONE",
ArchivedICalendarFeatureType::Screen => "SCREEN",
ArchivedICalendarFeatureType::Video => "VIDEO",
}
}
}
impl ArchivedICalendarFreeBusyType {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarFreeBusyType::Free => "FREE",
ArchivedICalendarFreeBusyType::Busy => "BUSY",
ArchivedICalendarFreeBusyType::BusyUnavailable => "BUSY-UNAVAILABLE",
ArchivedICalendarFreeBusyType::BusyTentative => "BUSY-TENTATIVE",
}
}
}
impl ArchivedICalendarMethod {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarMethod::Publish => "PUBLISH",
ArchivedICalendarMethod::Request => "REQUEST",
ArchivedICalendarMethod::Reply => "REPLY",
ArchivedICalendarMethod::Add => "ADD",
ArchivedICalendarMethod::Cancel => "CANCEL",
ArchivedICalendarMethod::Refresh => "REFRESH",
ArchivedICalendarMethod::Counter => "COUNTER",
ArchivedICalendarMethod::Declinecounter => "DECLINECOUNTER",
}
}
}
impl ArchivedICalendarParticipantType {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarParticipantType::Active => "ACTIVE",
ArchivedICalendarParticipantType::Inactive => "INACTIVE",
ArchivedICalendarParticipantType::Sponsor => "SPONSOR",
ArchivedICalendarParticipantType::Contact => "CONTACT",
ArchivedICalendarParticipantType::BookingContact => "BOOKING-CONTACT",
ArchivedICalendarParticipantType::EmergencyContact => "EMERGENCY-CONTACT",
ArchivedICalendarParticipantType::PublicityContact => "PUBLICITY-CONTACT",
ArchivedICalendarParticipantType::PlannerContact => "PLANNER-CONTACT",
ArchivedICalendarParticipantType::Performer => "PERFORMER",
ArchivedICalendarParticipantType::Speaker => "SPEAKER",
}
}
}
impl ArchivedICalendarParticipationRole {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarParticipationRole::Chair => "CHAIR",
ArchivedICalendarParticipationRole::ReqParticipant => "REQ-PARTICIPANT",
ArchivedICalendarParticipationRole::OptParticipant => "OPT-PARTICIPANT",
ArchivedICalendarParticipationRole::NonParticipant => "NON-PARTICIPANT",
ArchivedICalendarParticipationRole::Owner => "OWNER",
}
}
}
impl ArchivedICalendarParticipationStatus {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarParticipationStatus::NeedsAction => "NEEDS-ACTION",
ArchivedICalendarParticipationStatus::Accepted => "ACCEPTED",
ArchivedICalendarParticipationStatus::Declined => "DECLINED",
ArchivedICalendarParticipationStatus::Tentative => "TENTATIVE",
ArchivedICalendarParticipationStatus::Delegated => "DELEGATED",
ArchivedICalendarParticipationStatus::Completed => "COMPLETED",
ArchivedICalendarParticipationStatus::InProcess => "IN-PROCESS",
ArchivedICalendarParticipationStatus::Failed => "FAILED",
}
}
}
impl ArchivedICalendarStatus {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarStatus::Tentative => "TENTATIVE",
ArchivedICalendarStatus::Confirmed => "CONFIRMED",
ArchivedICalendarStatus::Cancelled => "CANCELLED",
ArchivedICalendarStatus::NeedsAction => "NEEDS-ACTION",
ArchivedICalendarStatus::Completed => "COMPLETED",
ArchivedICalendarStatus::InProcess => "IN-PROCESS",
ArchivedICalendarStatus::Draft => "DRAFT",
ArchivedICalendarStatus::Final => "FINAL",
ArchivedICalendarStatus::Failed => "FAILED",
ArchivedICalendarStatus::Pending => "PENDING",
}
}
}
impl ArchivedICalendarProperty {
pub fn as_str(&self) -> &str {
match self {
ArchivedICalendarProperty::Calscale => "CALSCALE",
ArchivedICalendarProperty::Method => "METHOD",
ArchivedICalendarProperty::Prodid => "PRODID",
ArchivedICalendarProperty::Version => "VERSION",
ArchivedICalendarProperty::Attach => "ATTACH",
ArchivedICalendarProperty::Categories => "CATEGORIES",
ArchivedICalendarProperty::Class => "CLASS",
ArchivedICalendarProperty::Comment => "COMMENT",
ArchivedICalendarProperty::Description => "DESCRIPTION",
ArchivedICalendarProperty::Geo => "GEO",
ArchivedICalendarProperty::Location => "LOCATION",
ArchivedICalendarProperty::PercentComplete => "PERCENT-COMPLETE",
ArchivedICalendarProperty::Priority => "PRIORITY",
ArchivedICalendarProperty::Resources => "RESOURCES",
ArchivedICalendarProperty::Status => "STATUS",
ArchivedICalendarProperty::Summary => "SUMMARY",
ArchivedICalendarProperty::Completed => "COMPLETED",
ArchivedICalendarProperty::Dtend => "DTEND",
ArchivedICalendarProperty::Due => "DUE",
ArchivedICalendarProperty::Dtstart => "DTSTART",
ArchivedICalendarProperty::Duration => "DURATION",
ArchivedICalendarProperty::Freebusy => "FREEBUSY",
ArchivedICalendarProperty::Transp => "TRANSP",
ArchivedICalendarProperty::Tzid => "TZID",
ArchivedICalendarProperty::Tzname => "TZNAME",
ArchivedICalendarProperty::Tzoffsetfrom => "TZOFFSETFROM",
ArchivedICalendarProperty::Tzoffsetto => "TZOFFSETTO",
ArchivedICalendarProperty::Tzurl => "TZURL",
ArchivedICalendarProperty::Attendee => "ATTENDEE",
ArchivedICalendarProperty::Contact => "CONTACT",
ArchivedICalendarProperty::Organizer => "ORGANIZER",
ArchivedICalendarProperty::RecurrenceId => "RECURRENCE-ID",
ArchivedICalendarProperty::RelatedTo => "RELATED-TO",
ArchivedICalendarProperty::Url => "URL",
ArchivedICalendarProperty::Uid => "UID",
ArchivedICalendarProperty::Exdate => "EXDATE",
ArchivedICalendarProperty::Exrule => "EXRULE",
ArchivedICalendarProperty::Rdate => "RDATE",
ArchivedICalendarProperty::Rrule => "RRULE",
ArchivedICalendarProperty::Action => "ACTION",
ArchivedICalendarProperty::Repeat => "REPEAT",
ArchivedICalendarProperty::Trigger => "TRIGGER",
ArchivedICalendarProperty::Created => "CREATED",
ArchivedICalendarProperty::Dtstamp => "DTSTAMP",
ArchivedICalendarProperty::LastModified => "LAST-MODIFIED",
ArchivedICalendarProperty::Sequence => "SEQUENCE",
ArchivedICalendarProperty::RequestStatus => "REQUEST-STATUS",
ArchivedICalendarProperty::Xml => "XML",
ArchivedICalendarProperty::Tzuntil => "TZUNTIL",
ArchivedICalendarProperty::TzidAliasOf => "TZID-ALIAS-OF",
ArchivedICalendarProperty::Busytype => "BUSYTYPE",
ArchivedICalendarProperty::Name => "NAME",
ArchivedICalendarProperty::RefreshInterval => "REFRESH-INTERVAL",
ArchivedICalendarProperty::Source => "SOURCE",
ArchivedICalendarProperty::Color => "COLOR",
ArchivedICalendarProperty::Image => "IMAGE",
ArchivedICalendarProperty::Conference => "CONFERENCE",
ArchivedICalendarProperty::CalendarAddress => "CALENDAR-ADDRESS",
ArchivedICalendarProperty::LocationType => "LOCATION-TYPE",
ArchivedICalendarProperty::ParticipantType => "PARTICIPANT-TYPE",
ArchivedICalendarProperty::ResourceType => "RESOURCE-TYPE",
ArchivedICalendarProperty::StructuredData => "STRUCTURED-DATA",
ArchivedICalendarProperty::StyledDescription => "STYLED-DESCRIPTION",
ArchivedICalendarProperty::Acknowledged => "ACKNOWLEDGED",
ArchivedICalendarProperty::Proximity => "PROXIMITY",
ArchivedICalendarProperty::Concept => "CONCEPT",
ArchivedICalendarProperty::Link => "LINK",
ArchivedICalendarProperty::Refid => "REFID",
ArchivedICalendarProperty::Begin => "BEGIN",
ArchivedICalendarProperty::End => "END",
ArchivedICalendarProperty::Coordinates => "COORDINATES",
ArchivedICalendarProperty::ShowWithoutTime => "SHOW-WITHOUT-TIME",
ArchivedICalendarProperty::Jsid => "JSID",
ArchivedICalendarProperty::Jsprop => "JSPROP",
ArchivedICalendarProperty::EstimatedDuration => "ESTIMATED-DURATION",
ArchivedICalendarProperty::Reason => "REASON",
ArchivedICalendarProperty::Substate => "SUBSTATE",
ArchivedICalendarProperty::TaskMode => "TASK-MODE",
ArchivedICalendarProperty::Other(value) => value.as_str(),
}
}
}
impl ArchivedICalendarProximityValue {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarProximityValue::Arrive => "ARRIVE",
ArchivedICalendarProximityValue::Depart => "DEPART",
ArchivedICalendarProximityValue::Connect => "CONNECT",
ArchivedICalendarProximityValue::Disconnect => "DISCONNECT",
}
}
}
impl ArchivedICalendarRelationshipType {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarRelationshipType::Child => "CHILD",
ArchivedICalendarRelationshipType::Parent => "PARENT",
ArchivedICalendarRelationshipType::Sibling => "SIBLING",
ArchivedICalendarRelationshipType::Snooze => "SNOOZE",
ArchivedICalendarRelationshipType::Concept => "CONCEPT",
ArchivedICalendarRelationshipType::DependsOn => "DEPENDS-ON",
ArchivedICalendarRelationshipType::Finishtofinish => "FINISHTOFINISH",
ArchivedICalendarRelationshipType::Finishtostart => "FINISHTOSTART",
ArchivedICalendarRelationshipType::First => "FIRST",
ArchivedICalendarRelationshipType::Next => "NEXT",
ArchivedICalendarRelationshipType::Refid => "REFID",
ArchivedICalendarRelationshipType::Starttofinish => "STARTTOFINISH",
ArchivedICalendarRelationshipType::Starttostart => "STARTTOSTART",
}
}
}
impl ArchivedICalendarResourceType {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarResourceType::Projector => "PROJECTOR",
ArchivedICalendarResourceType::Room => "ROOM",
ArchivedICalendarResourceType::RemoteConferenceAudio => "REMOTE-CONFERENCE-AUDIO",
ArchivedICalendarResourceType::RemoteConferenceVideo => "REMOTE-CONFERENCE-VIDEO",
}
}
}
impl ArchivedICalendarScheduleAgentValue {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarScheduleAgentValue::Server => "SERVER",
ArchivedICalendarScheduleAgentValue::Client => "CLIENT",
ArchivedICalendarScheduleAgentValue::None => "NONE",
}
}
}
impl ArchivedICalendarScheduleForceSendValue {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarScheduleForceSendValue::Request => "REQUEST",
ArchivedICalendarScheduleForceSendValue::Reply => "REPLY",
}
}
}
impl ArchivedICalendarValueType {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarValueType::Binary => "BINARY",
ArchivedICalendarValueType::Boolean => "BOOLEAN",
ArchivedICalendarValueType::CalAddress => "CAL-ADDRESS",
ArchivedICalendarValueType::Date => "DATE",
ArchivedICalendarValueType::DateTime => "DATE-TIME",
ArchivedICalendarValueType::Duration => "DURATION",
ArchivedICalendarValueType::Float => "FLOAT",
ArchivedICalendarValueType::Integer => "INTEGER",
ArchivedICalendarValueType::Period => "PERIOD",
ArchivedICalendarValueType::Recur => "RECUR",
ArchivedICalendarValueType::Text => "TEXT",
ArchivedICalendarValueType::Time => "TIME",
ArchivedICalendarValueType::Unknown => "UNKNOWN",
ArchivedICalendarValueType::Uri => "URI",
ArchivedICalendarValueType::UtcOffset => "UTC-OFFSET",
ArchivedICalendarValueType::XmlReference => "XML-REFERENCE",
ArchivedICalendarValueType::Uid => "UID",
}
}
}
impl ArchivedICalendarTransparency {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarTransparency::Opaque => "OPAQUE",
ArchivedICalendarTransparency::Transparent => "TRANSPARENT",
}
}
}
impl ArchivedLinkRelation {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedLinkRelation::About => "about",
ArchivedLinkRelation::Acl => "acl",
ArchivedLinkRelation::Alternate => "alternate",
ArchivedLinkRelation::Amphtml => "amphtml",
ArchivedLinkRelation::ApiCatalog => "api-catalog",
ArchivedLinkRelation::Appendix => "appendix",
ArchivedLinkRelation::AppleTouchIcon => "apple-touch-icon",
ArchivedLinkRelation::AppleTouchStartupImage => "apple-touch-startup-image",
ArchivedLinkRelation::Archives => "archives",
ArchivedLinkRelation::Author => "author",
ArchivedLinkRelation::BlockedBy => "blocked-by",
ArchivedLinkRelation::Bookmark => "bookmark",
ArchivedLinkRelation::C2paManifest => "c2pa-manifest",
ArchivedLinkRelation::Canonical => "canonical",
ArchivedLinkRelation::Chapter => "chapter",
ArchivedLinkRelation::CiteAs => "cite-as",
ArchivedLinkRelation::Collection => "collection",
ArchivedLinkRelation::CompressionDictionary => "compression-dictionary",
ArchivedLinkRelation::Contents => "contents",
ArchivedLinkRelation::Convertedfrom => "convertedfrom",
ArchivedLinkRelation::Copyright => "copyright",
ArchivedLinkRelation::CreateForm => "create-form",
ArchivedLinkRelation::Current => "current",
ArchivedLinkRelation::Deprecation => "deprecation",
ArchivedLinkRelation::Describedby => "describedby",
ArchivedLinkRelation::Describes => "describes",
ArchivedLinkRelation::Disclosure => "disclosure",
ArchivedLinkRelation::DnsPrefetch => "dns-prefetch",
ArchivedLinkRelation::Duplicate => "duplicate",
ArchivedLinkRelation::Edit => "edit",
ArchivedLinkRelation::EditForm => "edit-form",
ArchivedLinkRelation::EditMedia => "edit-media",
ArchivedLinkRelation::Enclosure => "enclosure",
ArchivedLinkRelation::External => "external",
ArchivedLinkRelation::First => "first",
ArchivedLinkRelation::Geofeed => "geofeed",
ArchivedLinkRelation::Glossary => "glossary",
ArchivedLinkRelation::Help => "help",
ArchivedLinkRelation::Hosts => "hosts",
ArchivedLinkRelation::Hub => "hub",
ArchivedLinkRelation::IceServer => "ice-server",
ArchivedLinkRelation::Icon => "icon",
ArchivedLinkRelation::Index => "index",
ArchivedLinkRelation::Intervalafter => "intervalafter",
ArchivedLinkRelation::Intervalbefore => "intervalbefore",
ArchivedLinkRelation::Intervalcontains => "intervalcontains",
ArchivedLinkRelation::Intervaldisjoint => "intervaldisjoint",
ArchivedLinkRelation::Intervalduring => "intervalduring",
ArchivedLinkRelation::Intervalequals => "intervalequals",
ArchivedLinkRelation::Intervalfinishedby => "intervalfinishedby",
ArchivedLinkRelation::Intervalfinishes => "intervalfinishes",
ArchivedLinkRelation::Intervalin => "intervalin",
ArchivedLinkRelation::Intervalmeets => "intervalmeets",
ArchivedLinkRelation::Intervalmetby => "intervalmetby",
ArchivedLinkRelation::Intervaloverlappedby => "intervaloverlappedby",
ArchivedLinkRelation::Intervaloverlaps => "intervaloverlaps",
ArchivedLinkRelation::Intervalstartedby => "intervalstartedby",
ArchivedLinkRelation::Intervalstarts => "intervalstarts",
ArchivedLinkRelation::Item => "item",
ArchivedLinkRelation::Last => "last",
ArchivedLinkRelation::LatestVersion => "latest-version",
ArchivedLinkRelation::License => "license",
ArchivedLinkRelation::Linkset => "linkset",
ArchivedLinkRelation::Lrdd => "lrdd",
ArchivedLinkRelation::Manifest => "manifest",
ArchivedLinkRelation::MaskIcon => "mask-icon",
ArchivedLinkRelation::Me => "me",
ArchivedLinkRelation::MediaFeed => "media-feed",
ArchivedLinkRelation::Memento => "memento",
ArchivedLinkRelation::Micropub => "micropub",
ArchivedLinkRelation::Modulepreload => "modulepreload",
ArchivedLinkRelation::Monitor => "monitor",
ArchivedLinkRelation::MonitorGroup => "monitor-group",
ArchivedLinkRelation::Next => "next",
ArchivedLinkRelation::NextArchive => "next-archive",
ArchivedLinkRelation::Nofollow => "nofollow",
ArchivedLinkRelation::Noopener => "noopener",
ArchivedLinkRelation::Noreferrer => "noreferrer",
ArchivedLinkRelation::Opener => "opener",
ArchivedLinkRelation::Openid2LocalId => "openid2.local_id",
ArchivedLinkRelation::Openid2Provider => "openid2.provider",
ArchivedLinkRelation::Original => "original",
ArchivedLinkRelation::P3pv1 => "p3pv1",
ArchivedLinkRelation::Payment => "payment",
ArchivedLinkRelation::Pingback => "pingback",
ArchivedLinkRelation::Preconnect => "preconnect",
ArchivedLinkRelation::PredecessorVersion => "predecessor-version",
ArchivedLinkRelation::Prefetch => "prefetch",
ArchivedLinkRelation::Preload => "preload",
ArchivedLinkRelation::Prerender => "prerender",
ArchivedLinkRelation::Prev => "prev",
ArchivedLinkRelation::Preview => "preview",
ArchivedLinkRelation::Previous => "previous",
ArchivedLinkRelation::PrevArchive => "prev-archive",
ArchivedLinkRelation::PrivacyPolicy => "privacy-policy",
ArchivedLinkRelation::Profile => "profile",
ArchivedLinkRelation::Publication => "publication",
ArchivedLinkRelation::RdapActive => "rdap-active",
ArchivedLinkRelation::RdapBottom => "rdap-bottom",
ArchivedLinkRelation::RdapDown => "rdap-down",
ArchivedLinkRelation::RdapTop => "rdap-top",
ArchivedLinkRelation::RdapUp => "rdap-up",
ArchivedLinkRelation::Related => "related",
ArchivedLinkRelation::Restconf => "restconf",
ArchivedLinkRelation::Replies => "replies",
ArchivedLinkRelation::Ruleinput => "ruleinput",
ArchivedLinkRelation::Search => "search",
ArchivedLinkRelation::Section => "section",
ArchivedLinkRelation::Self_ => "self",
ArchivedLinkRelation::Service => "service",
ArchivedLinkRelation::ServiceDesc => "service-desc",
ArchivedLinkRelation::ServiceDoc => "service-doc",
ArchivedLinkRelation::ServiceMeta => "service-meta",
ArchivedLinkRelation::SipTrunkingCapability => "sip-trunking-capability",
ArchivedLinkRelation::Sponsored => "sponsored",
ArchivedLinkRelation::Start => "start",
ArchivedLinkRelation::Status => "status",
ArchivedLinkRelation::Stylesheet => "stylesheet",
ArchivedLinkRelation::Subsection => "subsection",
ArchivedLinkRelation::SuccessorVersion => "successor-version",
ArchivedLinkRelation::Sunset => "sunset",
ArchivedLinkRelation::Tag => "tag",
ArchivedLinkRelation::TermsOfService => "terms-of-service",
ArchivedLinkRelation::Timegate => "timegate",
ArchivedLinkRelation::Timemap => "timemap",
ArchivedLinkRelation::Type => "type",
ArchivedLinkRelation::Ugc => "ugc",
ArchivedLinkRelation::Up => "up",
ArchivedLinkRelation::VersionHistory => "version-history",
ArchivedLinkRelation::Via => "via",
ArchivedLinkRelation::Webmention => "webmention",
ArchivedLinkRelation::WorkingCopy => "working-copy",
ArchivedLinkRelation::WorkingCopyOf => "working-copy-of",
}
}
}
impl ArchivedICalendarRelated {
pub fn as_str(&self) -> &'static str {
match self {
ArchivedICalendarRelated::Start => "START",
ArchivedICalendarRelated::End => "END",
}
}
}
#[derive(Debug)]
pub(crate) enum ArchivedValueType {
Ical(ArchivedICalendarValueType),
CalendarScale,
Method,
Classification,
Status,
Transparency,
Action,
BusyType,
ParticipantType,
ResourceType,
Proximity,
}
impl ArchivedValueType {
pub fn unwrap_ical(self) -> ArchivedICalendarValueType {
match self {
ArchivedValueType::Ical(v) => v,
_ => ArchivedICalendarValueType::Text,
}
}
}
+478
View File
@@ -0,0 +1,478 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::*;
use crate::common::{
ArchivedPartialDateTime,
timezone::{NominalDuration, Tz, ZonedDateTime},
};
impl ArchivedICalendar {
pub fn uids(&self) -> impl Iterator<Item = &str> {
self.components
.iter()
.filter(|component| {
component.component_type.is_calendar_object()
|| component.component_type == ICalendarComponentType::VCalendar
})
.filter_map(|component| component.uid())
}
pub fn size(&self) -> usize {
self.components
.iter()
.map(|component| component.size())
.sum()
}
}
impl ArchivedICalendarComponent {
pub fn uid(&self) -> Option<&str> {
self.property(&ICalendarProperty::Uid)
.and_then(|e| e.values.first())
.and_then(|v| v.as_text())
}
pub fn property(&self, prop: &ICalendarProperty) -> Option<&ArchivedICalendarEntry> {
self.entries.iter().find(|entry| &entry.name == prop)
}
pub fn properties<'x, 'y: 'x>(
&'x self,
prop: &'y ICalendarProperty,
) -> impl Iterator<Item = &'x ArchivedICalendarEntry> + 'x {
self.entries.iter().filter(move |entry| &entry.name == prop)
}
pub fn size(&self) -> usize {
self.entries.iter().map(|entry| entry.size()).sum()
}
pub fn is_recurrent(&self) -> bool {
self.entries.iter().any(|entry| {
matches!(
entry.name,
ArchivedICalendarProperty::Rrule | ArchivedICalendarProperty::Rdate
)
})
}
pub fn is_recurrence_override(&self) -> bool {
self.entries
.iter()
.any(|entry| matches!(entry.name, ArchivedICalendarProperty::RecurrenceId))
}
pub fn is_recurrent_or_override(&self) -> bool {
self.entries.iter().any(|entry| {
matches!(
entry.name,
ArchivedICalendarProperty::Rrule
| ArchivedICalendarProperty::Rdate
| ArchivedICalendarProperty::RecurrenceId
)
})
}
pub fn status(&self) -> Option<&ArchivedICalendarStatus> {
self.entries
.iter()
.find_map(|entry| match (&entry.name, entry.values.first()) {
(
ArchivedICalendarProperty::Status,
Some(ArchivedICalendarValue::Status(status)),
) => Some(status),
_ => None,
})
}
pub fn transparency(&self) -> Option<&ArchivedICalendarTransparency> {
self.entries
.iter()
.find_map(|entry| match (&entry.name, entry.values.first()) {
(
ArchivedICalendarProperty::Transp,
Some(ArchivedICalendarValue::Transparency(trans)),
) => Some(trans),
_ => None,
})
}
}
impl ArchivedICalendarValue {
pub fn size(&self) -> usize {
match self {
ArchivedICalendarValue::Binary(value) => value.len(),
ArchivedICalendarValue::Text(value) => value.len(),
ArchivedICalendarValue::PartialDateTime(_) => std::mem::size_of::<PartialDateTime>(),
ArchivedICalendarValue::RecurrenceRule(_) => {
std::mem::size_of::<ICalendarRecurrenceRule>()
}
_ => VALUE_SIZE,
}
}
pub fn as_text(&self) -> Option<&str> {
match self {
ArchivedICalendarValue::Text(s) => Some(s.as_str()),
ArchivedICalendarValue::Uri(v) => v.as_str(),
ArchivedICalendarValue::CalendarScale(v) => Some(v.as_str()),
ArchivedICalendarValue::Method(v) => Some(v.as_str()),
ArchivedICalendarValue::Classification(v) => Some(v.as_str()),
ArchivedICalendarValue::Status(v) => Some(v.as_str()),
ArchivedICalendarValue::Transparency(v) => Some(v.as_str()),
ArchivedICalendarValue::Action(v) => Some(v.as_str()),
ArchivedICalendarValue::BusyType(v) => Some(v.as_str()),
ArchivedICalendarValue::ParticipantType(v) => Some(v.as_str()),
ArchivedICalendarValue::ResourceType(v) => Some(v.as_str()),
ArchivedICalendarValue::Proximity(v) => Some(v.as_str()),
_ => None,
}
}
pub fn as_integer(&self) -> Option<i64> {
match self {
ArchivedICalendarValue::Integer(i) => Some(i.to_native()),
_ => None,
}
}
pub fn as_float(&self) -> Option<f64> {
match self {
ArchivedICalendarValue::Float(f) => Some(f.to_native()),
_ => None,
}
}
pub fn as_boolean(&self) -> Option<bool> {
match self {
ArchivedICalendarValue::Boolean(b) => Some(*b),
_ => None,
}
}
pub fn as_partial_date_time(&self) -> Option<&ArchivedPartialDateTime> {
match self {
ArchivedICalendarValue::PartialDateTime(dt) => Some(dt),
_ => None,
}
}
pub fn as_binary(&self) -> Option<&[u8]> {
match self {
ArchivedICalendarValue::Binary(d) => Some(d.as_slice()),
_ => None,
}
}
}
impl ArchivedICalendarParameterName {
pub fn as_str(&self) -> &str {
match self {
ArchivedICalendarParameterName::Altrep => "ALTREP",
ArchivedICalendarParameterName::Cn => "CN",
ArchivedICalendarParameterName::Cutype => "CUTYPE",
ArchivedICalendarParameterName::DelegatedFrom => "DELEGATED-FROM",
ArchivedICalendarParameterName::DelegatedTo => "DELEGATED-TO",
ArchivedICalendarParameterName::Dir => "DIR",
ArchivedICalendarParameterName::Fmttype => "FMTTYPE",
ArchivedICalendarParameterName::Fbtype => "FBTYPE",
ArchivedICalendarParameterName::Language => "LANGUAGE",
ArchivedICalendarParameterName::Member => "MEMBER",
ArchivedICalendarParameterName::Partstat => "PARTSTAT",
ArchivedICalendarParameterName::Range => "RANGE",
ArchivedICalendarParameterName::Related => "RELATED",
ArchivedICalendarParameterName::Reltype => "RELTYPE",
ArchivedICalendarParameterName::Role => "ROLE",
ArchivedICalendarParameterName::Rsvp => "RSVP",
ArchivedICalendarParameterName::ScheduleAgent => "SCHEDULE-AGENT",
ArchivedICalendarParameterName::ScheduleForceSend => "SCHEDULE-FORCE-SEND",
ArchivedICalendarParameterName::ScheduleStatus => "SCHEDULE-STATUS",
ArchivedICalendarParameterName::SentBy => "SENT-BY",
ArchivedICalendarParameterName::Tzid => "TZID",
ArchivedICalendarParameterName::Value => "VALUE",
ArchivedICalendarParameterName::Display => "DISPLAY",
ArchivedICalendarParameterName::Email => "EMAIL",
ArchivedICalendarParameterName::Feature => "FEATURE",
ArchivedICalendarParameterName::Label => "LABEL",
ArchivedICalendarParameterName::Size => "SIZE",
ArchivedICalendarParameterName::Filename => "FILENAME",
ArchivedICalendarParameterName::ManagedId => "MANAGED-ID",
ArchivedICalendarParameterName::Order => "ORDER",
ArchivedICalendarParameterName::Schema => "SCHEMA",
ArchivedICalendarParameterName::Derived => "DERIVED",
ArchivedICalendarParameterName::Gap => "GAP",
ArchivedICalendarParameterName::Linkrel => "LINKREL",
ArchivedICalendarParameterName::Jsptr => "JSPTR",
ArchivedICalendarParameterName::Jsid => "JSID",
ArchivedICalendarParameterName::Other(name) => name.as_str(),
}
}
}
impl ArchivedICalendarEntry {
#[inline]
pub fn parameters(
&self,
prop: &ICalendarParameterName,
) -> impl Iterator<Item = &ArchivedICalendarParameterValue> {
self.params.iter().filter_map(move |param| {
if &param.name == prop {
Some(&param.value)
} else {
None
}
})
}
pub fn size(&self) -> usize {
self.values.iter().map(|value| value.size()).sum::<usize>()
+ self
.params
.iter()
.map(|param| std::mem::size_of::<ICalendarParameterName>() + param.value.size())
.sum::<usize>()
+ self.name.as_str().len()
}
}
impl ArchivedICalendarParameterValue {
pub fn as_text(&self) -> Option<&str> {
match self {
ArchivedICalendarParameterValue::Text(v) => Some(v.as_str()),
ArchivedICalendarParameterValue::Bool(v) => Some(if *v { "TRUE" } else { "FALSE" }),
ArchivedICalendarParameterValue::Uri(uri) => uri.as_str(),
ArchivedICalendarParameterValue::Cutype(v) => Some(v.as_str()),
ArchivedICalendarParameterValue::Fbtype(v) => Some(v.as_str()),
ArchivedICalendarParameterValue::Partstat(v) => Some(v.as_str()),
ArchivedICalendarParameterValue::Related(v) => Some(v.as_str()),
ArchivedICalendarParameterValue::Reltype(v) => Some(v.as_str()),
ArchivedICalendarParameterValue::Role(v) => Some(v.as_str()),
ArchivedICalendarParameterValue::ScheduleAgent(v) => Some(v.as_str()),
ArchivedICalendarParameterValue::ScheduleForceSend(v) => Some(v.as_str()),
ArchivedICalendarParameterValue::Value(v) => Some(v.as_str()),
ArchivedICalendarParameterValue::Display(v) => Some(v.as_str()),
ArchivedICalendarParameterValue::Feature(v) => Some(v.as_str()),
ArchivedICalendarParameterValue::Linkrel(v) => Some(v.as_str()),
ArchivedICalendarParameterValue::Duration(_)
| ArchivedICalendarParameterValue::Integer(_)
| ArchivedICalendarParameterValue::Null => None,
}
}
pub fn size(&self) -> usize {
match self {
ArchivedICalendarParameterValue::Text(v) => v.len(),
ArchivedICalendarParameterValue::Uri(v) => v.size(),
_ => PARAMETER_VALUE_SIZE,
}
}
}
impl ArchivedUri {
pub fn as_str(&self) -> Option<&str> {
match self {
ArchivedUri::Data(data) => data.content_type.as_deref(),
ArchivedUri::Location(loc) => loc.as_str().into(),
}
}
pub fn size(&self) -> usize {
match self {
ArchivedUri::Data(data) => {
data.data.len() + data.content_type.as_ref().map(|s| s.len()).unwrap_or(0)
}
ArchivedUri::Location(loc) => loc.len(),
}
}
}
impl ArchivedICalendarPeriod {
pub fn time_range(&self, tz: Tz) -> Option<(ZonedDateTime, ZonedDateTime)> {
match self {
ArchivedICalendarPeriod::Range { start, end } => {
if let (Some(start), Some(end)) = (
start
.to_date_time()
.and_then(|start| start.to_date_time_with_tz(tz)),
end.to_date_time()
.and_then(|end| end.to_date_time_with_tz(tz)),
) {
Some((start, end))
} else {
None
}
}
ArchivedICalendarPeriod::Duration { start, duration } => start
.to_date_time()
.and_then(|start| start.to_date_time_with_tz(tz))
.zip(duration.to_nominal())
.and_then(|(start, duration)| {
start.checked_add_nominal(duration).map(|end| (start, end))
}),
}
}
}
impl ArchivedPartialDateTime {
pub fn to_date_time_with_tz(&self, tz: Tz) -> Option<ZonedDateTime> {
self.to_date_time()
.and_then(|dt| dt.to_date_time_with_tz(tz))
}
#[inline(always)]
pub fn has_date(&self) -> bool {
self.year.is_some() && self.month.is_some() && self.day.is_some()
}
#[inline(always)]
pub fn has_time(&self) -> bool {
self.hour.is_some() && self.minute.is_some()
}
#[inline(always)]
pub fn has_zone(&self) -> bool {
self.tz_hour.is_some()
}
#[inline(always)]
pub fn has_date_and_time(&self) -> bool {
self.has_date() && self.has_time()
}
}
impl ArchivedICalendarDuration {
/// Returns this duration as a calendar span.
pub fn to_nominal(&self) -> Option<NominalDuration> {
let days =
i32::try_from(i64::from(self.weeks.to_native()) * 7 + i64::from(self.days.to_native()))
.ok()?;
let seconds = i64::from(self.hours.to_native()) * 3600
+ i64::from(self.minutes.to_native()) * 60
+ i64::from(self.seconds.to_native());
Some(if self.neg {
NominalDuration::new(-days, -seconds)
} else {
NominalDuration::new(days, seconds)
})
}
pub fn as_seconds(&self) -> i64 {
let secs = self.seconds.to_native() as i64
+ self.minutes.to_native() as i64 * 60
+ self.hours.to_native() as i64 * 3600
+ self.days.to_native() as i64 * 86400
+ self.weeks.to_native() as i64 * 604800;
if self.neg { -secs } else { secs }
}
pub fn to_native(&self) -> ICalendarDuration {
ICalendarDuration {
neg: self.neg,
weeks: self.weeks.to_native(),
days: self.days.to_native(),
hours: self.hours.to_native(),
minutes: self.minutes.to_native(),
seconds: self.seconds.to_native(),
}
}
}
impl ArchivedICalendarComponentType {
pub fn is_calendar_object(&self) -> bool {
matches!(
self,
ArchivedICalendarComponentType::VEvent
| ArchivedICalendarComponentType::VTodo
| ArchivedICalendarComponentType::VJournal
| ArchivedICalendarComponentType::VFreebusy
| ArchivedICalendarComponentType::VAvailability
)
}
pub(crate) fn converts_links(&self) -> bool {
matches!(
self,
ArchivedICalendarComponentType::VEvent
| ArchivedICalendarComponentType::VTodo
| ArchivedICalendarComponentType::Participant
| ArchivedICalendarComponentType::VLocation
| ArchivedICalendarComponentType::VCalendar
)
}
pub fn has_time_ranges(&self) -> bool {
matches!(
self,
ArchivedICalendarComponentType::VEvent
| ArchivedICalendarComponentType::VTodo
| ArchivedICalendarComponentType::VJournal
| ArchivedICalendarComponentType::VFreebusy
)
}
pub fn is_scheduling_object(&self) -> bool {
matches!(
self,
ArchivedICalendarComponentType::VEvent
| ArchivedICalendarComponentType::VTodo
| ArchivedICalendarComponentType::VJournal
| ArchivedICalendarComponentType::VFreebusy
)
}
pub fn is_event(&self) -> bool {
matches!(self, ArchivedICalendarComponentType::VEvent)
}
pub fn is_todo(&self) -> bool {
matches!(self, ArchivedICalendarComponentType::VTodo)
}
pub fn is_event_or_todo(&self) -> bool {
matches!(
self,
ArchivedICalendarComponentType::VEvent | ArchivedICalendarComponentType::VTodo
)
}
pub fn is_journal(&self) -> bool {
matches!(self, ArchivedICalendarComponentType::VJournal)
}
pub fn is_freebusy(&self) -> bool {
matches!(self, ArchivedICalendarComponentType::VFreebusy)
}
pub fn is_location(&self) -> bool {
matches!(self, ArchivedICalendarComponentType::VLocation)
}
pub fn is_alarm(&self) -> bool {
matches!(self, ArchivedICalendarComponentType::VAlarm)
}
pub fn is_participant(&self) -> bool {
matches!(self, ArchivedICalendarComponentType::Participant)
}
}
impl ArchivedICalendarMonth {
pub fn is_leap(&self) -> bool {
self.0 < 0
}
pub fn month(&self) -> u8 {
self.0.unsigned_abs()
}
}
impl PartialEq<Box<ICalendarPeriod>> for ArchivedICalendarPeriod {
fn eq(&self, other: &Box<ICalendarPeriod>) -> bool {
self == other.as_ref()
}
}
+494
View File
@@ -0,0 +1,494 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::*;
use crate::{
common::{
ArchivedPartialDateTime, CalendarScale, PartialDateTime,
format::{AsciiPush, BufferedWrite, DurationParts},
stack::ComponentStack,
writer::{
DOCUMENT_BUFFER, ENTRY_BUFFER, FoldingWriter, LineWriter, VisitedComponents,
write_param_text, write_param_value, write_quoted_param_value, write_text, write_uri,
},
},
icalendar::{
ValueSeparator,
writer::{DATE_BUFFER, DURATION_BUFFER, PERIOD_BUFFER, RRULE_BUFFER},
},
};
use std::{
fmt::{self, Display, Write},
slice::Iter,
};
impl ArchivedICalendar {
pub fn write_to(&self, out: &mut impl Write) -> fmt::Result {
out.write_buffered::<DOCUMENT_BUFFER>(|buf| {
self.write_components(&mut FoldingWriter::new(buf))
})
}
fn write_components<W: Write + AsciiPush + ?Sized>(
&self,
out: &mut FoldingWriter<'_, W>,
) -> fmt::Result {
let _v = [0.into()];
let mut component_iter: Iter<'_, rkyv::primitive::ArchivedU32> = _v.iter();
let mut component_stack = ComponentStack::new();
let mut visited = VisitedComponents::new(self.components.len());
loop {
if let Some(component_id) = component_iter.next() {
let component_id = component_id.to_native() as usize;
let Some(component) = self
.components
.get(component_id)
.filter(|_| visited.insert(component_id))
else {
continue;
};
out.write_boundary("BEGIN:", component.component_type.as_str())?;
for entry in component.entries.iter() {
if !matches!(
entry.name,
ArchivedICalendarProperty::Begin | ArchivedICalendarProperty::End
) {
entry.write_line(out, true)?;
}
}
if !component.component_ids.is_empty() {
component_stack.push((component, component_iter));
component_iter = component.component_ids.iter();
} else {
out.write_boundary("END:", component.component_type.as_str())?;
}
} else if let Some((component, iter)) = component_stack.pop() {
out.write_boundary("END:", component.component_type.as_str())?;
component_iter = iter;
} else {
break;
}
}
Ok(())
}
}
impl ArchivedICalendarEntry {
pub fn write_to(&self, out: &mut impl Write, with_value: bool) -> fmt::Result {
out.write_buffered::<ENTRY_BUFFER>(|buf| {
self.write_line(&mut FoldingWriter::new(buf), with_value)
})
}
pub(crate) fn write_line<W: Write + AsciiPush + ?Sized>(
&self,
out: &mut FoldingWriter<'_, W>,
with_value: bool,
) -> fmt::Result {
out.write_atomic(self.name.as_str())?;
if matches!(
self.values.first().as_ref(),
Some(ArchivedICalendarValue::Binary(_))
) {
out.write_atomic(";ENCODING=BASE64")?;
if !self
.params
.iter()
.any(|param| param.name == ArchivedICalendarParameterName::Value)
{
out.write_atomic(";VALUE=BINARY")?;
}
}
let mut types = None;
let mut last_param: Option<&ArchivedICalendarParameterName> = None;
for param in self.params.iter() {
if last_param.is_some_and(|last_param| last_param == &param.name) {
out.write_atomic(",")?;
} else {
out.write_atomic(";")?;
out.write_atomic(param.name.as_str())?;
if !matches!(param.value, ArchivedICalendarParameterValue::Null) {
out.write_atomic("=")?;
}
last_param = Some(&param.name);
}
match &param.value {
ArchivedICalendarParameterValue::Text(v)
if matches!(param.name, ArchivedICalendarParameterName::Jsptr) =>
{
write_quoted_param_value(out, v, true)?;
}
ArchivedICalendarParameterValue::Text(v) => {
write_param_value(out, v, true)?;
}
ArchivedICalendarParameterValue::Integer(i) => {
out.push_u64(i.to_native())?;
}
ArchivedICalendarParameterValue::Bool(v) => {
let v = if !matches!(param.name, ArchivedICalendarParameterName::Range) {
if *v { "TRUE" } else { "FALSE" }
} else {
"THISANDFUTURE"
};
out.write_atomic(v)?;
}
ArchivedICalendarParameterValue::Uri(uri) => {
out.write_atomic("\"")?;
uri.write_param_text(out)?;
out.write_atomic("\"")?;
}
ArchivedICalendarParameterValue::Cutype(v) => {
write_param_value(out, v.as_str(), true)?;
}
ArchivedICalendarParameterValue::Fbtype(v) => {
write_param_value(out, v.as_str(), true)?;
}
ArchivedICalendarParameterValue::Partstat(v) => {
write_param_value(out, v.as_str(), true)?;
}
ArchivedICalendarParameterValue::Related(v) => {
write_param_value(out, v.as_str(), true)?;
}
ArchivedICalendarParameterValue::Reltype(v) => {
write_param_value(out, v.as_str(), true)?;
}
ArchivedICalendarParameterValue::Role(v) => {
write_param_value(out, v.as_str(), true)?;
}
ArchivedICalendarParameterValue::ScheduleAgent(v) => {
write_param_value(out, v.as_str(), true)?;
}
ArchivedICalendarParameterValue::ScheduleForceSend(v) => {
write_param_value(out, v.as_str(), true)?;
}
ArchivedICalendarParameterValue::Value(v) => {
types = Some(v);
write_param_value(out, v.as_str(), true)?;
}
ArchivedICalendarParameterValue::Display(v) => {
write_param_value(out, v.as_str(), true)?;
}
ArchivedICalendarParameterValue::Feature(v) => {
write_param_value(out, v.as_str(), true)?;
}
ArchivedICalendarParameterValue::Duration(v) => {
out.push_duration(v.parts())?;
}
ArchivedICalendarParameterValue::Linkrel(v) => {
write_param_value(out, v.as_str(), true)?;
}
ArchivedICalendarParameterValue::Null => {
last_param = None;
}
}
}
out.write_atomic(":")?;
if with_value {
let (default_type, separator) = self.name.default_types();
let separator = if !matches!(separator, ValueSeparator::Comma) {
";"
} else {
","
};
let default_type = default_type.unwrap_ical();
for (pos, value) in self.values.iter().enumerate() {
if pos > 0 {
out.write_atomic(separator)?;
}
let text = match value {
ArchivedICalendarValue::Binary(v) => {
out.write_base64(v)?;
continue;
}
ArchivedICalendarValue::Boolean(v) => {
out.write_atomic(if *v { "TRUE" } else { "FALSE" })?;
continue;
}
ArchivedICalendarValue::Uri(v) => {
v.write_value(out)?;
continue;
}
ArchivedICalendarValue::PartialDateTime(v) => {
PartialDateTime::from(v).push_ical(
out,
&ICalendarValueType::from(types.unwrap_or(&default_type)),
)?;
continue;
}
ArchivedICalendarValue::Duration(v) => {
out.push_duration(v.parts())?;
continue;
}
ArchivedICalendarValue::RecurrenceRule(v) => {
v.push_to(out)?;
continue;
}
ArchivedICalendarValue::Period(v) => {
v.push_to(out)?;
continue;
}
ArchivedICalendarValue::Float(v) => {
write!(out, "{v}")?;
continue;
}
ArchivedICalendarValue::Integer(v) => {
out.push_i64(v.to_native())?;
continue;
}
ArchivedICalendarValue::Text(v) => {
match types.unwrap_or(&default_type) {
ArchivedICalendarValueType::Uri
| ArchivedICalendarValueType::CalAddress => {
write_uri(out, v)?;
}
ArchivedICalendarValueType::Recur => write_text(out, v, false, false)?,
_ => write_text(out, v, true, true)?,
}
continue;
}
ArchivedICalendarValue::CalendarScale(v) => v.as_str(),
ArchivedICalendarValue::Method(v) => v.as_str(),
ArchivedICalendarValue::Classification(v) => v.as_str(),
ArchivedICalendarValue::Status(v) => v.as_str(),
ArchivedICalendarValue::Transparency(v) => v.as_str(),
ArchivedICalendarValue::Action(v) => v.as_str(),
ArchivedICalendarValue::BusyType(v) => v.as_str(),
ArchivedICalendarValue::ParticipantType(v) => v.as_str(),
ArchivedICalendarValue::ResourceType(v) => v.as_str(),
ArchivedICalendarValue::Proximity(v) => v.as_str(),
};
out.write_atomic(text)?;
}
}
out.end_line()
}
}
impl ArchivedUri {
fn write_value(&self, out: &mut impl LineWriter) -> fmt::Result {
match self {
ArchivedUri::Data(v) => {
out.write_str("data:")?;
out.write_str(v.content_type.as_deref().unwrap_or_default())?;
out.write_str(";")?;
out.write_atomic("base64,")?;
out.write_base64(&v.data)
}
ArchivedUri::Location(v) => write_uri(out, v),
}
}
fn write_param_text(&self, out: &mut impl LineWriter) -> fmt::Result {
match self {
ArchivedUri::Data(v) => {
out.write_str("data:")?;
write_param_text(out, v.content_type.as_deref().unwrap_or_default(), true)?;
out.write_str(";")?;
out.write_atomic("base64,")?;
out.write_base64(&v.data)
}
ArchivedUri::Location(v) => write_param_text(out, v, true),
}
}
}
impl ArchivedICalendarRecurrenceRule {
fn push_to(&self, buf: &mut impl AsciiPush) -> fmt::Result {
buf.push_ascii_str("FREQ=")?;
buf.push_ascii_str(self.freq.as_str())?;
if let Some(until) = self.until.as_ref() {
buf.push_ascii_str(";UNTIL=")?;
PartialDateTime::from(until).push_ical(
buf,
if until.has_date_and_time() {
&ICalendarValueType::DateTime
} else {
&ICalendarValueType::Date
},
)?;
}
if let Some(count) = self.count.as_ref().filter(|c| **c > 0) {
buf.push_ascii_str(";COUNT=")?;
buf.push_u64(count.to_native().into())?;
}
if let Some(interval) = self.interval.as_ref() {
buf.push_ascii_str(";INTERVAL=")?;
buf.push_u64(interval.to_native().into())?;
}
buf.push_list(";BYSECOND=", &self.bysecond, |buf, item| {
buf.push_u64((*item).into())
})?;
buf.push_list(";BYMINUTE=", &self.byminute, |buf, item| {
buf.push_u64((*item).into())
})?;
buf.push_list(";BYHOUR=", &self.byhour, |buf, item| {
buf.push_u64((*item).into())
})?;
buf.push_list(";BYDAY=", &self.byday, |buf, item| item.push_to(buf))?;
buf.push_list(";BYMONTHDAY=", &self.bymonthday, |buf, item| {
buf.push_i64((*item).into())
})?;
buf.push_list(";BYYEARDAY=", &self.byyearday, |buf, item| {
buf.push_i64(item.to_native().into())
})?;
buf.push_list(";BYWEEKNO=", &self.byweekno, |buf, item| {
buf.push_i64((*item).into())
})?;
buf.push_list(";BYMONTH=", &self.bymonth, |buf, item| item.push_to(buf))?;
buf.push_list(";BYSETPOS=", &self.bysetpos, |buf, item| {
buf.push_i64(item.to_native().into())
})?;
if let Some(wkst) = self.wkst.as_ref() {
buf.push_ascii_str(";WKST=")?;
buf.push_ascii_str(wkst.as_str())?;
}
if let Some(rscale) = self.rscale.as_ref() {
buf.push_ascii_str(";RSCALE=")?;
buf.push_ascii_str(rscale.as_str())?;
} else if self.skip.is_some() {
buf.push_ascii_str(";RSCALE=")?;
buf.push_ascii_str(CalendarScale::Gregorian.as_str())?;
}
if let Some(skip) = self.skip.as_ref() {
buf.push_ascii_str(";SKIP=")?;
buf.push_ascii_str(skip.as_str())?;
}
Ok(())
}
}
impl Display for ArchivedICalendarRecurrenceRule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_buffered::<RRULE_BUFFER>(|buf| self.push_to(buf))
}
}
impl ArchivedICalendarDay {
fn push_to(&self, buf: &mut impl AsciiPush) -> fmt::Result {
if let Some(ordwk) = self.ordwk.as_ref() {
buf.push_i64(ordwk.to_native().into())?;
}
buf.push_ascii_str(self.weekday.as_str())
}
}
impl Display for ArchivedICalendarDay {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_buffered::<DATE_BUFFER>(|buf| self.push_to(buf))
}
}
impl ArchivedICalendarMonth {
fn push_to(&self, buf: &mut impl AsciiPush) -> fmt::Result {
buf.push_u64(self.month().into())?;
if self.is_leap() {
buf.push_byte(b'L')?;
}
Ok(())
}
}
impl Display for ArchivedICalendarMonth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_buffered::<DATE_BUFFER>(|buf| self.push_to(buf))
}
}
impl ArchivedICalendarPeriod {
fn push_to(&self, buf: &mut impl AsciiPush) -> fmt::Result {
match self {
ArchivedICalendarPeriod::Range { start, end } => {
PartialDateTime::from(start).push_ical(buf, &ICalendarValueType::DateTime)?;
buf.push_byte(b'/')?;
PartialDateTime::from(end).push_ical(buf, &ICalendarValueType::DateTime)
}
ArchivedICalendarPeriod::Duration { start, duration } => {
PartialDateTime::from(start).push_ical(buf, &ICalendarValueType::DateTime)?;
buf.push_byte(b'/')?;
buf.push_duration(duration.parts())
}
}
}
}
impl Display for ArchivedICalendarPeriod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_buffered::<PERIOD_BUFFER>(|buf| self.push_to(buf))
}
}
impl ArchivedICalendarDuration {
fn parts(&self) -> DurationParts {
DurationParts {
neg: self.neg,
weeks: self.weeks.to_native(),
days: self.days.to_native(),
hours: self.hours.to_native(),
minutes: self.minutes.to_native(),
seconds: self.seconds.to_native(),
}
}
}
impl Display for ArchivedICalendarDuration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_buffered::<DURATION_BUFFER>(|buf| buf.push_duration(self.parts()))
}
}
impl ArchivedPartialDateTime {
pub fn format_as_ical(
&self,
out: &mut impl Write,
fmt: &ArchivedICalendarValueType,
) -> fmt::Result {
PartialDateTime::from(self).format_as_ical(out, &ICalendarValueType::from(fmt))
}
}
impl From<&ArchivedICalendarValueType> for ICalendarValueType {
fn from(value_type: &ArchivedICalendarValueType) -> Self {
match value_type {
ArchivedICalendarValueType::Binary => ICalendarValueType::Binary,
ArchivedICalendarValueType::Boolean => ICalendarValueType::Boolean,
ArchivedICalendarValueType::CalAddress => ICalendarValueType::CalAddress,
ArchivedICalendarValueType::Date => ICalendarValueType::Date,
ArchivedICalendarValueType::DateTime => ICalendarValueType::DateTime,
ArchivedICalendarValueType::Duration => ICalendarValueType::Duration,
ArchivedICalendarValueType::Float => ICalendarValueType::Float,
ArchivedICalendarValueType::Integer => ICalendarValueType::Integer,
ArchivedICalendarValueType::Period => ICalendarValueType::Period,
ArchivedICalendarValueType::Recur => ICalendarValueType::Recur,
ArchivedICalendarValueType::Text => ICalendarValueType::Text,
ArchivedICalendarValueType::Time => ICalendarValueType::Time,
ArchivedICalendarValueType::Unknown => ICalendarValueType::Unknown,
ArchivedICalendarValueType::Uri => ICalendarValueType::Uri,
ArchivedICalendarValueType::UtcOffset => ICalendarValueType::UtcOffset,
ArchivedICalendarValueType::XmlReference => ICalendarValueType::XmlReference,
ArchivedICalendarValueType::Uid => ICalendarValueType::Uid,
}
}
}
impl Display for ArchivedICalendar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.write_to(f)
}
}
+700
View File
@@ -0,0 +1,700 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarDay, ICalendarEntry,
ICalendarFrequency, ICalendarMonth, ICalendarPeriod, ICalendarProperty,
ICalendarRecurrenceRule, ICalendarValue, ICalendarWeekday,
};
use crate::{
common::{
PartialDateTime,
timezone::{Tz, ZonedDateTime},
},
icalendar::ICalendarParameterName,
};
use jiff::{
Timestamp,
civil::{self, Date, Weekday},
tz::{Offset, TimeZone},
};
use std::{collections::HashMap, ops::Range, str::FromStr};
pub struct TzResolver<T> {
tzs: HashMap<T, Tz>,
default: Tz,
}
impl<T> TzResolver<T>
where
T: std::borrow::Borrow<str> + std::hash::Hash + Eq,
{
pub fn resolve(&self, tz_name: &str) -> Option<Tz> {
self.tzs
.get(tz_name)
.copied()
.or_else(|| Tz::from_str(tz_name).ok())
}
pub fn resolve_or_default(&self, tz_name: Option<&str>) -> Tz {
tz_name
.and_then(|tz_name| {
self.tzs
.get(tz_name)
.copied()
.or_else(|| Tz::from_str(tz_name).ok())
})
.unwrap_or(self.default)
}
pub fn with_default(mut self, default: impl Into<Tz>) -> Self {
self.default = default.into();
self
}
}
impl ICalendar {
pub fn timezones(&self) -> impl Iterator<Item = &ICalendarComponent> {
self.components
.iter()
.filter(|comp| matches!(comp.component_type, ICalendarComponentType::VTimezone))
}
pub fn is_timezone(&self) -> bool {
self.timezones().count() == 1
}
pub fn build_tz_resolver(&self) -> TzResolver<&'_ str> {
TzResolver {
tzs: self.timezones().filter_map(|tz| tz.timezone()).collect(),
default: Tz::Floating,
}
}
pub fn build_owned_tz_resolver(&self) -> TzResolver<String> {
TzResolver {
tzs: self
.timezones()
.filter_map(|tz| tz.timezone())
.map(|(name, tz)| (name.to_string(), tz))
.collect(),
default: Tz::Floating,
}
}
}
impl ICalendarComponent {
pub fn timezone(&self) -> Option<(&str, Tz)> {
let mut tz_name = None;
let mut tz_id = None;
let mut tz_lic = None;
let mut tz_cdo_id = None;
for entry in &self.entries {
match (&entry.name, entry.values.first()) {
(ICalendarProperty::Tzid, Some(ICalendarValue::Text(id))) => {
tz_id = Tz::from_str(id).ok();
tz_name = Some(id.as_str());
}
(ICalendarProperty::Other(value), Some(ICalendarValue::Text(id))) => {
hashify::fnc_map!(value.as_bytes(),
"X-LIC-LOCATION" => {
tz_lic = Tz::from_str(id.strip_prefix("SystemV/").unwrap_or(id.as_str())).ok();
},
"X-MICROSOFT-CDO-TZID" => {
tz_cdo_id = Tz::from_ms_cdo_zone_id(id);
},
_ => {}
);
}
_ => (),
}
}
tz_name.zip(tz_id.or(tz_lic).or(tz_cdo_id))
}
}
impl ICalendarPeriod {
pub fn time_range(&self, tz: Tz) -> Option<(ZonedDateTime, ZonedDateTime)> {
match self {
ICalendarPeriod::Range { start, end } => start
.to_date_time_with_tz(tz)
.zip(end.to_date_time_with_tz(tz)),
ICalendarPeriod::Duration { start, duration } => start
.to_date_time_with_tz(tz)
.zip(duration.to_nominal())
.and_then(|(start, duration)| {
start.checked_add_nominal(duration).map(|end| (start, end))
}),
}
}
}
impl ICalendarEntry {
pub fn tz_id(&self) -> Option<&str> {
self.parameters(&ICalendarParameterName::Tzid)
.find_map(|v| v.as_text())
}
}
const TZ_MIN_YEAR: i32 = 1900;
const TZ_MAX_FUTURE_YEARS: i32 = 100;
const TZ_RANGE_MARGIN: i64 = 86400;
const TZ_RULE_ACTIVE_SECONDS: i64 = 366 * 86400;
const MAX_OBSERVANCES: usize = 4096;
impl ICalendar {
pub fn add_timezone(&mut self, tz_id: &str, from: i64, to: i64) -> Option<u32> {
self.calendar_root()?;
let tz = Tz::from_str(tz_id).ok()?;
let observances = match tz {
Tz::Iana(_) => build_observances(tz.time_zone()?, from, to),
Tz::Fixed(offset) => {
let offset = round_to_minute(offset.seconds());
vec![TzObservance {
at: from,
from_offset: offset,
to_offset: offset,
is_dst: false,
name: None,
rrule: None,
}]
}
Tz::Floating => return None,
};
if observances.is_empty() {
return None;
}
let tz_component_id = u32::try_from(self.components.len()).ok()?;
let first_component_id = tz_component_id.checked_add(1)?;
let last_component_id =
first_component_id.checked_add(u32::try_from(observances.len()).ok()?)?;
let insert_at = self
.calendar_root()?
.component_ids
.iter()
.position(|component_id| {
self.component_by_id(*component_id)
.is_none_or(|comp| comp.component_type != ICalendarComponentType::VTimezone)
});
self.components.reserve(observances.len() + 1);
self.components.push(ICalendarComponent {
component_type: ICalendarComponentType::VTimezone,
entries: vec![
ICalendarEntry::new(ICalendarProperty::Tzid).with_value(tz_id.to_string()),
],
component_ids: (first_component_id..last_component_id).collect(),
});
self.components
.extend(observances.into_iter().map(TzObservance::into_component));
let root_ids = &mut self.components.first_mut()?.component_ids;
root_ids.insert(insert_at.unwrap_or(root_ids.len()), tz_component_id);
Some(tz_component_id)
}
pub fn add_missing_timezones(&mut self) -> usize {
let mut defined: Vec<&str> = Vec::new();
for component in self.timezones() {
for entry in &component.entries {
if entry.name == ICalendarProperty::Tzid
&& let Some(ICalendarValue::Text(tz_id)) = entry.values.first()
{
defined.push(tz_id.as_str());
}
}
}
let mut referenced: Vec<String> = Vec::new();
let mut range: Option<(i64, i64)> = None;
let mut recurrence_span = 0;
let mut is_unbounded = false;
for component in &self.components {
if matches!(
component.component_type,
ICalendarComponentType::VTimezone
| ICalendarComponentType::Standard
| ICalendarComponentType::Daylight
) {
continue;
}
for entry in &component.entries {
for param in &entry.params {
if param.name == ICalendarParameterName::Tzid
&& let Some(tz_id) = param.value.as_text()
&& !defined.contains(&tz_id)
&& !referenced.iter().any(|id| id == tz_id)
{
referenced.push(tz_id.to_string());
}
}
match &entry.name {
ICalendarProperty::Rrule => {
for value in &entry.values {
if let ICalendarValue::RecurrenceRule(rule) = value {
match (&rule.until, &rule.count) {
(Some(until), _) => expand_range(&mut range, until),
(None, Some(count)) => {
recurrence_span = recurrence_span.max(recurrence_span_of(
&rule.freq,
*count,
rule.interval,
));
}
(None, None) => is_unbounded = true,
}
}
}
}
ICalendarProperty::Dtstart
| ICalendarProperty::Dtend
| ICalendarProperty::Due
| ICalendarProperty::RecurrenceId
| ICalendarProperty::Exdate
| ICalendarProperty::Rdate => {
for value in &entry.values {
match value {
ICalendarValue::PartialDateTime(dt) => {
expand_range(&mut range, dt);
}
ICalendarValue::Period(period) => match period.as_ref() {
ICalendarPeriod::Range { start, end } => {
expand_range(&mut range, start);
expand_range(&mut range, end);
}
ICalendarPeriod::Duration { start, .. } => {
expand_range(&mut range, start);
}
},
_ => (),
}
}
}
_ => (),
}
}
}
if referenced.is_empty() {
return 0;
}
let now = Timestamp::now().as_second();
let (min, max) = range.unwrap_or((now, now));
let max = max.saturating_add(recurrence_span);
let current_year = year_of(now);
let max_year = current_year.saturating_add(TZ_MAX_FUTURE_YEARS);
let from_year = year_of(min).clamp(TZ_MIN_YEAR, max_year);
let to_year = if is_unbounded {
year_of(max).max(current_year)
} else {
year_of(max)
}
.saturating_add(2)
.clamp(from_year.saturating_add(1), max_year.saturating_add(2));
let from = start_of_year(from_year).saturating_sub(TZ_RANGE_MARGIN);
let to = start_of_year(to_year);
let mut added = 0;
for tz_id in referenced {
if self.add_timezone(&tz_id, from, to).is_some() {
added += 1;
}
}
added
}
}
#[derive(PartialEq, Eq)]
struct TzRuleKey {
month: u8,
ordwk: i16,
weekday: ICalendarWeekday,
hour: u8,
minute: u8,
second: u8,
from_offset: i32,
to_offset: i32,
}
struct TzObservance {
at: i64,
from_offset: i32,
to_offset: i32,
is_dst: bool,
name: Option<String>,
rrule: Option<ICalendarRecurrenceRule>,
}
impl TzObservance {
fn onset(&self) -> civil::DateTime {
naive_utc(self.at.saturating_add(self.from_offset as i64))
}
fn rule_key(&self) -> Option<(i32, TzRuleKey)> {
let onset = self.onset();
let day = u32::from(onset.day().unsigned_abs());
let ordwk = if day.saturating_add(7)
> days_in_month(
i32::from(onset.year()),
u32::from(onset.month().unsigned_abs()),
) {
-1
} else {
((day - 1) / 7 + 1) as i16
};
Some((
i32::from(onset.year()),
TzRuleKey {
month: u8::try_from(onset.month()).ok()?,
ordwk,
weekday: weekday_of(onset.weekday()),
hour: u8::try_from(onset.hour()).ok()?,
minute: u8::try_from(onset.minute()).ok()?,
second: u8::try_from(onset.second()).ok()?,
from_offset: self.from_offset,
to_offset: self.to_offset,
},
))
}
fn into_component(self) -> ICalendarComponent {
let mut component = ICalendarComponent::new(if self.is_dst {
ICalendarComponentType::Daylight
} else {
ICalendarComponentType::Standard
});
component
.entries
.push(ICalendarEntry::new(ICalendarProperty::Dtstart).with_value(
PartialDateTime::from_naive_timestamp(naive_seconds(self.onset())),
));
component.entries.push(
ICalendarEntry::new(ICalendarProperty::Tzoffsetfrom)
.with_value(offset_to_partial(self.from_offset)),
);
component.entries.push(
ICalendarEntry::new(ICalendarProperty::Tzoffsetto)
.with_value(offset_to_partial(self.to_offset)),
);
if let Some(name) = self.name {
component
.entries
.push(ICalendarEntry::new(ICalendarProperty::Tzname).with_value(name));
}
if let Some(rrule) = self.rrule {
component
.entries
.push(ICalendarEntry::new(ICalendarProperty::Rrule).with_value(rrule));
}
component
}
}
/// Collects the observances a zone goes through between two instants.
fn build_observances(tz: &TimeZone, from: i64, to: i64) -> Vec<TzObservance> {
let Ok(start) = Timestamp::from_second(from) else {
return Vec::new();
};
let initial = tz.to_offset_info(start);
let mut observances = Vec::with_capacity(estimated_observances(from, to));
observances.push(TzObservance {
at: from,
from_offset: round_to_minute(initial.offset().seconds()),
to_offset: round_to_minute(initial.offset().seconds()),
is_dst: initial.dst().is_dst(),
name: abbreviation(initial.abbreviation()),
rrule: None,
});
let mut previous = initial.offset();
for transition in tz.following(start) {
let at = transition.timestamp().as_second();
if at >= to || observances.len() >= MAX_OBSERVANCES {
break;
}
observances.push(TzObservance {
at,
from_offset: round_to_minute(previous.seconds()),
to_offset: round_to_minute(transition.offset().seconds()),
is_dst: transition.dst().is_dst(),
name: abbreviation(transition.abbreviation()),
rrule: None,
});
previous = transition.offset();
}
label_observances(&mut observances);
collapse_observances(observances, to)
}
fn abbreviation(name: &str) -> Option<String> {
(!name.is_empty()
&& !name.starts_with(['+', '-'])
&& name.bytes().all(|b| b.is_ascii_alphabetic()))
.then(|| name.to_string())
}
fn label_observances(observances: &mut [TzObservance]) {
let mut daylight_offset = None;
let mut standard_offset = None;
for observance in observances.iter() {
let slot = if observance.is_dst {
&mut daylight_offset
} else {
&mut standard_offset
};
*slot = Some(slot.map_or(observance.to_offset, |offset: i32| {
offset.max(observance.to_offset)
}));
}
if let (Some(daylight_offset), Some(standard_offset)) = (daylight_offset, standard_offset)
&& daylight_offset < standard_offset
{
for observance in observances.iter_mut() {
observance.is_dst = !observance.is_dst;
}
}
}
fn collapse_observances(mut observances: Vec<TzObservance>, to: i64) -> Vec<TzObservance> {
let mut discard = vec![false; observances.len()];
let mut unbounded: [Option<(usize, i64)>; 2] = [None, None];
let mut trailing: [Option<(usize, u8, i16, ICalendarWeekday)>; 2] = [None, None];
let mut onsets: Vec<(usize, i32, TzRuleKey)> = Vec::new();
let mut runs: Vec<Range<usize>> = Vec::new();
for is_dst in [false, true] {
onsets.clear();
runs.clear();
for (index, observance) in observances.iter().enumerate() {
if index == 0 || observance.is_dst != is_dst {
continue;
}
let Some((year, key)) = observance.rule_key() else {
continue;
};
let extends = onsets
.last()
.is_some_and(|(last_index, last_year, last_key)| {
last_year.saturating_add(1) == year
&& last_key == &key
&& observances[*last_index].name == observance.name
});
onsets.push((index, year, key));
match runs.last_mut() {
Some(run) if extends => run.end = onsets.len(),
_ => runs.push(onsets.len() - 1..onsets.len()),
}
}
for (position, run) in runs.iter().enumerate() {
let (Some((first, _, key)), Some((last, _, _))) =
(onsets.get(run.start), onsets.get(run.end - 1))
else {
continue;
};
if run.len() < 2 {
continue;
}
let last_at = observances[*last].at;
let is_expired = last_at.saturating_add(TZ_RULE_ACTIVE_SECONDS) < to;
let is_final = position + 1 == runs.len() && !is_expired;
observances[*first].rrule = Some(ICalendarRecurrenceRule {
freq: ICalendarFrequency::Yearly,
until: (!is_final).then(|| PartialDateTime::from_utc_timestamp(last_at)),
bymonth: vec![ICalendarMonth::new(key.month, false)],
byday: vec![ICalendarDay {
ordwk: Some(key.ordwk),
weekday: key.weekday,
}],
..Default::default()
});
if is_final {
unbounded[usize::from(is_dst)] = Some((*first, last_at));
}
for (index, _, _) in &onsets[run.start + 1..run.end] {
discard[*index] = true;
}
}
if let Some(run) = runs.last()
&& run.len() == 1
&& let Some((index, _, key)) = onsets.get(run.start)
{
trailing[usize::from(is_dst)] = Some((*index, key.month, key.ordwk, key.weekday));
}
}
for is_dst in [false, true] {
let class = usize::from(is_dst);
let (None, Some((other_index, other_at))) =
(unbounded[class], unbounded[usize::from(!is_dst)])
else {
continue;
};
match trailing[class] {
Some((index, month, ordwk, weekday))
if observances[index].at.saturating_add(TZ_RULE_ACTIVE_SECONDS) >= to =>
{
observances[index].rrule = Some(ICalendarRecurrenceRule {
freq: ICalendarFrequency::Yearly,
bymonth: vec![ICalendarMonth::new(month, false)],
byday: vec![ICalendarDay {
ordwk: Some(ordwk),
weekday,
}],
..Default::default()
});
unbounded[class] = Some((index, observances[index].at));
}
_ => {
if let Some(rrule) = observances[other_index].rrule.as_mut() {
rrule.until = Some(PartialDateTime::from_utc_timestamp(other_at));
unbounded[usize::from(!is_dst)] = None;
}
}
}
}
let mut index = 0;
observances.retain(|_| {
let keep = !discard[index];
index += 1;
keep
});
observances
}
fn estimated_observances(from: i64, to: i64) -> usize {
let years = to.saturating_sub(from) / TZ_RULE_ACTIVE_SECONDS;
(years.clamp(0, 1024) as usize) * 2 + 2
}
/// Returns a civil datetime as seconds since the Unix epoch, read as UTC.
fn naive_seconds(date_time: civil::DateTime) -> i64 {
Offset::UTC
.to_timestamp(date_time)
.map_or(0, |timestamp| timestamp.as_second())
}
fn round_to_minute(offset: i32) -> i32 {
let minutes = (offset.unsigned_abs() + 30) / 60 * 60;
if offset < 0 {
-(minutes as i32)
} else {
minutes as i32
}
}
fn offset_to_partial(offset: i32) -> PartialDateTime {
let seconds = offset.unsigned_abs();
PartialDateTime {
tz_hour: u8::try_from(seconds / 3600).ok(),
tz_minute: u8::try_from((seconds % 3600) / 60).ok(),
tz_minus: offset < 0,
..Default::default()
}
}
fn recurrence_span_of(freq: &ICalendarFrequency, count: u32, interval: Option<u16>) -> i64 {
let unit = match freq {
ICalendarFrequency::Yearly => 366 * 86400,
ICalendarFrequency::Monthly => 31 * 86400,
ICalendarFrequency::Weekly => 7 * 86400,
ICalendarFrequency::Daily => 86400,
ICalendarFrequency::Hourly => 3600,
ICalendarFrequency::Minutely => 60,
ICalendarFrequency::Secondly => 1,
};
i64::from(count)
.saturating_mul(i64::from(interval.unwrap_or(1)))
.saturating_mul(unit)
}
fn expand_range(range: &mut Option<(i64, i64)>, value: &PartialDateTime) {
let Some(timestamp) = value.to_date_time().map(|result| {
naive_seconds(result.date_time)
- result
.offset
.map_or(0, |offset| i64::from(offset.seconds()))
}) else {
return;
};
match range {
Some((min, max)) => {
*min = (*min).min(timestamp);
*max = (*max).max(timestamp);
}
None => {
*range = Some((timestamp, timestamp));
}
}
}
fn naive_utc(timestamp: i64) -> civil::DateTime {
Timestamp::from_second(timestamp).map_or(civil::DateTime::default(), |timestamp| {
Offset::UTC.to_datetime(timestamp)
})
}
fn year_of(timestamp: i64) -> i32 {
i32::from(naive_utc(timestamp).year())
}
fn start_of_year(year: i32) -> i64 {
i16::try_from(year)
.ok()
.and_then(|year| civil::DateTime::new(year, 1, 1, 0, 0, 0, 0).ok())
.map_or(0, naive_seconds)
}
fn days_in_month(year: i32, month: u32) -> u32 {
i16::try_from(year)
.ok()
.zip(i8::try_from(month).ok())
.and_then(|(year, month)| Date::new(year, month, 1).ok())
.map_or(31, |date| u32::from(date.days_in_month().unsigned_abs()))
}
fn weekday_of(weekday: Weekday) -> ICalendarWeekday {
match weekday {
Weekday::Monday => ICalendarWeekday::Monday,
Weekday::Tuesday => ICalendarWeekday::Tuesday,
Weekday::Wednesday => ICalendarWeekday::Wednesday,
Weekday::Thursday => ICalendarWeekday::Thursday,
Weekday::Friday => ICalendarWeekday::Friday,
Weekday::Saturday => ICalendarWeekday::Saturday,
Weekday::Sunday => ICalendarWeekday::Sunday,
}
}
File diff suppressed because it is too large. Load diff
+583
View File
@@ -0,0 +1,583 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use super::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarDuration, ICalendarEntry,
ICalendarParameterName, ICalendarProperty, ICalendarRecurrenceRule, ICalendarStatus,
ICalendarTransparency, ICalendarValue, PARAMETER_VALUE_SIZE, Uri, VALUE_SIZE,
};
use crate::{
common::{IanaString, IanaType, PartialDateTime, timezone::NominalDuration},
icalendar::{ICalendarParameterValue, ICalendarValueType},
};
pub fn strip_mailto_scheme(value: &str) -> &str {
value
.split_once(':')
.filter(|(scheme, _)| scheme.eq_ignore_ascii_case("mailto"))
.map_or(value, |(_, address)| address)
}
impl ICalendar {
pub fn uids(&self) -> impl Iterator<Item = &str> {
self.components
.iter()
.filter(|component| {
component.component_type.is_calendar_object()
|| component.component_type == ICalendarComponentType::VCalendar
})
.filter_map(|component| component.uid())
}
pub fn size(&self) -> usize {
self.components
.iter()
.map(|component| component.size())
.sum()
}
pub fn component_by_id(&self, id: u32) -> Option<&ICalendarComponent> {
self.components.get(id as usize)
}
pub(crate) fn calendar_root(&self) -> Option<&ICalendarComponent> {
self.components
.first()
.filter(|root| root.component_type == ICalendarComponentType::VCalendar)
}
pub(crate) fn has_calendar_components(&self) -> bool {
self.calendar_root().is_some_and(|root| {
root.component_ids
.iter()
.any(|id| *id != 0 && self.component_by_id(*id).is_some())
})
}
pub fn alarms_for_id(&self, id: u32) -> impl Iterator<Item = &ICalendarComponent> {
self.component_by_id(id)
.map_or(&[][..], |c| c.component_ids.as_slice())
.iter()
.filter_map(|id| {
self.component_by_id(*id)
.filter(|c| c.component_type == ICalendarComponentType::VAlarm)
})
}
}
impl ICalendarComponent {
pub fn uid(&self) -> Option<&str> {
self.property(&ICalendarProperty::Uid)
.and_then(|e| e.values.first())
.and_then(|v| v.as_text())
}
pub fn jsid(&self) -> Option<&str> {
self.property(&ICalendarProperty::Jsid)
.and_then(|e| e.values.first())
.and_then(|v| v.as_text())
}
pub fn property(&self, prop: &ICalendarProperty) -> Option<&ICalendarEntry> {
self.entries.iter().find(|entry| &entry.name == prop)
}
pub fn property_mut(&mut self, prop: &ICalendarProperty) -> Option<&mut ICalendarEntry> {
self.entries.iter_mut().find(|entry| &entry.name == prop)
}
pub fn has_property(&self, prop: &ICalendarProperty) -> bool {
self.entries.iter().any(|entry| &entry.name == prop)
}
pub fn properties<'x, 'y: 'x>(
&'x self,
prop: &'y ICalendarProperty,
) -> impl Iterator<Item = &'x ICalendarEntry> + 'x {
self.entries.iter().filter(move |entry| &entry.name == prop)
}
pub fn properties_mut<'x, 'y: 'x>(
&'x mut self,
prop: &'y ICalendarProperty,
) -> impl Iterator<Item = &'x mut ICalendarEntry> + 'x {
self.entries
.iter_mut()
.filter(move |entry| &entry.name == prop)
}
pub fn size(&self) -> usize {
self.entries.iter().map(|entry| entry.size()).sum()
}
pub fn is_recurrent(&self) -> bool {
self.entries.iter().any(|entry| {
matches!(
entry.name,
ICalendarProperty::Rrule | ICalendarProperty::Rdate
)
})
}
pub fn is_recurrence_override(&self) -> bool {
self.entries
.iter()
.any(|entry| matches!(entry.name, ICalendarProperty::RecurrenceId))
}
pub fn is_recurrent_or_override(&self) -> bool {
self.entries.iter().any(|entry| {
matches!(
entry.name,
ICalendarProperty::Rrule
| ICalendarProperty::Rdate
| ICalendarProperty::RecurrenceId
)
})
}
pub fn status(&self) -> Option<&ICalendarStatus> {
self.entries
.iter()
.find_map(|entry| match (&entry.name, entry.values.first()) {
(ICalendarProperty::Status, Some(ICalendarValue::Status(status))) => Some(status),
_ => None,
})
}
pub fn transparency(&self) -> Option<&ICalendarTransparency> {
self.entries
.iter()
.find_map(|entry| match (&entry.name, entry.values.first()) {
(ICalendarProperty::Transp, Some(ICalendarValue::Transparency(trans))) => {
Some(trans)
}
_ => None,
})
}
}
impl ICalendarValue {
pub(crate) fn binary_bytes(&self) -> Option<&[u8]> {
match self {
ICalendarValue::Binary(data) => Some(data.as_slice()),
ICalendarValue::Uri(Uri::Data(data)) => Some(data.data.as_slice()),
_ => None,
}
.filter(|data| !data.is_empty())
}
pub fn size(&self) -> usize {
match self {
ICalendarValue::Binary(value) => value.len(),
ICalendarValue::Text(value) => value.len(),
ICalendarValue::PartialDateTime(_) => std::mem::size_of::<PartialDateTime>(),
ICalendarValue::RecurrenceRule(_) => std::mem::size_of::<ICalendarRecurrenceRule>(),
_ => VALUE_SIZE,
}
}
pub fn as_text(&self) -> Option<&str> {
match self {
ICalendarValue::Text(s) => Some(s.as_str()),
ICalendarValue::Uri(v) => v.as_str(),
ICalendarValue::CalendarScale(v) => Some(v.as_str()),
ICalendarValue::Method(v) => Some(v.as_str()),
ICalendarValue::Classification(v) => Some(v.as_str()),
ICalendarValue::Status(v) => Some(v.as_str()),
ICalendarValue::Transparency(v) => Some(v.as_str()),
ICalendarValue::Action(v) => Some(v.as_str()),
ICalendarValue::BusyType(v) => Some(v.as_str()),
ICalendarValue::ParticipantType(v) => Some(v.as_str()),
ICalendarValue::ResourceType(v) => Some(v.as_str()),
ICalendarValue::Proximity(v) => Some(v.as_str()),
_ => None,
}
}
pub fn into_text(self) -> Option<Cow<'static, str>> {
match self {
ICalendarValue::Text(s) => Some(Cow::Owned(s)),
ICalendarValue::Uri(v) => Some(Cow::Owned(v.into_unwrapped_string())),
ICalendarValue::CalendarScale(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarValue::Method(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarValue::Classification(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarValue::Status(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarValue::Transparency(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarValue::Action(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarValue::BusyType(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarValue::ParticipantType(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarValue::ResourceType(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarValue::Proximity(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarValue::Integer(i) => Some(Cow::Owned(i.to_string())),
ICalendarValue::Float(f) => Some(Cow::Owned(f.to_string())),
ICalendarValue::Boolean(b) => Some(Cow::Borrowed(if b { "TRUE" } else { "FALSE" })),
ICalendarValue::PartialDateTime(dt) => dt.to_rfc3339().map(Cow::Owned),
ICalendarValue::RecurrenceRule(rrule) => Some(Cow::Owned(rrule.to_string())),
ICalendarValue::Binary(value) => String::from_utf8(value).ok().map(Cow::Owned),
ICalendarValue::Duration(value) => Some(Cow::Owned(value.to_string())),
ICalendarValue::Period(value) => Some(Cow::Owned(value.to_string())),
}
}
pub fn into_partial_date_time(self) -> Option<PartialDateTime> {
match self {
ICalendarValue::PartialDateTime(dt) => Some(dt),
_ => None,
}
}
pub fn as_integer(&self) -> Option<i64> {
match self {
ICalendarValue::Integer(i) => Some(*i),
_ => None,
}
}
pub fn as_float(&self) -> Option<f64> {
match self {
ICalendarValue::Float(f) => Some(*f),
_ => None,
}
}
pub fn as_boolean(&self) -> Option<bool> {
match self {
ICalendarValue::Boolean(b) => Some(*b),
_ => None,
}
}
pub fn as_partial_date_time(&self) -> Option<&PartialDateTime> {
match self {
ICalendarValue::PartialDateTime(dt) => Some(dt),
_ => None,
}
}
pub fn as_binary(&self) -> Option<&[u8]> {
match self {
ICalendarValue::Binary(d) => Some(d.as_slice()),
_ => None,
}
}
}
impl ICalendarEntry {
#[inline]
pub fn parameters(
&self,
prop: &ICalendarParameterName,
) -> impl Iterator<Item = &ICalendarParameterValue> {
self.params.iter().filter_map(move |param| {
if &param.name == prop {
Some(&param.value)
} else {
None
}
})
}
#[inline]
pub fn parameter(&self, prop: &ICalendarParameterName) -> Option<&ICalendarParameterValue> {
self.params.iter().find_map(move |param| {
if &param.name == prop {
Some(&param.value)
} else {
None
}
})
}
#[inline]
pub fn has_parameter(&self, prop: &ICalendarParameterName) -> bool {
self.params.iter().any(|param| &param.name == prop)
}
pub fn jsid(&self) -> Option<&str> {
self.parameter(&ICalendarParameterName::Jsid)
.and_then(|v| v.as_text())
}
pub fn is_derived(&self) -> bool {
self.parameter(&ICalendarParameterName::Derived)
.is_some_and(|v| matches!(v, ICalendarParameterValue::Bool(true)))
}
pub fn calendar_address(&self) -> Option<&str> {
self.values
.first()
.and_then(|v| v.as_text())
.map(strip_mailto_scheme)
}
pub fn size(&self) -> usize {
self.values.iter().map(|value| value.size()).sum::<usize>()
+ self
.params
.iter()
.map(|param| std::mem::size_of::<ICalendarParameterName>() + param.value.size())
.sum::<usize>()
+ self.name.as_str().len()
}
}
impl ICalendarParameterValue {
pub fn as_text(&self) -> Option<&str> {
match self {
ICalendarParameterValue::Text(v) => Some(v.as_str()),
ICalendarParameterValue::Bool(v) => Some(if *v { "TRUE" } else { "FALSE" }),
ICalendarParameterValue::Uri(uri) => uri.as_str(),
ICalendarParameterValue::Cutype(v) => Some(v.as_str()),
ICalendarParameterValue::Fbtype(v) => Some(v.as_str()),
ICalendarParameterValue::Partstat(v) => Some(v.as_str()),
ICalendarParameterValue::Related(v) => Some(v.as_str()),
ICalendarParameterValue::Reltype(v) => Some(v.as_str()),
ICalendarParameterValue::Role(v) => Some(v.as_str()),
ICalendarParameterValue::ScheduleAgent(v) => Some(v.as_str()),
ICalendarParameterValue::ScheduleForceSend(v) => Some(v.as_str()),
ICalendarParameterValue::Value(v) => Some(v.as_str()),
ICalendarParameterValue::Display(v) => Some(v.as_str()),
ICalendarParameterValue::Feature(v) => Some(v.as_str()),
ICalendarParameterValue::Linkrel(v) => Some(v.as_str()),
ICalendarParameterValue::Duration(_)
| ICalendarParameterValue::Integer(_)
| ICalendarParameterValue::Null => None,
}
}
pub fn into_text(self) -> Option<Cow<'static, str>> {
match self {
ICalendarParameterValue::Text(v) => Some(Cow::Owned(v)),
ICalendarParameterValue::Bool(v) => {
Some(Cow::Borrowed(if v { "TRUE" } else { "FALSE" }))
}
ICalendarParameterValue::Uri(uri) => Some(Cow::Owned(uri.into_unwrapped_string())),
ICalendarParameterValue::Cutype(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarParameterValue::Fbtype(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarParameterValue::Partstat(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarParameterValue::Related(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarParameterValue::Reltype(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarParameterValue::Role(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarParameterValue::ScheduleAgent(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarParameterValue::ScheduleForceSend(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarParameterValue::Value(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarParameterValue::Display(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarParameterValue::Feature(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarParameterValue::Linkrel(v) => Some(Cow::Borrowed(v.as_str())),
ICalendarParameterValue::Duration(v) => Some(Cow::Owned(v.to_string())),
ICalendarParameterValue::Integer(v) => Some(Cow::Owned(v.to_string())),
ICalendarParameterValue::Null => None,
}
}
pub fn into_value_type(self) -> Option<IanaType<ICalendarValueType, String>> {
match self {
ICalendarParameterValue::Value(v) => Some(IanaType::Iana(v)),
ICalendarParameterValue::Text(v) => Some(IanaType::Other(v)),
_ => None,
}
}
pub fn as_integer(&self) -> Option<u64> {
match self {
ICalendarParameterValue::Integer(v) => Some(*v),
ICalendarParameterValue::Text(v) => v.parse().ok(),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
ICalendarParameterValue::Bool(v) => Some(*v),
_ => None,
}
}
pub fn size(&self) -> usize {
match self {
ICalendarParameterValue::Text(v) => v.len(),
ICalendarParameterValue::Uri(v) => v.size(),
_ => PARAMETER_VALUE_SIZE,
}
}
}
impl ICalendarComponentType {
pub fn is_calendar_object(&self) -> bool {
matches!(
self,
ICalendarComponentType::VEvent
| ICalendarComponentType::VTodo
| ICalendarComponentType::VJournal
| ICalendarComponentType::VFreebusy
| ICalendarComponentType::VAvailability
)
}
pub(crate) fn converts_links(&self) -> bool {
matches!(
self,
ICalendarComponentType::VEvent
| ICalendarComponentType::VTodo
| ICalendarComponentType::Participant
| ICalendarComponentType::VLocation
| ICalendarComponentType::VCalendar
)
}
pub fn has_time_ranges(&self) -> bool {
matches!(
self,
ICalendarComponentType::VEvent
| ICalendarComponentType::VTodo
| ICalendarComponentType::VJournal
| ICalendarComponentType::VFreebusy
)
}
pub fn is_scheduling_object(&self) -> bool {
matches!(
self,
ICalendarComponentType::VEvent
| ICalendarComponentType::VTodo
| ICalendarComponentType::VJournal
| ICalendarComponentType::VFreebusy
)
}
pub fn is_event(&self) -> bool {
matches!(self, ICalendarComponentType::VEvent)
}
pub fn is_todo(&self) -> bool {
matches!(self, ICalendarComponentType::VTodo)
}
pub fn is_event_or_todo(&self) -> bool {
matches!(
self,
ICalendarComponentType::VEvent | ICalendarComponentType::VTodo
)
}
pub fn is_journal(&self) -> bool {
matches!(self, ICalendarComponentType::VJournal)
}
pub fn is_freebusy(&self) -> bool {
matches!(self, ICalendarComponentType::VFreebusy)
}
pub fn is_location(&self) -> bool {
matches!(self, ICalendarComponentType::VLocation)
}
pub fn is_alarm(&self) -> bool {
matches!(self, ICalendarComponentType::VAlarm)
}
pub fn is_participant(&self) -> bool {
matches!(self, ICalendarComponentType::Participant)
}
}
impl Uri {
pub fn as_str(&self) -> Option<&str> {
match self {
Uri::Data(data) => data.content_type.as_deref(),
Uri::Location(loc) => loc.as_str().into(),
}
}
pub fn size(&self) -> usize {
match self {
Uri::Data(data) => {
data.data.len() + data.content_type.as_ref().map(|s| s.len()).unwrap_or(0)
}
Uri::Location(loc) => loc.len(),
}
}
}
impl ICalendarDuration {
pub fn from_seconds(seconds: i64) -> Self {
let mut secs = seconds;
let neg = secs < 0;
if neg {
secs = -secs;
}
let weeks = (secs / 604800) as u32;
secs %= 604800;
let days = (secs / 86400) as u32;
secs %= 86400;
let hours = (secs / 3600) as u32;
secs %= 3600;
let minutes = (secs / 60) as u32;
secs %= 60;
let seconds = secs as u32;
Self {
weeks,
days,
hours,
minutes,
seconds,
neg,
}
}
pub fn from_days(days: i64) -> Self {
let neg = days < 0;
let days = days.unsigned_abs();
let (weeks, days) = if days.is_multiple_of(7) {
(days / 7, 0)
} else {
(0, days)
};
Self {
weeks: weeks as u32,
days: days as u32,
hours: 0,
minutes: 0,
seconds: 0,
neg,
}
}
/// Returns this duration as a calendar span.
pub fn to_nominal(&self) -> Option<NominalDuration> {
let days = i32::try_from(i64::from(self.weeks) * 7 + i64::from(self.days)).ok()?;
let seconds =
i64::from(self.hours) * 3600 + i64::from(self.minutes) * 60 + i64::from(self.seconds);
Some(if self.neg {
NominalDuration::new(-days, -seconds)
} else {
NominalDuration::new(days, seconds)
})
}
pub fn as_seconds(&self) -> i64 {
let secs = self.seconds as i64
+ self.minutes as i64 * 60
+ self.hours as i64 * 3600
+ self.days as i64 * 86400
+ self.weeks as i64 * 604800;
if self.neg { -secs } else { secs }
}
pub fn is_empty(&self) -> bool {
self.weeks == 0
&& self.days == 0
&& self.hours == 0
&& self.minutes == 0
&& self.seconds == 0
}
}
+524
View File
@@ -0,0 +1,524 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{
ICalendar, ICalendarDay, ICalendarDuration, ICalendarEntry, ICalendarPeriod,
ICalendarRecurrenceRule, ICalendarValueType,
};
use crate::{
common::{
CalendarScale, IanaString, PartialDateTime,
format::{AsciiPush, BufferedWrite, DurationParts},
stack::ComponentStack,
writer::{
DOCUMENT_BUFFER, ENTRY_BUFFER, FoldingWriter, LineWriter, VisitedComponents,
write_param_text, write_param_value, write_quoted_param_value, write_text, write_uri,
},
},
icalendar::{
ICalendarMonth, ICalendarParameterName, ICalendarParameterValue, ICalendarProperty,
ICalendarValue, Uri, ValueSeparator,
},
};
use std::{
fmt::{self, Display, Write},
slice::Iter,
};
pub(crate) const RRULE_BUFFER: usize = 256;
pub(crate) const DURATION_BUFFER: usize = 64;
pub(crate) const DATE_BUFFER: usize = 32;
pub(crate) const PERIOD_BUFFER: usize = 96;
impl ICalendar {
pub fn write_to(&self, out: &mut impl Write) -> fmt::Result {
out.write_buffered::<DOCUMENT_BUFFER>(|buf| {
self.write_components(&mut FoldingWriter::new(buf))
})
}
fn write_components<W: Write + AsciiPush + ?Sized>(
&self,
out: &mut FoldingWriter<'_, W>,
) -> fmt::Result {
let mut component_iter: Iter<'_, u32> = [0].iter();
let mut component_stack = ComponentStack::new();
let mut visited = VisitedComponents::new(self.components.len());
loop {
if let Some(component_id) = component_iter.next() {
let component_id = *component_id as usize;
let Some(component) = self
.components
.get(component_id)
.filter(|_| visited.insert(component_id))
else {
continue;
};
out.write_boundary("BEGIN:", component.component_type.as_str())?;
for entry in &component.entries {
if !matches!(
entry.name,
ICalendarProperty::Begin | ICalendarProperty::End
) {
entry.write_line(out, true)?;
}
}
if !component.component_ids.is_empty() {
component_stack.push((component, component_iter));
component_iter = component.component_ids.iter();
} else {
out.write_boundary("END:", component.component_type.as_str())?;
}
} else if let Some((component, iter)) = component_stack.pop() {
out.write_boundary("END:", component.component_type.as_str())?;
component_iter = iter;
} else {
break;
}
}
Ok(())
}
}
impl ICalendarEntry {
pub fn write_to(&self, out: &mut impl Write) -> fmt::Result {
self.write_with_value(out, true)
}
pub fn write_with_value(&self, out: &mut impl Write, with_value: bool) -> fmt::Result {
out.write_buffered::<ENTRY_BUFFER>(|buf| {
self.write_line(&mut FoldingWriter::new(buf), with_value)
})
}
pub(crate) fn write_line<W: Write + AsciiPush + ?Sized>(
&self,
out: &mut FoldingWriter<'_, W>,
with_value: bool,
) -> fmt::Result {
out.write_atomic(self.name.as_str())?;
if matches!(self.values.first(), Some(ICalendarValue::Binary(_))) {
out.write_atomic(";ENCODING=BASE64")?;
if !self
.params
.iter()
.any(|param| param.name == ICalendarParameterName::Value)
{
out.write_atomic(";VALUE=BINARY")?;
}
}
let mut types = None;
let mut last_param: Option<&ICalendarParameterName> = None;
for param in &self.params {
if last_param.is_some_and(|last_param| last_param == &param.name) {
out.write_atomic(",")?;
} else {
out.write_atomic(";")?;
out.write_atomic(param.name.as_str())?;
if !matches!(param.value, ICalendarParameterValue::Null) {
out.write_atomic("=")?;
}
last_param = Some(&param.name);
}
match &param.value {
ICalendarParameterValue::Text(v)
if matches!(param.name, ICalendarParameterName::Jsptr) =>
{
write_quoted_param_value(out, v, true)?;
}
ICalendarParameterValue::Text(v) => {
write_param_value(out, v, true)?;
}
ICalendarParameterValue::Integer(i) => {
out.push_u64(*i)?;
}
ICalendarParameterValue::Bool(v) => {
let v = if !matches!(param.name, ICalendarParameterName::Range) {
if *v { "TRUE" } else { "FALSE" }
} else {
"THISANDFUTURE"
};
out.write_atomic(v)?;
}
ICalendarParameterValue::Uri(uri) => {
out.write_atomic("\"")?;
uri.write_param_text(out)?;
out.write_atomic("\"")?;
}
ICalendarParameterValue::Cutype(v) => {
write_param_value(out, v.as_str(), true)?;
}
ICalendarParameterValue::Fbtype(v) => {
write_param_value(out, v.as_str(), true)?;
}
ICalendarParameterValue::Partstat(v) => {
write_param_value(out, v.as_str(), true)?;
}
ICalendarParameterValue::Related(v) => {
write_param_value(out, v.as_str(), true)?;
}
ICalendarParameterValue::Reltype(v) => {
write_param_value(out, v.as_str(), true)?;
}
ICalendarParameterValue::Role(v) => {
write_param_value(out, v.as_str(), true)?;
}
ICalendarParameterValue::ScheduleAgent(v) => {
write_param_value(out, v.as_str(), true)?;
}
ICalendarParameterValue::ScheduleForceSend(v) => {
write_param_value(out, v.as_str(), true)?;
}
ICalendarParameterValue::Value(v) => {
types = Some(v);
write_param_value(out, v.as_str(), true)?;
}
ICalendarParameterValue::Display(v) => {
write_param_value(out, v.as_str(), true)?;
}
ICalendarParameterValue::Feature(v) => {
write_param_value(out, v.as_str(), true)?;
}
ICalendarParameterValue::Duration(v) => {
out.push_duration(v.parts())?;
}
ICalendarParameterValue::Linkrel(v) => {
write_param_value(out, v.as_str(), true)?;
}
ICalendarParameterValue::Null => {
last_param = None;
}
}
}
out.write_atomic(":")?;
if !with_value {
return out.end_line();
}
let (default_type, separator) = self.name.default_types();
let separator = if !matches!(separator, ValueSeparator::Comma) {
";"
} else {
","
};
let default_type = default_type.unwrap_ical();
for (pos, value) in self.values.iter().enumerate() {
if pos > 0 {
out.write_atomic(separator)?;
}
let text = match value {
ICalendarValue::Binary(v) => {
out.write_base64(v)?;
continue;
}
ICalendarValue::Boolean(v) => {
out.write_atomic(if *v { "TRUE" } else { "FALSE" })?;
continue;
}
ICalendarValue::Uri(v) => {
v.write_value(out)?;
continue;
}
ICalendarValue::PartialDateTime(v) => {
v.push_ical(out, types.unwrap_or(&default_type))?;
continue;
}
ICalendarValue::Duration(v) => {
out.push_duration(v.parts())?;
continue;
}
ICalendarValue::RecurrenceRule(v) => {
v.push_to(out)?;
continue;
}
ICalendarValue::Period(v) => {
v.push_to(out)?;
continue;
}
ICalendarValue::Float(v) => {
write!(out, "{v}")?;
continue;
}
ICalendarValue::Integer(v) => {
out.push_i64(*v)?;
continue;
}
ICalendarValue::Text(v) => {
match types.unwrap_or(&default_type) {
ICalendarValueType::Uri | ICalendarValueType::CalAddress => {
write_uri(out, v)?;
}
ICalendarValueType::Recur => write_text(out, v, false, false)?,
_ => write_text(out, v, true, true)?,
}
continue;
}
ICalendarValue::CalendarScale(v) => v.as_str(),
ICalendarValue::Method(v) => v.as_str(),
ICalendarValue::Classification(v) => v.as_str(),
ICalendarValue::Status(v) => v.as_str(),
ICalendarValue::Transparency(v) => v.as_str(),
ICalendarValue::Action(v) => v.as_str(),
ICalendarValue::BusyType(v) => v.as_str(),
ICalendarValue::ParticipantType(v) => v.as_str(),
ICalendarValue::ResourceType(v) => v.as_str(),
ICalendarValue::Proximity(v) => v.as_str(),
};
out.write_atomic(text)?;
}
out.end_line()
}
}
impl Uri {
fn write_value(&self, out: &mut impl LineWriter) -> fmt::Result {
match self {
Uri::Data(v) => {
out.write_str("data:")?;
out.write_str(v.content_type.as_deref().unwrap_or_default())?;
out.write_str(";")?;
out.write_atomic("base64,")?;
out.write_base64(&v.data)
}
Uri::Location(v) => write_uri(out, v),
}
}
fn write_param_text(&self, out: &mut impl LineWriter) -> fmt::Result {
match self {
Uri::Data(v) => {
out.write_str("data:")?;
write_param_text(out, v.content_type.as_deref().unwrap_or_default(), true)?;
out.write_str(";")?;
out.write_atomic("base64,")?;
out.write_base64(&v.data)
}
Uri::Location(v) => write_param_text(out, v, true),
}
}
}
impl Uri {
pub fn to_unwrapped_string(&self) -> String {
match self {
Uri::Data(v) => v.to_unwrapped_string(),
Uri::Location(v) => v.to_string(),
}
}
pub fn into_unwrapped_string(self) -> String {
match self {
Uri::Data(v) => v.to_unwrapped_string(),
Uri::Location(v) => v,
}
}
}
impl ICalendarRecurrenceRule {
pub(crate) fn push_to(&self, buf: &mut impl AsciiPush) -> fmt::Result {
buf.push_ascii_str("FREQ=")?;
buf.push_ascii_str(self.freq.as_str())?;
if let Some(until) = &self.until {
buf.push_ascii_str(";UNTIL=")?;
until.push_ical(
buf,
if until.has_date_and_time() {
&ICalendarValueType::DateTime
} else {
&ICalendarValueType::Date
},
)?;
}
if let Some(count) = self.count.filter(|c| *c > 0) {
buf.push_ascii_str(";COUNT=")?;
buf.push_u64(count.into())?;
}
if let Some(interval) = self.interval {
buf.push_ascii_str(";INTERVAL=")?;
buf.push_u64(interval.into())?;
}
buf.push_list(";BYSECOND=", &self.bysecond, |buf, item| {
buf.push_u64((*item).into())
})?;
buf.push_list(";BYMINUTE=", &self.byminute, |buf, item| {
buf.push_u64((*item).into())
})?;
buf.push_list(";BYHOUR=", &self.byhour, |buf, item| {
buf.push_u64((*item).into())
})?;
buf.push_list(";BYDAY=", &self.byday, |buf, item| item.push_to(buf))?;
buf.push_list(";BYMONTHDAY=", &self.bymonthday, |buf, item| {
buf.push_i64((*item).into())
})?;
buf.push_list(";BYYEARDAY=", &self.byyearday, |buf, item| {
buf.push_i64((*item).into())
})?;
buf.push_list(";BYWEEKNO=", &self.byweekno, |buf, item| {
buf.push_i64((*item).into())
})?;
buf.push_list(";BYMONTH=", &self.bymonth, |buf, item| item.push_to(buf))?;
buf.push_list(";BYSETPOS=", &self.bysetpos, |buf, item| {
buf.push_i64((*item).into())
})?;
if let Some(wkst) = self.wkst {
buf.push_ascii_str(";WKST=")?;
buf.push_ascii_str(wkst.as_str())?;
}
if let Some(rscale) = &self.rscale {
buf.push_ascii_str(";RSCALE=")?;
buf.push_ascii_str(rscale.as_str())?;
} else if self.skip.is_some() {
buf.push_ascii_str(";RSCALE=")?;
buf.push_ascii_str(CalendarScale::Gregorian.as_str())?;
}
if let Some(skip) = &self.skip {
buf.push_ascii_str(";SKIP=")?;
buf.push_ascii_str(skip.as_str())?;
}
Ok(())
}
}
impl Display for ICalendarRecurrenceRule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_buffered::<RRULE_BUFFER>(|buf| self.push_to(buf))
}
}
impl ICalendarDay {
fn push_to(&self, buf: &mut impl AsciiPush) -> fmt::Result {
if let Some(ordwk) = self.ordwk {
buf.push_i64(ordwk.into())?;
}
buf.push_ascii_str(self.weekday.as_str())
}
}
impl Display for ICalendarDay {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_buffered::<DATE_BUFFER>(|buf| self.push_to(buf))
}
}
impl ICalendarMonth {
fn push_to(&self, buf: &mut impl AsciiPush) -> fmt::Result {
buf.push_u64(self.month().into())?;
if self.is_leap() {
buf.push_byte(b'L')?;
}
Ok(())
}
}
impl Display for ICalendarMonth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_buffered::<DATE_BUFFER>(|buf| self.push_to(buf))
}
}
impl ICalendarPeriod {
fn push_to(&self, buf: &mut impl AsciiPush) -> fmt::Result {
match self {
ICalendarPeriod::Range { start, end } => {
start.push_ical(buf, &ICalendarValueType::DateTime)?;
buf.push_byte(b'/')?;
end.push_ical(buf, &ICalendarValueType::DateTime)
}
ICalendarPeriod::Duration { start, duration } => {
start.push_ical(buf, &ICalendarValueType::DateTime)?;
buf.push_byte(b'/')?;
buf.push_duration(duration.parts())
}
}
}
}
impl Display for ICalendarPeriod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_buffered::<PERIOD_BUFFER>(|buf| self.push_to(buf))
}
}
impl ICalendarDuration {
pub(crate) fn parts(&self) -> DurationParts {
DurationParts {
neg: self.neg,
weeks: self.weeks,
days: self.days,
hours: self.hours,
minutes: self.minutes,
seconds: self.seconds,
}
}
}
impl Display for ICalendarDuration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_buffered::<DURATION_BUFFER>(|buf| buf.push_duration(self.parts()))
}
}
impl PartialDateTime {
pub fn format_as_ical(&self, out: &mut impl Write, fmt: &ICalendarValueType) -> fmt::Result {
out.write_buffered::<DATE_BUFFER>(|buf| self.push_ical(buf, fmt))
}
pub(crate) fn push_ical(
&self,
buf: &mut impl AsciiPush,
fmt: &ICalendarValueType,
) -> fmt::Result {
if matches!(fmt, ICalendarValueType::Date | ICalendarValueType::DateTime) {
buf.push_4_digits(self.year.unwrap_or_default())?;
buf.push_2_digits(self.month.unwrap_or_default())?;
buf.push_2_digits(self.day.unwrap_or_default())?;
}
if matches!(fmt, ICalendarValueType::DateTime) {
buf.push_byte(b'T')?;
}
if matches!(fmt, ICalendarValueType::DateTime | ICalendarValueType::Time) {
buf.push_2_digits(self.hour.unwrap_or_default())?;
buf.push_2_digits(self.minute.unwrap_or_default())?;
buf.push_2_digits(self.second.unwrap_or_default())?;
if matches!((self.tz_hour, self.tz_minute), (Some(0), Some(0))) {
buf.push_byte(b'Z')?;
}
}
if matches!(fmt, ICalendarValueType::UtcOffset) {
buf.push_byte(if self.tz_minus { b'-' } else { b'+' })?;
buf.push_2_digits(self.tz_hour.unwrap_or_default())?;
buf.push_2_digits(self.tz_minute.unwrap_or_default())?;
}
Ok(())
}
}
impl Display for ICalendar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.write_to(f)
}
}
File diff suppressed because it is too large. Load diff
+211
View File
@@ -0,0 +1,211 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::common::timezone::ZonedDateTime;
use crate::{
common::{
blob::{BlobOptions, BlobResolver, BlobResolverFn, NoBlobIds, ResolvedBlobs},
export::{EmbeddedBudget, ExportError, RejectedPatch},
timezone::Tz,
},
datecalc::MAX_UNPRODUCTIVE_WORK,
icalendar::{ICalendar, ICalendarComponentType},
jscalendar::{JSCalendarId, JSCalendarProperty, JSCalendarValue},
};
use jmap_tools::{Key, Map, Value};
use std::hash::Hash;
pub mod convert;
pub mod params;
pub mod props;
struct State<'x, I: JSCalendarId, B: JSCalendarId> {
entries: Map<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
default_component_type: ICalendarComponentType,
uid: Option<&'x str>,
tz: Option<Tz>,
tz_end: Option<Tz>,
tz_rid: Option<Tz>,
start: Option<ZonedDateTime>,
recurrence_id: Option<ZonedDateTime>,
is_date: bool,
}
#[allow(clippy::type_complexity)]
struct ConvertedComponent<'x, I: JSCalendarId, B: JSCalendarId> {
pub(super) name: ICalendarComponentType,
pub(super) converted_props: Vec<(
Vec<Key<'x, JSCalendarProperty<I>>>,
Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
)>,
pub(super) converted_props_count: usize,
pub(super) properties: Vec<Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>>,
pub(super) components: Vec<Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>>,
}
#[derive(Debug, Clone, Copy)]
pub struct ExportOptions<R = NoBlobIds> {
max_expansions: usize,
blobs: BlobOptions<R>,
}
pub(super) struct ExportContext<'a, B> {
max_expansions: usize,
unproductive_budget: usize,
blobs: Option<ResolvedBlobs<'a, B>>,
budget: EmbeddedBudget,
error: Option<ExportError>,
rejected_patches: Vec<RejectedPatch>,
}
const DEFAULT_MAX_EXPANSIONS: usize = 3000;
const ENTRY_CAPACITY_EXTRA: usize = 1;
impl Default for ExportOptions {
fn default() -> Self {
Self {
max_expansions: DEFAULT_MAX_EXPANSIONS,
blobs: BlobOptions::default(),
}
}
}
impl ExportOptions {
pub fn new() -> Self {
Self::default()
}
}
impl<R> ExportOptions<R> {
pub fn max_expansions(mut self, max_expansions: usize) -> Self {
self.max_expansions = max_expansions;
self
}
pub fn with_blob_resolver<B, F>(self, blob_resolver: F) -> ExportOptions<BlobResolverFn<F>>
where
F: FnMut(&B) -> Option<Vec<u8>>,
{
self.with_resolver(BlobResolverFn(blob_resolver))
}
pub fn with_resolver<T>(self, blob_resolver: T) -> ExportOptions<T> {
ExportOptions {
max_expansions: self.max_expansions,
blobs: self.blobs.with_handler(blob_resolver),
}
}
pub fn max_embedded_size(mut self, max_embedded_size: usize) -> Self {
self.blobs = self.blobs.with_max_embedded_size(max_embedded_size);
self
}
pub(super) fn context<B: Clone + Eq + Hash>(&mut self) -> ExportContext<'_, B>
where
R: BlobResolver<B>,
{
ExportContext {
max_expansions: self.max_expansions,
unproductive_budget: MAX_UNPRODUCTIVE_WORK,
budget: self.blobs.budget(),
blobs: self.blobs.resolved_blobs(),
error: None,
rejected_patches: Vec::new(),
}
}
}
impl<I: JSCalendarId, B: JSCalendarId> State<'_, I, B> {
fn entry_capacity(&self) -> usize {
self.entries
.iter()
.map(|(key, value)| match (key, value) {
(
Key::Property(
JSCalendarProperty::Participants
| JSCalendarProperty::Links
| JSCalendarProperty::Locations
| JSCalendarProperty::VirtualLocations
| JSCalendarProperty::Categories
| JSCalendarProperty::RelatedTo
| JSCalendarProperty::RecurrenceOverrides,
),
Value::Object(members),
) => members.len(),
(Key::Property(JSCalendarProperty::ICalendar), Value::Object(ical)) => ical
.iter()
.filter_map(|(key, value)| match (key, value) {
(Key::Property(JSCalendarProperty::Properties), Value::Array(items)) => {
Some(items.len())
}
_ => None,
})
.sum(),
(
Key::Property(
JSCalendarProperty::Type
| JSCalendarProperty::Alerts
| JSCalendarProperty::Entries
| JSCalendarProperty::TimeZone
| JSCalendarProperty::EndTimeZone
| JSCalendarProperty::RecurrenceIdTimeZone
| JSCalendarProperty::Locale
| JSCalendarProperty::MainLocationId
| JSCalendarProperty::DescriptionContentType
| JSCalendarProperty::SentBy
| JSCalendarProperty::Version
| JSCalendarProperty::Excluded
| JSCalendarProperty::Method
| JSCalendarProperty::Metadata
| JSCalendarProperty::PrivateMetadata,
),
_,
) => 0,
_ => 1,
})
.sum::<usize>()
+ ENTRY_CAPACITY_EXTRA
}
}
impl<B> ExportContext<'_, B> {
fn fail(&mut self, error: ExportError) {
if self.error.is_none() {
self.error = Some(error);
}
}
fn has_failed(&self) -> bool {
self.error.is_some()
}
fn reject_patch(&mut self, recurrence_id: String, pointer: String) {
self.rejected_patches.push(RejectedPatch {
recurrence_id,
pointer,
});
}
fn embed(&mut self, data: &[u8]) -> bool {
match self.budget.take(data) {
Err(error) => {
self.blobs = None;
self.fail(error);
false
}
Ok(()) => true,
}
}
fn into_result(self, ical: ICalendar) -> Result<(ICalendar, Vec<RejectedPatch>), ExportError> {
match self.error {
Some(error) => Err(error),
None if ical.has_calendar_components() => Ok((ical, self.rejected_patches)),
None => Err(ExportError::NoComponents),
}
}
}
@@ -0,0 +1,441 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{
IanaParse, LinkRelation, PartialDateTime,
parser::Boolean,
timezone::{Tz, ZonedDateTime},
},
icalendar::*,
jscalendar::{
JSCalendarDateTime, JSCalendarId, JSCalendarProperty, JSCalendarValue,
export::ConvertedComponent, ext::JSCalendarKeyExt,
},
jscontact::export::params::ParamValue,
};
use jiff::{Timestamp, civil, tz::Offset};
use jmap_tools::{Key, Value};
use std::str::FromStr;
impl ICalendarEntry {
pub(super) fn import_converted<I: JSCalendarId, B: JSCalendarId>(
self,
path: &[JSCalendarProperty<I>],
conversions: &mut Option<ConvertedComponent<'_, I, B>>,
) -> Self {
self.import_converted_with_id(path, conversions, None)
}
pub(super) fn import_converted_with_id<I: JSCalendarId, B: JSCalendarId>(
mut self,
path: &[JSCalendarProperty<I>],
conversions: &mut Option<ConvertedComponent<'_, I, B>>,
id: Option<&str>,
) -> Self {
let Some(conversions) = conversions
.as_mut()
.filter(|c| c.converted_props_count < c.converted_props.len())
else {
return self;
};
// Obtain jsid
let value = self.values.first().and_then(|v| v.as_text());
let jsid = if matches!(self.name, ICalendarProperty::RelatedTo) {
value
} else {
id.or_else(|| self.jsid())
};
let mut matched_once = false;
'outer: for (keys, value) in conversions.converted_props.iter_mut() {
if matches!(value, Value::Null) {
continue;
}
for (pos, item) in path.iter().enumerate() {
if !keys
.iter()
.any(|k| matches!(k, Key::Property(p) if p == item))
{
if pos == 0 && matched_once {
// Array is sorted, so if we didn't match the first item,
// we won't match any further.
break 'outer;
} else {
continue 'outer;
}
} else {
matched_once = true;
}
}
if jsid
.map(Key::Borrowed)
.is_none_or(|prop_id| keys.iter().any(|k| k.same_key(&prop_id)))
{
self.import_converted_properties(std::mem::take(value));
conversions.converted_props_count += 1;
break;
}
}
// Update binary contents
if self
.parameter(&ICalendarParameterName::Value)
.is_some_and(|v| {
matches!(
v,
ICalendarParameterValue::Value(ICalendarValueType::Binary)
)
})
&& let Some(ICalendarValue::Uri(Uri::Data(data))) = self.values.first_mut()
{
let bin = ICalendarValue::Binary(std::mem::take(&mut data.data));
if let Some(content_type) = data.content_type.take()
&& !self.has_parameter(&ICalendarParameterName::Fmttype)
{
self.params.push(ICalendarParameter::fmttype(content_type));
}
self.values[0] = bin;
}
self
}
pub(super) fn import_converted_properties<I: JSCalendarId, B: JSCalendarId>(
&mut self,
props: Value<'_, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) {
for (key, value) in props.into_expanded_object() {
match key {
Key::Property(JSCalendarProperty::Name) => {
if let Some(name) = value.into_string() {
self.name = ICalendarProperty::parse(name.as_bytes())
.unwrap_or(ICalendarProperty::Other(name.to_ascii_uppercase()));
}
}
Key::Property(JSCalendarProperty::Parameters) => {
self.import_jcal_params(value);
}
_ => {}
}
}
}
pub(super) fn import_jcal_params<I: JSCalendarId, B: JSCalendarId>(
&mut self,
params: Value<'_, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) {
for (key, value) in params.into_expanded_object() {
let (first, rest) = match value {
Value::Array(values) => (None, values),
value => (Some(value), Vec::new()),
};
let mut values = first
.into_iter()
.chain(rest)
.filter_map(ParamValue::try_from_value)
.peekable();
if values.peek().is_none() {
continue;
}
let key = key.to_string();
let Some(param) = ICalendarParameterName::try_parse(key.as_bytes()) else {
let key = key.into_owned();
for value in values {
self.params.push(ICalendarParameter {
name: ICalendarParameterName::Other(key.to_ascii_uppercase()),
value: value.into_string().into_owned().into(),
});
}
continue;
};
for value in values {
let value = match &param {
ICalendarParameterName::Value => {
let value = value.into_string();
ICalendarValueType::parse(value.as_bytes())
.map(ICalendarParameterValue::Value)
.unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned()))
}
ICalendarParameterName::Size | ICalendarParameterName::Order => {
match value.into_number() {
Ok(n) => ICalendarParameterValue::Integer(n.unsigned_abs()),
Err(value) => {
ICalendarParameterValue::Text(value.into_string().into_owned())
}
}
}
ICalendarParameterName::Rsvp | ICalendarParameterName::Derived => {
let value = value.into_string();
Boolean::parse(value.as_bytes())
.map(|v| ICalendarParameterValue::Bool(v.0))
.unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned()))
}
ICalendarParameterName::Altrep
| ICalendarParameterName::DelegatedFrom
| ICalendarParameterName::DelegatedTo
| ICalendarParameterName::Dir
| ICalendarParameterName::Member
| ICalendarParameterName::SentBy
| ICalendarParameterName::Schema => {
ICalendarParameterValue::Uri(Uri::parse(value.into_string().into_owned()))
}
ICalendarParameterName::Range => {
let value = value.into_string();
if value.eq_ignore_ascii_case("THISANDFUTURE") {
ICalendarParameterValue::Bool(true)
} else {
ICalendarParameterValue::Text(value.into_owned())
}
}
ICalendarParameterName::Gap => {
let value = value.into_string();
ICalendarDuration::parse(value.as_bytes())
.map(ICalendarParameterValue::Duration)
.unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned()))
}
ICalendarParameterName::Cutype => {
let value = value.into_string();
ICalendarUserTypes::parse(value.as_bytes())
.map(ICalendarParameterValue::Cutype)
.unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned()))
}
ICalendarParameterName::Fbtype => {
let value = value.into_string();
ICalendarFreeBusyType::parse(value.as_bytes())
.map(ICalendarParameterValue::Fbtype)
.unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned()))
}
ICalendarParameterName::Partstat => {
let value = value.into_string();
ICalendarParticipationStatus::parse(value.as_bytes())
.map(ICalendarParameterValue::Partstat)
.unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned()))
}
ICalendarParameterName::Related => {
let value = value.into_string();
ICalendarRelated::parse(value.as_bytes())
.map(ICalendarParameterValue::Related)
.unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned()))
}
ICalendarParameterName::Reltype => {
let value = value.into_string();
ICalendarRelationshipType::parse(value.as_bytes())
.map(ICalendarParameterValue::Reltype)
.unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned()))
}
ICalendarParameterName::Role => {
let value = value.into_string();
ICalendarParticipationRole::parse(value.as_bytes())
.map(ICalendarParameterValue::Role)
.unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned()))
}
ICalendarParameterName::ScheduleAgent => {
let value = value.into_string();
ICalendarScheduleAgentValue::parse(value.as_bytes())
.map(ICalendarParameterValue::ScheduleAgent)
.unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned()))
}
ICalendarParameterName::ScheduleForceSend => {
let value = value.into_string();
ICalendarScheduleForceSendValue::parse(value.as_bytes())
.map(ICalendarParameterValue::ScheduleForceSend)
.unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned()))
}
ICalendarParameterName::Display => {
let value = value.into_string();
ICalendarDisplayType::parse(value.as_bytes())
.map(ICalendarParameterValue::Display)
.unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned()))
}
ICalendarParameterName::Feature => {
let value = value.into_string();
ICalendarFeatureType::parse(value.as_bytes())
.map(ICalendarParameterValue::Feature)
.unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned()))
}
ICalendarParameterName::Linkrel => {
let value = value.into_string();
LinkRelation::parse(value.as_bytes())
.map(ICalendarParameterValue::Linkrel)
.unwrap_or_else(|| ICalendarParameterValue::Text(value.into_owned()))
}
_ => ICalendarParameterValue::Text(value.into_string().into_owned()),
};
self.params.push(ICalendarParameter {
name: param.clone(),
value,
});
}
}
}
}
impl ICalendarEntry {
/*
A DATE value has no time and no timezone: the TZID parameter MUST NOT be applied to
it (Section 3.2.19 of [RFC5545]).
*/
pub(super) fn with_date(mut self, dt: ZonedDateTime, is_date: bool) -> Self {
if !is_date {
return self.with_date_time(dt);
}
debug_assert!(self.values.is_empty());
if !self.has_parameter(&ICalendarParameterName::Value) {
self.params
.push(ICalendarParameter::value(ICalendarValueType::Date));
}
self.values
.push(PartialDateTime::from_date_timestamp(dt.naive_timestamp()).into());
self
}
pub(super) fn with_dates(mut self, dts: Vec<ZonedDateTime>, is_date: bool) -> Self {
if !is_date {
return self.with_date_times(dts);
}
debug_assert!(self.values.is_empty());
if !self.has_parameter(&ICalendarParameterName::Value) {
self.params
.push(ICalendarParameter::value(ICalendarValueType::Date));
}
for dt in dts {
self.values
.push(PartialDateTime::from_date_timestamp(dt.naive_timestamp()).into());
}
self
}
pub(super) fn with_date_time(mut self, dt: ZonedDateTime) -> Self {
debug_assert!(self.values.is_empty());
self.remove_period_value_type();
// Best effort to restore the original timezone
let tz_id = self.tz_id();
let has_tz_id = tz_id.is_some();
self.insert_date(
dt,
has_tz_id,
!has_tz_id,
tz_id.and_then(|id| Tz::from_str(id).ok()),
);
self
}
pub(super) fn with_period(mut self, dt: ZonedDateTime, duration: ICalendarDuration) -> Self {
self = self.with_date_time(dt);
if let Some(ICalendarValue::PartialDateTime(start)) = self.values.pop() {
self.values.push(ICalendarValue::Period(Box::new(
ICalendarPeriod::Duration { start, duration },
)));
}
if !self.has_parameter(&ICalendarParameterName::Value) {
self.params
.push(ICalendarParameter::value(ICalendarValueType::Period));
}
self
}
pub(super) fn with_date_times(mut self, dts: Vec<ZonedDateTime>) -> Self {
debug_assert!(self.values.is_empty());
self.remove_period_value_type();
let tz_id = self.tz_id();
let has_tz_id = tz_id.is_some();
let entry_tz = tz_id
.and_then(|id| Tz::from_str(id).ok())
.or_else(|| dts.first().map(|dt| dt.timezone()));
for (pos, dt) in dts.into_iter().enumerate() {
self.insert_date(dt, has_tz_id, pos == 0, entry_tz);
}
self
}
fn remove_period_value_type(&mut self) {
self.params.retain(|param| {
!matches!(
(&param.name, &param.value),
(
ICalendarParameterName::Value,
ICalendarParameterValue::Value(ICalendarValueType::Period)
)
)
});
}
fn insert_date(
&mut self,
mut dt: ZonedDateTime,
has_tz_id: bool,
add_tz_id: bool,
entry_tz: Option<Tz>,
) {
if let Some(tz) = entry_tz
&& tz != dt.timezone()
{
dt = dt.with_timezone(tz);
}
let tz = dt.timezone();
if has_tz_id {
self.values
.push(PartialDateTime::from_naive_timestamp(dt.naive_timestamp()).into());
} else if tz.is_utc() {
self.values
.push(PartialDateTime::from_utc_timestamp(dt.timestamp()).into());
} else {
if add_tz_id && let Some(tz_name) = tz.name() {
self.params
.push(ICalendarParameter::tzid(tz_name.into_owned()));
}
self.values
.push(PartialDateTime::from_naive_timestamp(dt.naive_timestamp()).into());
}
}
}
impl JSCalendarDateTime {
pub fn to_utc_date_time(&self) -> Option<ZonedDateTime> {
Timestamp::from_second(self.timestamp)
.ok()
.map(|timestamp| Tz::UTC.from_timestamp(timestamp))
}
pub fn to_naive_date_time(&self) -> Option<civil::DateTime> {
Timestamp::from_second(self.timestamp)
.ok()
.map(|timestamp| Offset::UTC.to_datetime(timestamp))
}
}
impl From<JSCalendarDateTime> for PartialDateTime {
fn from(dt: JSCalendarDateTime) -> Self {
if !dt.is_local {
Self::from_utc_timestamp(dt.timestamp)
} else {
Self::from_naive_timestamp(dt.timestamp)
}
}
}
@@ -0,0 +1,403 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Parser,
common::{
CalendarScale, IanaParse, IanaType, PartialDateTime, jsprop::text::ConvertedKeys,
parser::Integer,
},
icalendar::*,
jscalendar::{JSCalendarId, JSCalendarProperty, JSCalendarValue, export::ConvertedComponent},
};
use jmap_tools::{Key, Map, Value};
use smallvec::{SmallVec, smallvec};
impl<I: JSCalendarId, B: JSCalendarId> ConvertedComponent<'_, I, B> {
pub(super) fn apply_conversions(
self,
mut component: ICalendarComponent,
ical: &mut ICalendar,
) -> ICalendarComponent {
if !self.properties.is_empty() {
component.import_properties(self.properties);
}
if !self.components.is_empty() {
let mut jcal_components = self.components.into_iter();
let mut stack = Vec::new();
loop {
if let Some(jcal_component) = jcal_components.next() {
let Some(items) = jcal_component.into_array() else {
continue;
};
let mut items = items.into_iter();
if let (
Some(Value::Str(name)),
Some(Value::Array(properties)),
Some(Value::Array(child_components)),
) = (items.next(), items.next(), items.next())
{
// Process the component
let mut sub_component = ICalendarComponent::new(
ICalendarComponentType::parse(name.as_bytes()).unwrap_or_else(|| {
ICalendarComponentType::Other(name.to_ascii_uppercase())
}),
);
sub_component.import_properties(properties);
if !child_components.is_empty() {
stack.push((jcal_components, component));
jcal_components = child_components.into_iter();
component = sub_component;
} else {
component
.component_ids
.push(ical.push_component(sub_component));
}
}
} else if let Some((next_components, mut parent_component)) = stack.pop() {
parent_component
.component_ids
.push(ical.push_component(component));
jcal_components = next_components;
component = parent_component;
} else {
break;
}
}
}
component
}
pub(super) fn retain_class_property(&mut self, keep_unknown: bool) -> bool {
let mut has_class = false;
self.properties.retain(|property| {
let Some([Value::Str(name), _, _, value, ..]) = property.as_array() else {
return true;
};
if ICalendarProperty::parse(name.as_bytes()) != Some(ICalendarProperty::Class) {
return true;
}
let keep = keep_unknown
&& !has_class
&& matches!(value, Value::Str(value)
if ICalendarClassification::parse(value.as_bytes()).is_none());
has_class |= keep;
keep
});
has_class
}
}
impl ICalendarComponent {
pub(super) fn import_properties<I: JSCalendarId, B: JSCalendarId>(
&mut self,
props: Vec<Value<'_, JSCalendarProperty<I>, JSCalendarValue<I, B>>>,
) {
for prop in props.into_iter().flat_map(|prop| prop.into_array()) {
let mut prop = prop.into_iter();
let Some(name) = prop
.next()
.and_then(|v| v.into_string())
.map(|name| {
ICalendarProperty::parse(name.as_bytes())
.unwrap_or(ICalendarProperty::Other(name.to_ascii_uppercase()))
})
.filter(|name| !matches!(name, ICalendarProperty::Begin | ICalendarProperty::End))
else {
continue;
};
let Some(params) = prop.next() else {
continue;
};
let Some(value_type) =
prop.next()
.and_then(|v| v.into_string())
.map(|v| match ICalendarValueType::parse(v.as_bytes()) {
Some(v) => IanaType::Iana(v),
None => IanaType::Other(v.to_ascii_uppercase()),
})
else {
continue;
};
let (default_type, _) = name.default_types();
let convert_type = value_type
.iana()
.filter(|&v| v != &ICalendarValueType::Unknown)
.map(|v| ValueType::Ical(*v))
.unwrap_or(default_type);
let Some(values) = prop.next().and_then(|v| match v {
Value::Array(arr) => Some(
arr.into_iter()
.filter_map(|v| convert_value(v, &convert_type).ok())
.collect::<SmallVec<_>>(),
),
v => convert_value(v, &convert_type).ok().map(|v| smallvec![v]),
}) else {
continue;
};
let mut entry = ICalendarEntry::new(name);
entry.import_jcal_params(params);
entry.values = values;
if convert_type != default_type {
entry.params.push(ICalendarParameter::value(value_type));
}
self.entries.push(entry);
}
}
}
pub(super) fn convert_value<'x, I: JSCalendarId, B: JSCalendarId>(
value: Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
value_type: &'_ ValueType,
) -> Result<ICalendarValue, Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>> {
match value {
Value::Element(e) => match e {
JSCalendarValue::CalendarScale(v) => Ok(ICalendarValue::CalendarScale(v)),
JSCalendarValue::DateTime(v) => Ok(ICalendarValue::PartialDateTime(v.into())),
JSCalendarValue::Duration(v) => Ok(ICalendarValue::Duration(v)),
JSCalendarValue::Method(v) => Ok(ICalendarValue::Method(v)),
JSCalendarValue::AlertAction(_)
| JSCalendarValue::FreeBusyStatus(_)
| JSCalendarValue::ParticipantKind(_)
| JSCalendarValue::ParticipationStatus(_)
| JSCalendarValue::Privacy(_)
| JSCalendarValue::Progress(_)
| JSCalendarValue::RelativeTo(_)
| JSCalendarValue::ScheduleAgent(_)
| JSCalendarValue::EventStatus(_)
| JSCalendarValue::Frequency(_)
| JSCalendarValue::Skip(_)
| JSCalendarValue::Weekday(_)
| JSCalendarValue::Month(_)
| JSCalendarValue::LinkRelation(_)
| JSCalendarValue::Type(_)
| JSCalendarValue::Id(_)
| JSCalendarValue::BlobId(_)
| JSCalendarValue::IdReference(_) => Err(Value::Element(e)),
},
Value::Str(s) => {
match value_type {
ValueType::CalendarScale => {
if let Some(value) = CalendarScale::parse(s.as_ref().as_bytes()) {
return Ok(ICalendarValue::CalendarScale(value));
}
}
ValueType::Method => {
if let Some(value) = ICalendarMethod::parse(s.as_ref().as_bytes()) {
return Ok(ICalendarValue::Method(value));
}
}
ValueType::Classification => {
if let Some(value) = ICalendarClassification::parse(s.as_ref().as_bytes()) {
return Ok(ICalendarValue::Classification(value));
}
}
ValueType::Status => {
if let Some(value) = ICalendarStatus::parse(s.as_ref().as_bytes()) {
return Ok(ICalendarValue::Status(value));
}
}
ValueType::Transparency => {
if let Some(value) = ICalendarTransparency::parse(s.as_ref().as_bytes()) {
return Ok(ICalendarValue::Transparency(value));
}
}
ValueType::Action => {
if let Some(value) = ICalendarAction::parse(s.as_ref().as_bytes()) {
return Ok(ICalendarValue::Action(value));
}
}
ValueType::BusyType => {
if let Some(value) = ICalendarFreeBusyType::parse(s.as_ref().as_bytes()) {
return Ok(ICalendarValue::BusyType(value));
}
}
ValueType::ParticipantType => {
if let Some(value) = ICalendarParticipantType::parse(s.as_ref().as_bytes()) {
return Ok(ICalendarValue::ParticipantType(value));
}
}
ValueType::ResourceType => {
if let Some(value) = ICalendarResourceType::parse(s.as_ref().as_bytes()) {
return Ok(ICalendarValue::ResourceType(value));
}
}
ValueType::Proximity => {
if let Some(value) = ICalendarProximityValue::parse(s.as_ref().as_bytes()) {
return Ok(ICalendarValue::Proximity(value));
}
}
ValueType::Ical(typ) => match typ {
ICalendarValueType::Uri | ICalendarValueType::CalAddress => {
return Ok(ICalendarValue::Uri(Uri::parse(s)));
}
ICalendarValueType::Date => {
let mut dt = PartialDateTime::default();
if dt.parse_ical_date(&mut s.as_ref().as_bytes().iter().peekable()) {
return Ok(ICalendarValue::PartialDateTime(dt));
}
}
ICalendarValueType::Time => {
let mut dt = PartialDateTime::default();
if dt.parse_ical_time(&mut s.as_ref().as_bytes().iter().peekable()) {
return Ok(ICalendarValue::PartialDateTime(dt));
}
}
ICalendarValueType::DateTime => {
let mut dt = PartialDateTime::default();
if dt.parse_timestamp(&mut s.as_ref().as_bytes().iter().peekable(), false) {
return Ok(ICalendarValue::PartialDateTime(dt));
}
}
ICalendarValueType::UtcOffset => {
let mut dt = PartialDateTime::default();
if dt.parse_zone(&mut s.as_ref().as_bytes().iter().peekable()) {
return Ok(ICalendarValue::PartialDateTime(dt));
}
}
ICalendarValueType::Duration => {
if let Some(duration) = ICalendarDuration::parse(s.as_ref().as_bytes()) {
return Ok(ICalendarValue::Duration(duration));
}
}
ICalendarValueType::Float => {
if let Ok(float) = s.as_ref().parse::<f64>()
&& float.is_finite()
{
return Ok(ICalendarValue::Float(float));
}
}
ICalendarValueType::Integer => {
if let Some(integer) = Integer::parse(s.as_ref().as_bytes()) {
return Ok(ICalendarValue::Integer(integer.0));
}
}
ICalendarValueType::Period => {
if let Some(period) = ICalendarPeriod::parse(s.as_ref().as_bytes()) {
return Ok(ICalendarValue::Period(Box::new(period)));
}
}
ICalendarValueType::Recur => {
let mut parser = Parser::new(s.as_ref());
if let Ok(recur) = parser.rrule() {
return Ok(ICalendarValue::RecurrenceRule(Box::new(recur)));
}
}
ICalendarValueType::Boolean => {
if s.eq_ignore_ascii_case("true") {
return Ok(ICalendarValue::Boolean(true));
} else if s.eq_ignore_ascii_case("false") {
return Ok(ICalendarValue::Boolean(false));
}
}
ICalendarValueType::Binary => {
return Ok(match Uri::parse(s) {
Uri::Data(data) => ICalendarValue::Binary(data.data),
Uri::Location(text) => ICalendarValue::Text(text),
});
}
ICalendarValueType::Text
| ICalendarValueType::Unknown
| ICalendarValueType::XmlReference
| ICalendarValueType::Uid => (),
},
}
Ok(ICalendarValue::Text(s.into_owned()))
}
Value::Bool(b) => Ok(ICalendarValue::Boolean(b)),
Value::Number(n) => {
if let Some(integer) = n.as_i64() {
Ok(ICalendarValue::Integer(integer))
} else if let Some(integer) = n.as_u64() {
Ok(ICalendarValue::Text(integer.to_string()))
} else {
n.as_f64()
.filter(|float| float.is_finite())
.map(ICalendarValue::Float)
.ok_or(Value::Number(n))
}
}
value => Err(value),
}
}
impl<'x, I: JSCalendarId, B: JSCalendarId> ConvertedComponent<'x, I, B> {
#[allow(clippy::type_complexity)]
pub(super) fn try_from_object(
obj: Vec<(
Key<'x, JSCalendarProperty<I>>,
Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
)>,
) -> Option<Self> {
let mut converted = ConvertedComponent {
name: ICalendarComponentType::Other(String::new()),
converted_props: Vec::new(),
converted_props_count: 0,
properties: Vec::new(),
components: Vec::new(),
};
let mut has_name = false;
for (sub_property, value) in obj {
match (sub_property, value) {
(Key::Property(JSCalendarProperty::ConvertedProperties), Value::Object(obj)) => {
let obj = obj.into_vec();
converted.converted_props.reserve(obj.len());
for (key, value) in obj {
converted
.converted_props
.push((key.into_converted_keys(), value));
}
}
(Key::Property(JSCalendarProperty::Properties), Value::Array(array)) => {
converted.properties = array;
}
(Key::Property(JSCalendarProperty::Components), Value::Array(array)) => {
converted.components = array;
}
(Key::Property(JSCalendarProperty::Name), Value::Str(text)) => {
converted.name =
ICalendarComponentType::parse(text.as_bytes()).unwrap_or_else(|| {
ICalendarComponentType::Other(text.to_ascii_uppercase())
});
has_name = true;
}
_ => {}
}
}
if !converted.converted_props.is_empty() {
converted
.converted_props
.sort_unstable_by(|a, b| a.0.cmp(&b.0));
}
(!converted.properties.is_empty()
|| !converted.components.is_empty()
|| has_name
|| !converted.converted_props.is_empty())
.then_some(converted)
}
pub(super) fn build(
entries: &mut Map<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) -> Option<Self> {
for (property, value) in entries.as_mut_vec() {
if let (Key::Property(JSCalendarProperty::ICalendar), Value::Object(obj)) =
(property, value)
{
return Self::try_from_object(std::mem::take(obj.as_mut_vec()));
}
}
None
}
}
+317
View File
@@ -0,0 +1,317 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::jscalendar::{JSCalendarId, JSCalendarProperty, JSCalendarType, JSCalendarValue, Uuid5};
use jmap_tools::{JsonPointer, JsonPointerItem, Key, Map, Value};
use std::{
mem::{self, discriminant},
str::FromStr,
};
use uuid::fmt::Hyphenated;
pub(crate) trait JSCalendarKeyExt<I: JSCalendarId> {
fn is_forbidden_override_key(&self) -> bool;
fn is_ignored_override_key(&self) -> bool;
fn is_series_converted_property(&self) -> bool;
fn is_uuid5_of(&self, value: &[u8]) -> bool;
fn matches_pointer_item(&self, item: &JsonPointerItem<JSCalendarProperty<I>>) -> bool;
fn pointer_index(&self) -> Option<u64>;
fn same_key(&self, other: &Key<'_, JSCalendarProperty<I>>) -> bool;
fn same_data_key(&self, other: &Key<'_, JSCalendarProperty<I>>) -> bool;
}
pub(crate) trait JSCalendarMapExt {
fn is_type_or_untyped(&self, types: &[JSCalendarType]) -> bool;
}
pub(crate) trait JSCalendarObjectExt<'x, I: JSCalendarId, B: JSCalendarId> {
fn key_position(&self, key: &Key<'_, JSCalendarProperty<I>>) -> Option<usize>;
fn lookup(
&self,
key: &Key<'_, JSCalendarProperty<I>>,
) -> Option<&Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>>;
fn lookup_mut(
&mut self,
key: &Key<'_, JSCalendarProperty<I>>,
) -> Option<&mut Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>>;
fn upsert(
&mut self,
key: Key<'x, JSCalendarProperty<I>>,
value: Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) -> Option<Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>>;
fn upsert_or_get_mut(
&mut self,
key: Key<'x, JSCalendarProperty<I>>,
value: impl FnOnce() -> Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) -> &mut Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>;
fn contains_true(&self, key: &Key<'_, JSCalendarProperty<I>>) -> bool;
}
pub(crate) trait JSCalendarValueExt<I: JSCalendarId>: Sized {
fn apply_jsprop(&mut self, pointer: &[JsonPointerItem<JSCalendarProperty<I>>], patch: Self);
}
pub trait JSCalendarPatch {
fn is_instance_patch(&self) -> bool;
}
impl<I: JSCalendarId> JSCalendarKeyExt<I> for Key<'_, JSCalendarProperty<I>> {
fn is_forbidden_override_key(&self) -> bool {
match self {
Key::Property(JSCalendarProperty::Pointer(pointer)) => {
JSCalendarProperty::is_forbidden_override_pointer(pointer)
}
Key::Property(property) => property.is_forbidden_override_patch(),
_ => false,
}
}
fn is_ignored_override_key(&self) -> bool {
matches!(
self,
Key::Property(property) if property.is_forbidden_override_patch()
|| matches!(property, JSCalendarProperty::Excluded)
)
}
fn is_series_converted_property(&self) -> bool {
let first = match self {
Key::Property(JSCalendarProperty::Pointer(ptr)) => {
return matches!(
ptr.iter()
.find(|item| !matches!(item, JsonPointerItem::Root)),
Some(JsonPointerItem::Key(Key::Property(
JSCalendarProperty::RecurrenceOverrides
| JSCalendarProperty::RecurrenceRule
)))
);
}
Key::Property(property) => {
return matches!(
property,
JSCalendarProperty::RecurrenceOverrides | JSCalendarProperty::RecurrenceRule
);
}
Key::Borrowed(key) => key.split('/').next(),
Key::Owned(key) => key.split('/').next(),
};
matches!(
first.and_then(|name| JSCalendarProperty::<I>::from_str(name).ok()),
Some(JSCalendarProperty::RecurrenceOverrides | JSCalendarProperty::RecurrenceRule)
)
}
fn is_uuid5_of(&self, value: &[u8]) -> bool {
let name = self.to_string();
name.len() == Hyphenated::LENGTH && {
let mut buffer = [0u8; Hyphenated::LENGTH];
value.uuid5_hyphenated(&mut buffer) == name.as_ref()
}
}
fn matches_pointer_item(&self, item: &JsonPointerItem<JSCalendarProperty<I>>) -> bool {
match item {
JsonPointerItem::Key(item) => item.same_key(self),
JsonPointerItem::Number(number) => self.pointer_index() == Some(*number),
JsonPointerItem::Root | JsonPointerItem::Wildcard | JsonPointerItem::Invalid(_) => {
false
}
}
}
fn pointer_index(&self) -> Option<u64> {
let name = self.as_string_key()?;
(!name.is_empty()
&& name.bytes().all(|byte| byte.is_ascii_digit())
&& (name == "0" || !name.starts_with('0')))
.then(|| name.parse().ok())
.flatten()
}
#[inline(always)]
fn same_key(&self, other: &Key<'_, JSCalendarProperty<I>>) -> bool {
match (self, other) {
(Key::Property(a), Key::Property(b)) if !a.has_data() && !b.has_data() => {
discriminant(a) == discriminant(b)
}
_ => self.same_data_key(other),
}
}
#[inline(never)]
fn same_data_key(&self, other: &Key<'_, JSCalendarProperty<I>>) -> bool {
self == other
}
}
impl<'x, I: JSCalendarId, B: JSCalendarId> JSCalendarObjectExt<'x, I, B>
for Map<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>
{
#[inline]
fn key_position(&self, key: &Key<'_, JSCalendarProperty<I>>) -> Option<usize> {
self.as_vec().iter().position(|(k, _)| k.same_key(key))
}
#[inline]
fn lookup(
&self,
key: &Key<'_, JSCalendarProperty<I>>,
) -> Option<&Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>> {
self.as_vec()
.iter()
.find_map(|(k, v)| k.same_key(key).then_some(v))
}
#[inline]
fn lookup_mut(
&mut self,
key: &Key<'_, JSCalendarProperty<I>>,
) -> Option<&mut Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>> {
self.as_mut_vec()
.iter_mut()
.find_map(|(k, v)| k.same_key(key).then_some(v))
}
#[inline]
fn upsert(
&mut self,
key: Key<'x, JSCalendarProperty<I>>,
value: Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) -> Option<Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>> {
match self.lookup_mut(&key) {
Some(current) => Some(mem::replace(current, value)),
None => {
self.insert_unchecked(key, value);
None
}
}
}
#[inline]
fn upsert_or_get_mut(
&mut self,
key: Key<'x, JSCalendarProperty<I>>,
value: impl FnOnce() -> Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) -> &mut Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>> {
let position = match self.key_position(&key) {
Some(position) => position,
None => {
self.insert_unchecked(key, value());
self.len() - 1
}
};
&mut self.as_mut_vec()[position].1
}
#[inline]
fn contains_true(&self, key: &Key<'_, JSCalendarProperty<I>>) -> bool {
self.as_vec()
.iter()
.any(|(k, v)| matches!(v, Value::Bool(true)) && k.same_key(key))
}
}
impl<I: JSCalendarId, B: JSCalendarId> JSCalendarMapExt
for Map<'_, JSCalendarProperty<I>, JSCalendarValue<I, B>>
{
fn is_type_or_untyped(&self, types: &[JSCalendarType]) -> bool {
match self.lookup(&Key::Property(JSCalendarProperty::Type)) {
None => true,
Some(Value::Element(JSCalendarValue::Type(typ))) => types.contains(typ),
Some(_) => false,
}
}
}
impl<I: JSCalendarId, B: JSCalendarId> JSCalendarPatch
for Value<'_, JSCalendarProperty<I>, JSCalendarValue<I, B>>
{
fn is_instance_patch(&self) -> bool {
self.as_object().is_some_and(|patch| {
let excluded = Key::Property(JSCalendarProperty::Excluded);
!patch.contains_true(&excluded)
&& patch
.keys()
.any(|key| !key.same_key(&excluded) && !key.is_forbidden_override_key())
})
}
}
impl<I: JSCalendarId, B: JSCalendarId> JSCalendarValueExt<I>
for Value<'_, JSCalendarProperty<I>, JSCalendarValue<I, B>>
{
fn apply_jsprop(&mut self, pointer: &[JsonPointerItem<JSCalendarProperty<I>>], patch: Self) {
let (Some((last, parents)), false) = (pointer.split_last(), patch.is_null()) else {
return;
};
let mut current = self;
for item in parents {
let Value::Object(obj) = current else {
return;
};
let Some(next) = obj
.iter_mut()
.find_map(|(key, value)| key.matches_pointer_item(item).then_some(value))
else {
return;
};
current = next;
}
let Value::Object(obj) = current else {
return;
};
if obj.iter().any(|(key, _)| key.matches_pointer_item(last)) {
return;
}
let key = match last {
JsonPointerItem::Key(key) => key.clone(),
JsonPointerItem::Number(number) => Key::Owned(number.to_string()),
JsonPointerItem::Root | JsonPointerItem::Wildcard | JsonPointerItem::Invalid(_) => {
return;
}
};
obj.insert_unchecked(key, patch);
}
}
impl<I: JSCalendarId> JSCalendarProperty<I> {
pub(crate) fn is_series_property(&self) -> bool {
matches!(
self,
JSCalendarProperty::RecurrenceRule
| JSCalendarProperty::RecurrenceOverrides
| JSCalendarProperty::RecurrenceId
| JSCalendarProperty::RecurrenceIdTimeZone
| JSCalendarProperty::Excluded
)
}
pub(crate) fn is_member_map(&self) -> bool {
matches!(
self,
JSCalendarProperty::Participants
| JSCalendarProperty::Locations
| JSCalendarProperty::VirtualLocations
| JSCalendarProperty::Alerts
| JSCalendarProperty::Links
| JSCalendarProperty::RelatedTo
)
}
pub(crate) fn member_pointer<const N: usize>(
&self,
keys: [Key<'static, JSCalendarProperty<I>>; N],
) -> Key<'static, JSCalendarProperty<I>> {
let mut items = Vec::with_capacity(N + 1);
items.push(JsonPointerItem::Key(Key::Property(self.clone())));
items.extend(keys.into_iter().map(|key| match key.pointer_index() {
Some(index) => JsonPointerItem::Number(index),
None => JsonPointerItem::Key(key),
}));
Key::Property(JSCalendarProperty::Pointer(JsonPointer::new(items)))
}
}
File diff suppressed because it is too large. Load diff
+249
View File
@@ -0,0 +1,249 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use ahash::AHashMap;
use jmap_tools::{JsonPointer, Key, Map, Value};
use crate::common::timezone::ZonedDateTime;
use crate::{
common::{
blob::{BlobIdFn, BlobIdGenerator, BlobIds, BlobOptions, NoBlobIds},
jsprop::ordered::OrderedMap,
timezone::Tz,
},
icalendar::{
ICalendarComponentType, ICalendarEntry, ICalendarParameterName, ICalendarProperty,
},
jscalendar::{
JSCalendarDateTime, JSCalendarId, JSCalendarProperty, JSCalendarValue,
ext::JSCalendarObjectExt,
},
};
pub mod convert;
pub mod params;
pub mod props;
const PROPERTY_MAP_CAPACITY: usize = 16;
#[derive(Default)]
#[allow(clippy::type_complexity)]
struct State<I: JSCalendarId, B: JSCalendarId> {
component_type: ICalendarComponentType,
entries: PropertyMap<I, B>,
ical_converted_properties: OrderedMap<String, ICalendarConvertedProperty<I, B>>,
ical_properties: Vec<Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>>,
ical_components: Option<Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>>,
recurrence_overrides: Vec<(JSCalendarDateTime, State<I, B>)>,
patch_objects: Vec<(
JsonPointer<JSCalendarProperty<I>>,
Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
)>,
link_ids: LinkIds,
parameters: Map<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
jsid: Option<String>,
uid: Option<String>,
recurrence_id: Option<ZonedDateTime>,
recurrence_id_is_date: bool,
due: Option<ZonedDateTime>,
tz_start: Option<Tz>,
tz_end: Option<Tz>,
has_dates: bool,
has_end: bool,
map_component: bool,
is_recurrence_instance: bool,
include_ical_components: bool,
}
struct PropertyMap<I: JSCalendarId, B: JSCalendarId>(
Map<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
);
struct PropertyEntry<'a, I: JSCalendarId, B: JSCalendarId> {
map: &'a mut Map<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
key: Key<'static, JSCalendarProperty<I>>,
}
impl<I: JSCalendarId, B: JSCalendarId> Default for PropertyMap<I, B> {
fn default() -> Self {
Self(Map::from(Vec::with_capacity(PROPERTY_MAP_CAPACITY)))
}
}
impl<I: JSCalendarId, B: JSCalendarId> PropertyMap<I, B> {
#[inline]
fn insert(
&mut self,
key: Key<'static, JSCalendarProperty<I>>,
value: Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) -> Option<Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>> {
self.0.upsert(key, value)
}
#[inline]
fn entry(&mut self, key: Key<'static, JSCalendarProperty<I>>) -> PropertyEntry<'_, I, B> {
PropertyEntry {
map: &mut self.0,
key,
}
}
#[inline]
fn get(
&self,
key: &Key<'_, JSCalendarProperty<I>>,
) -> Option<&Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>> {
self.0.lookup(key)
}
#[inline]
fn contains_key(&self, key: &Key<'_, JSCalendarProperty<I>>) -> bool {
self.0.key_position(key).is_some()
}
fn retain(
&mut self,
mut keep: impl FnMut(
&Key<'static, JSCalendarProperty<I>>,
&mut Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) -> bool,
) {
self.0
.as_mut_vec()
.retain_mut(|(key, value)| keep(key, value));
}
#[inline]
fn into_map(self) -> Map<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>> {
self.0
}
}
impl<'a, I: JSCalendarId, B: JSCalendarId> PropertyEntry<'a, I, B> {
#[inline]
fn or_insert_with(
self,
value: impl FnOnce() -> Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) -> &'a mut Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>> {
let position = match self.map.key_position(&self.key) {
Some(position) => position,
None => {
self.map.insert_unchecked(self.key, value());
self.map.len() - 1
}
};
&mut self.map.as_mut_vec()[position].1
}
}
#[derive(Debug, Default)]
struct ICalendarConvertedProperty<I: JSCalendarId, B: JSCalendarId> {
name: Option<ICalendarProperty>,
params: ICalendarParams<I, B>,
}
#[derive(Debug, Default)]
struct ICalendarParams<I: JSCalendarId, B: JSCalendarId>(
Vec<(ICalendarParameterName, ParamValues<I, B>)>,
);
#[derive(Debug)]
enum ParamValues<I: JSCalendarId, B: JSCalendarId> {
One(Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>),
Many(Vec<Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>>),
}
#[derive(Debug, Default)]
struct LinkIds(AHashMap<String, LinkId>);
#[derive(Debug, Clone, Copy)]
struct LinkId {
position: usize,
next_suffix: u32,
}
#[derive(Debug, Clone)]
struct EntryState {
entry: ICalendarEntry,
converted_to: Option<ConvertedTo>,
map_name: bool,
keep_converted_path: bool,
}
#[derive(Debug, Clone)]
enum ConvertedTo {
Name(&'static str),
Pointer(String),
}
#[derive(Debug, Clone, Copy)]
pub struct ImportOptions<G = NoBlobIds> {
include_ical_components: bool,
return_first: bool,
blobs: BlobOptions<G>,
}
pub(super) struct ImportContext<'a, B> {
include_ical_components: bool,
return_first: bool,
blob_ids: Option<BlobIds<'a, B>>,
task_series: Vec<(String, convert::TaskAnchors)>,
}
impl Default for ImportOptions {
fn default() -> Self {
Self {
include_ical_components: true,
return_first: false,
blobs: BlobOptions::default(),
}
}
}
impl ImportOptions {
pub fn new() -> Self {
Self::default()
}
}
impl<G> ImportOptions<G> {
pub fn include_ical_components(mut self, include: bool) -> Self {
self.include_ical_components = include;
self
}
pub fn return_first(mut self, return_first: bool) -> Self {
self.return_first = return_first;
self
}
pub fn with_blob_ids<B, F>(self, blob_ids: F) -> ImportOptions<BlobIdFn<F>>
where
F: FnMut(&[u8]) -> Option<B>,
{
self.with_blob_id_generator(BlobIdFn(blob_ids))
}
pub fn with_blob_id_generator<T>(self, blob_id_generator: T) -> ImportOptions<T> {
ImportOptions {
include_ical_components: self.include_ical_components,
return_first: self.return_first,
blobs: self.blobs.with_handler(blob_id_generator),
}
}
pub(super) fn context<B: Clone>(&mut self) -> ImportContext<'_, B>
where
G: BlobIdGenerator<B>,
{
ImportContext {
include_ical_components: self.include_ical_components,
return_first: self.return_first,
blob_ids: self.blobs.blob_ids(),
task_series: Vec::new(),
}
}
}
@@ -0,0 +1,482 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{Data, IanaString, IanaType, jsprop::text::IntoAsciiLowercase},
icalendar::{
ICalendarDisplayType, ICalendarEntry, ICalendarFeatureType, ICalendarParameter,
ICalendarParameterName, ICalendarParameterValue, ICalendarParticipationRole,
ICalendarParticipationStatus, ICalendarProperty, ICalendarRelated, ICalendarUserTypes,
ICalendarValue, ICalendarValueType, Uri,
},
jscalendar::{
JSCalendarId, JSCalendarLinkDisplay, JSCalendarParticipantKind, JSCalendarParticipantRole,
JSCalendarParticipationStatus, JSCalendarProperty, JSCalendarRelativeTo, JSCalendarValue,
JSCalendarVirtualLocationFeature,
ext::JSCalendarObjectExt,
import::{ICalendarParams, ParamValues, PropertyMap},
},
};
use jmap_tools::{Key, Map, Value};
use std::{borrow::Cow, mem};
pub(super) trait ExtractParams<I: JSCalendarId, B: JSCalendarId> {
fn extract_params(
&mut self,
entry: &mut ICalendarEntry,
extract: &[ICalendarParameterName],
) -> Option<String>;
}
pub(super) trait ParamTarget<I: JSCalendarId, B: JSCalendarId> {
fn set(
&mut self,
property: JSCalendarProperty<I>,
value: Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
);
fn set_flag(
&mut self,
property: JSCalendarProperty<I>,
flag: Key<'static, JSCalendarProperty<I>>,
);
}
impl<I: JSCalendarId, B: JSCalendarId> ParamTarget<I, B> for PropertyMap<I, B> {
fn set(
&mut self,
property: JSCalendarProperty<I>,
value: Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) {
self.insert(Key::Property(property), value);
}
fn set_flag(
&mut self,
property: JSCalendarProperty<I>,
flag: Key<'static, JSCalendarProperty<I>>,
) {
if let Some(flags) = self
.entry(Key::Property(property))
.or_insert_with(Value::new_object)
.as_object_mut()
{
flags.upsert(flag, Value::Bool(true));
}
}
}
impl<I: JSCalendarId, B: JSCalendarId> ParamTarget<I, B>
for Map<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>
{
fn set(
&mut self,
property: JSCalendarProperty<I>,
value: Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) {
self.upsert(Key::Property(property), value);
}
fn set_flag(
&mut self,
property: JSCalendarProperty<I>,
flag: Key<'static, JSCalendarProperty<I>>,
) {
if let Some(flags) = self
.upsert_or_get_mut(Key::Property(property), Value::new_object)
.as_object_mut()
{
flags.upsert(flag, Value::Bool(true));
}
}
}
impl<I: JSCalendarId, B: JSCalendarId, T: ParamTarget<I, B>> ExtractParams<I, B> for T {
fn extract_params(
&mut self,
entry: &mut ICalendarEntry,
extract: &[ICalendarParameterName],
) -> Option<String> {
let mut jsid = None;
let is_styled_description = matches!(entry.name, ICalendarProperty::StyledDescription);
let is_conference = matches!(entry.name, ICalendarProperty::Conference);
entry.params.retain_mut(|param| {
if !extract.contains(&param.name) {
return true;
}
match param.name {
ICalendarParameterName::Cn => {
if let Some(text) = param.take_value().into_text() {
self.set(JSCalendarProperty::Name, Value::Str(text));
}
}
ICalendarParameterName::Cutype => {
let kind = match param.take_value() {
ICalendarParameterValue::Text(value) => Value::Str(value.into()),
ICalendarParameterValue::Cutype(value) => match value {
ICalendarUserTypes::Individual => {
Value::Element(JSCalendarValue::ParticipantKind(
JSCalendarParticipantKind::Individual,
))
}
ICalendarUserTypes::Group => Value::Element(
JSCalendarValue::ParticipantKind(JSCalendarParticipantKind::Group),
),
ICalendarUserTypes::Resource => {
Value::Element(JSCalendarValue::ParticipantKind(
JSCalendarParticipantKind::Resource,
))
}
ICalendarUserTypes::Room => {
Value::Element(JSCalendarValue::ParticipantKind(
JSCalendarParticipantKind::Location,
))
}
ICalendarUserTypes::Unknown => {
Value::Str(value.as_str().to_lowercase().into())
}
},
_ => return false,
};
self.set(JSCalendarProperty::Kind, kind);
}
ICalendarParameterName::DelegatedFrom => {
if let Some(text) = param.take_value().into_text() {
self.set_flag(JSCalendarProperty::DelegatedFrom, Key::from(text));
}
}
ICalendarParameterName::DelegatedTo => {
if let Some(text) = param.take_value().into_text() {
self.set_flag(JSCalendarProperty::DelegatedTo, Key::from(text));
}
}
ICalendarParameterName::Email => {
if let Some(text) = param.take_value().into_text() {
self.set(JSCalendarProperty::Email, Value::Str(text));
}
}
ICalendarParameterName::Rsvp => {
if let Some(boolean) = param.value.as_bool() {
self.set(JSCalendarProperty::ExpectReply, Value::Bool(boolean));
}
}
ICalendarParameterName::Member => {
if let Some(text) = param.take_value().into_text() {
self.set_flag(JSCalendarProperty::MemberOf, Key::from(text));
}
}
ICalendarParameterName::Partstat => {
let status = match param.take_value() {
ICalendarParameterValue::Partstat(value) => {
Value::Element(JSCalendarValue::ParticipationStatus(match value {
ICalendarParticipationStatus::NeedsAction => {
JSCalendarParticipationStatus::NeedsAction
}
ICalendarParticipationStatus::Declined => {
JSCalendarParticipationStatus::Declined
}
ICalendarParticipationStatus::Tentative => {
JSCalendarParticipationStatus::Tentative
}
ICalendarParticipationStatus::Delegated => {
JSCalendarParticipationStatus::Delegated
}
ICalendarParticipationStatus::Completed
| ICalendarParticipationStatus::InProcess
| ICalendarParticipationStatus::Failed
| ICalendarParticipationStatus::Accepted => {
JSCalendarParticipationStatus::Accepted
}
}))
}
ICalendarParameterValue::Text(value) => Value::Str(value.into()),
_ => return false,
};
self.set(JSCalendarProperty::ParticipationStatus, status);
}
ICalendarParameterName::Role => {
let role = match param.take_value() {
ICalendarParameterValue::Role(value) => {
Key::Property(JSCalendarProperty::ParticipantRole(match value {
ICalendarParticipationRole::Chair => {
JSCalendarParticipantRole::Chair
}
ICalendarParticipationRole::ReqParticipant => {
JSCalendarParticipantRole::Required
}
ICalendarParticipationRole::OptParticipant => {
JSCalendarParticipantRole::Optional
}
ICalendarParticipationRole::NonParticipant => {
JSCalendarParticipantRole::Informational
}
ICalendarParticipationRole::Owner => {
JSCalendarParticipantRole::Owner
}
}))
}
ICalendarParameterValue::Text(value) => Key::Owned(value),
_ => return false,
};
self.set_flag(JSCalendarProperty::Roles, role);
}
ICalendarParameterName::SentBy => {
if let Some(text) = param.take_value().into_text() {
self.set(JSCalendarProperty::SentBy, Value::Str(text));
}
}
ICalendarParameterName::Fmttype => {
if let Some(text) = param.take_value().into_text() {
self.set(
if is_styled_description {
JSCalendarProperty::DescriptionContentType
} else {
JSCalendarProperty::ContentType
},
Value::Str(text),
);
}
}
ICalendarParameterName::Label | ICalendarParameterName::Filename => {
if let Some(text) = param.take_value().into_text() {
self.set(
if is_conference {
JSCalendarProperty::Name
} else {
JSCalendarProperty::Title
},
Value::Str(text),
);
}
}
ICalendarParameterName::Size => match param.value.as_integer() {
Some(number) => {
self.set(JSCalendarProperty::Size, Value::Number(number.into()));
}
None => return true,
},
ICalendarParameterName::Linkrel => match param.take_value() {
ICalendarParameterValue::Linkrel(linkrel) => {
self.set(
JSCalendarProperty::Rel,
Value::Element(JSCalendarValue::LinkRelation(linkrel)),
);
}
ICalendarParameterValue::Text(value) => {
self.set(JSCalendarProperty::Rel, Value::Str(value.into()));
}
value => {
param.value = value;
return true;
}
},
ICalendarParameterName::Related => match param.take_value() {
ICalendarParameterValue::Related(related) => {
self.set(
JSCalendarProperty::RelativeTo,
Value::Element(JSCalendarValue::RelativeTo(match related {
ICalendarRelated::Start => JSCalendarRelativeTo::Start,
ICalendarRelated::End => JSCalendarRelativeTo::End,
})),
);
}
value => {
param.value = value;
return true;
}
},
ICalendarParameterName::Feature => {
let feature = match param.take_value() {
ICalendarParameterValue::Feature(value) => {
Key::Property(JSCalendarProperty::VirtualLocationFeature(match value {
ICalendarFeatureType::Audio => {
JSCalendarVirtualLocationFeature::Audio
}
ICalendarFeatureType::Chat => {
JSCalendarVirtualLocationFeature::Chat
}
ICalendarFeatureType::Feed => {
JSCalendarVirtualLocationFeature::Feed
}
ICalendarFeatureType::Moderator => {
JSCalendarVirtualLocationFeature::Moderator
}
ICalendarFeatureType::Phone => {
JSCalendarVirtualLocationFeature::Phone
}
ICalendarFeatureType::Screen => {
JSCalendarVirtualLocationFeature::Screen
}
ICalendarFeatureType::Video => {
JSCalendarVirtualLocationFeature::Video
}
}))
}
ICalendarParameterValue::Text(value) => Key::Owned(value),
_ => return false,
};
self.set_flag(JSCalendarProperty::Features, feature);
}
ICalendarParameterName::Language => {
if let Some(text) = param.take_value().into_text() {
self.set(JSCalendarProperty::Locale, Value::Str(text));
}
}
ICalendarParameterName::Display => {
let display = match param.take_value() {
ICalendarParameterValue::Display(value) => {
Key::Property(JSCalendarProperty::LinkDisplay(match value {
ICalendarDisplayType::Badge => JSCalendarLinkDisplay::Badge,
ICalendarDisplayType::Graphic => JSCalendarLinkDisplay::Graphic,
ICalendarDisplayType::Fullsize => JSCalendarLinkDisplay::Fullsize,
ICalendarDisplayType::Thumbnail => JSCalendarLinkDisplay::Thumbnail,
}))
}
ICalendarParameterValue::Text(value) => Key::Owned(value),
value => {
param.value = value;
return true;
}
};
self.set_flag(JSCalendarProperty::Display, display);
}
ICalendarParameterName::Jsid => {
jsid = param.take_value().into_text().map(Cow::into_owned);
}
ICalendarParameterName::Range
| ICalendarParameterName::Reltype
| ICalendarParameterName::Dir
| ICalendarParameterName::Fbtype
| ICalendarParameterName::ScheduleAgent
| ICalendarParameterName::ScheduleForceSend
| ICalendarParameterName::ScheduleStatus
| ICalendarParameterName::Tzid
| ICalendarParameterName::Value
| ICalendarParameterName::ManagedId
| ICalendarParameterName::Order
| ICalendarParameterName::Schema
| ICalendarParameterName::Derived
| ICalendarParameterName::Gap
| ICalendarParameterName::Jsptr
| ICalendarParameterName::Other(_)
| ICalendarParameterName::Altrep => {}
}
false
});
jsid
}
}
impl ICalendarParameter {
fn take_value(&mut self) -> ICalendarParameterValue {
mem::replace(&mut self.value, ICalendarParameterValue::Null)
}
}
impl<I: JSCalendarId, B: JSCalendarId> ICalendarParams<I, B> {
pub(super) fn push(
&mut self,
name: ICalendarParameterName,
value: Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) {
match self.0.iter_mut().find(|(param, _)| param == &name) {
Some((_, values)) => values.push(value),
None => self.0.push((name, ParamValues::One(value))),
}
}
pub(super) fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub(super) fn into_jscalendar_value(
self,
) -> Option<Map<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>> {
(!self.0.is_empty()).then(|| {
self.0
.into_iter()
.map(|(param, values)| {
let value = match values {
ParamValues::One(value) => value,
ParamValues::Many(values) => Value::Array(values),
};
(
Key::Owned(param.into_string().into_ascii_lowercase()),
value,
)
})
.collect()
})
}
}
impl<I: JSCalendarId, B: JSCalendarId> ParamValues<I, B> {
fn push(&mut self, value: Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>) {
match self {
ParamValues::Many(values) => values.push(value),
ParamValues::One(first) => {
let first = mem::replace(first, Value::Null);
*self = ParamValues::Many(vec![first, value]);
}
}
}
}
impl ICalendarValue {
pub(super) fn into_jscalendar_value<I: JSCalendarId, B: JSCalendarId>(
self,
value_type: Option<&IanaType<ICalendarValueType, String>>,
) -> Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>> {
match self {
ICalendarValue::Text(v) => Value::Str(v.into()),
ICalendarValue::Integer(v) => Value::Number(v.into()),
ICalendarValue::Float(v) => Value::Number(v.into()),
ICalendarValue::Boolean(v) => Value::Bool(v),
ICalendarValue::PartialDateTime(v) => {
let mut out = String::new();
let _ = v.format_as_ical(
&mut out,
value_type.and_then(|v| v.iana()).unwrap_or(
match (v.has_date(), v.has_time(), v.has_zone()) {
(true, true, _) => &ICalendarValueType::DateTime,
(true, _, _) => &ICalendarValueType::Date,
(_, true, _) => &ICalendarValueType::Time,
(_, _, true) => &ICalendarValueType::UtcOffset,
_ => &ICalendarValueType::Text,
},
),
);
Value::Str(out.into())
}
ICalendarValue::Binary(v) => Value::Str(
Uri::Data(Box::new(Data {
content_type: None,
data: v,
}))
.into_unwrapped_string()
.into(),
),
ICalendarValue::Uri(v) => Value::Str(v.into_unwrapped_string().into()),
ICalendarValue::Duration(v) => Value::Str(v.to_string().into()),
ICalendarValue::RecurrenceRule(v) => Value::Str(v.to_string().into()),
ICalendarValue::Period(v) => Value::Str(v.to_string().into()),
ICalendarValue::CalendarScale(v) => Value::Str(v.as_str().into()),
ICalendarValue::Method(v) => Value::Str(v.as_str().into()),
ICalendarValue::Classification(v) => Value::Str(v.as_str().into()),
ICalendarValue::Status(v) => Value::Str(v.as_str().into()),
ICalendarValue::Transparency(v) => Value::Str(v.as_str().into()),
ICalendarValue::Action(v) => Value::Str(v.as_str().into()),
ICalendarValue::BusyType(v) => Value::Str(v.as_str().into()),
ICalendarValue::ParticipantType(v) => Value::Str(v.as_str().into()),
ICalendarValue::ResourceType(v) => Value::Str(v.as_str().into()),
ICalendarValue::Proximity(v) => Value::Str(v.as_str().into()),
}
}
}
@@ -0,0 +1,917 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{
Data, IanaString, IanaType, LinkRelation,
blob::GeneratedBlobId,
jsprop::text::{IntoAsciiLowercase, PointerString},
timezone::{Tz, ZonedDateTime},
},
icalendar::{
ICalendar, ICalendarComponentType, ICalendarDuration, ICalendarEntry,
ICalendarParameterName, ICalendarParameterValue, ICalendarProperty, ICalendarValue,
ICalendarValueType, Uri,
},
jscalendar::{
JSCALENDAR_VERSION, JSCalendarDateTime, JSCalendarId, JSCalendarPrivacy,
JSCalendarProperty, JSCalendarType, JSCalendarValue,
ext::{JSCalendarKeyExt, JSCalendarObjectExt, JSCalendarValueExt},
import::{
ConvertedTo, EntryState, ICalendarConvertedProperty, ICalendarParams, LinkId, LinkIds,
State, params::ExtractParams,
},
overrides::{Inherited, OverrideDiff},
uuid5,
},
};
use ahash::AHashMap;
use jmap_tools::{JsonPointerItem, Key, Map, Property, Value};
use std::{borrow::Cow, collections::hash_map::Entry, mem};
impl<I: JSCalendarId, B: JSCalendarId> State<I, B> {
pub(super) fn map_named_entry(
&mut self,
entry: &mut EntryState,
extract: &[ICalendarParameterName],
top_property_name: JSCalendarProperty<I>,
values: impl IntoIterator<
Item = (
Key<'static, JSCalendarProperty<I>>,
Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
),
>,
) {
self.map_named_entry_with_id(entry, extract, top_property_name, values, None)
}
pub(super) fn map_named_entry_with_id(
&mut self,
entry: &mut EntryState,
extract: &[ICalendarParameterName],
top_property_name: JSCalendarProperty<I>,
values: impl IntoIterator<
Item = (
Key<'static, JSCalendarProperty<I>>,
Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
),
>,
default_id: Option<String>,
) {
// Obtain main property and value
let mut values = values.into_iter().peekable();
let (property, value) = match values.peek() {
Some((property, Value::Str(s))) => (property, s.as_ref()),
Some((property, _)) => (property, "unknown"),
_ => {
panic!("Cannot generate jsid without a value");
}
};
let mut parameters = mem::take(&mut self.parameters);
let is_link = top_property_name == JSCalendarProperty::Links;
let js_id = match parameters.extract_params(&mut entry.entry, extract) {
Some(js_id) => js_id,
None => {
let js_id = default_id.unwrap_or_else(|| uuid5(value));
if is_link {
self.link_ids.unique(js_id)
} else {
js_id
}
}
};
entry.set_converted_to(|| {
String::from_pointer([
top_property_name.to_cow().as_ref(),
js_id.as_str(),
property.to_string().as_ref(),
])
});
let capacity = values.size_hint().1.unwrap_or_default() + parameters.len();
let new_object = || Value::Object(Map::from(Vec::with_capacity(capacity)));
if let Some(obj) = self
.entries
.entry(Key::Property(top_property_name))
.or_insert_with(Value::new_object)
.as_object_mut()
.and_then(|objects| {
if is_link {
self.link_ids.entry(objects, js_id, new_object)
} else {
Some(objects.upsert_or_get_mut(Key::Owned(js_id), new_object))
}
})
.and_then(Value::as_object_mut)
{
for (key, value) in values.chain(parameters.as_mut_vec().drain(..)) {
if let Some(current_value) = obj.lookup_mut(&key) {
match (value, current_value) {
(Value::Object(new_obj), Value::Object(existing_obj)) => {
for (key, value) in new_obj.into_vec() {
existing_obj.upsert(key, value);
}
}
(value, current_value) => {
*current_value = value;
}
}
} else {
obj.insert_unchecked(key, value);
}
}
}
parameters.as_mut_vec().clear();
self.parameters = parameters;
}
pub(super) fn map_blob_link(
&mut self,
entry: &mut EntryState,
generated: GeneratedBlobId<B>,
content_type: Option<String>,
default_id: Option<String>,
value_type: ICalendarValueType,
) {
let (rel, extract): (_, &[ICalendarParameterName]) = match (&entry.entry.name, value_type) {
(ICalendarProperty::Image, ICalendarValueType::Binary) => (
LinkRelation::Icon,
&[
ICalendarParameterName::Display,
ICalendarParameterName::Fmttype,
ICalendarParameterName::Filename,
ICalendarParameterName::Linkrel,
ICalendarParameterName::Jsid,
],
),
(ICalendarProperty::Image, _) => (
LinkRelation::Icon,
&[
ICalendarParameterName::Display,
ICalendarParameterName::Fmttype,
ICalendarParameterName::Filename,
ICalendarParameterName::Jsid,
],
),
(_, ICalendarValueType::Binary) => (
LinkRelation::Enclosure,
&[
ICalendarParameterName::Fmttype,
ICalendarParameterName::Filename,
ICalendarParameterName::Linkrel,
ICalendarParameterName::Jsid,
],
),
_ => (
LinkRelation::Enclosure,
&[
ICalendarParameterName::Fmttype,
ICalendarParameterName::Filename,
ICalendarParameterName::Jsid,
],
),
};
entry.entry.params.retain(|param| {
!matches!(
param.name,
ICalendarParameterName::Value | ICalendarParameterName::Size
)
});
entry.set_map_name();
self.map_named_entry_with_id(
entry,
extract,
JSCalendarProperty::Links,
[
Some((
Key::Property(JSCalendarProperty::BlobId),
Value::Element(JSCalendarValue::BlobId(generated.blob_id)),
)),
Some((
Key::Property(JSCalendarProperty::Type),
Value::Element(JSCalendarValue::Type(JSCalendarType::Link)),
)),
Some((
Key::Property(JSCalendarProperty::Rel),
Value::Element(JSCalendarValue::LinkRelation(rel)),
)),
Some((
Key::Property(JSCalendarProperty::Size),
Value::Number((generated.size as u64).into()),
)),
content_type.map(|content_type| {
(
Key::Property(JSCalendarProperty::ContentType),
Value::Str(content_type.into()),
)
}),
]
.into_iter()
.flatten(),
default_id,
);
}
pub(super) fn insert_recurrence_override(
&mut self,
key: Key<'static, JSCalendarProperty<I>>,
value: Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) {
self.entries
.entry(Key::Property(JSCalendarProperty::RecurrenceOverrides))
.or_insert_with(Value::new_object)
.as_object_mut()
.unwrap()
.upsert(key, value);
}
pub(super) fn add_period_conversion_prop(&mut self, converted_to: String) {
if self.include_ical_components {
let mut params = ICalendarParams::default();
params.push(
ICalendarParameterName::Value,
Value::Str(ICalendarValueType::Period.as_str().into()),
);
self.ical_converted_properties.insert_if_absent(
converted_to,
ICalendarConvertedProperty {
name: Some(ICalendarProperty::Rdate),
params,
},
);
}
}
pub(super) fn add_conversion_props(&mut self, mut entry: EntryState) {
if self.include_ical_components {
if let Some(converted_to) = entry.converted_to.take() {
if entry.map_name || !entry.entry.params.is_empty() {
let mut value_type = None;
let converted_to = converted_to.into_string();
match self.ical_converted_properties.get_mut(&converted_to) {
Some(conv_prop) => {
entry.jcal_parameters(&mut conv_prop.params, &mut value_type);
}
None => {
let mut params = ICalendarParams::default();
entry.jcal_parameters(&mut params, &mut value_type);
if let Some(value_type) = value_type {
params.push(
ICalendarParameterName::Value,
Value::Str(value_type.into_string()),
);
}
if !params.is_empty() || entry.map_name {
self.ical_converted_properties.push(
converted_to,
ICalendarConvertedProperty {
name: if entry.map_name {
Some(entry.entry.name)
} else {
None
},
params,
},
);
}
}
}
}
} else {
self.ical_properties.push(entry.into_jcal());
}
}
}
pub(super) fn into_object(
mut self,
) -> Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>> {
let mut ical_obj = Map::from(Vec::new());
if !self.ical_converted_properties.is_empty() {
let mut converted_properties =
Map::from(Vec::with_capacity(self.ical_converted_properties.len()));
for (converted_to, props) in self.ical_converted_properties {
let mut obj = Map::from(Vec::with_capacity(2));
if let Some(params) = props.params.into_jscalendar_value() {
obj.upsert(
Key::Property(JSCalendarProperty::Parameters),
Value::Object(params),
);
}
if let Some(name) = props.name {
obj.upsert(
Key::Property(JSCalendarProperty::Name),
Value::Str(name.into_string().into_ascii_lowercase().into()),
);
}
converted_properties.insert_unchecked(Key::Owned(converted_to), Value::Object(obj));
}
ical_obj.insert_unchecked(
Key::Property(JSCalendarProperty::ConvertedProperties),
Value::Object(converted_properties),
);
}
if !self.ical_properties.is_empty() {
ical_obj.insert_unchecked(
Key::Property(JSCalendarProperty::Properties),
Value::Array(self.ical_properties),
);
}
if let Some(components) = self.ical_components {
ical_obj.insert_unchecked(Key::Property(JSCalendarProperty::Components), components);
}
if !ical_obj.is_empty() || self.map_component {
ical_obj.insert_unchecked(
Key::Property(JSCalendarProperty::Name),
Value::Str(self.component_type.as_str().to_ascii_lowercase().into()),
);
self.entries.insert(
Key::Property(JSCalendarProperty::ICalendar),
Value::Object(ical_obj),
);
}
if self.has_dates {
/*
If a date-time value does not have a timezone, then the timezone is not set
in JSCalendar.
*/
if let Some(tz) = self.tz_start.and_then(|tz| tz.name()) {
self.entries
.insert(Key::Property(JSCalendarProperty::TimeZone), Value::Str(tz));
}
if self.tz_end.is_some()
&& self.tz_start.is_some()
&& self.tz_end != self.tz_start
&& let Some(tz) = self.tz_end.and_then(|tz| tz.name())
{
self.entries.insert(
Key::Property(JSCalendarProperty::EndTimeZone),
Value::Str(tz),
);
}
if let Some(recurrence_id) = self.recurrence_id {
self.entries.insert(
Key::Property(JSCalendarProperty::RecurrenceId),
Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::new(
recurrence_id.naive_timestamp(),
true,
))),
);
if !self.is_recurrence_instance
&& let Some(tz) = recurrence_id
.timezone()
.to_resolved()
.and_then(|tz| tz.name())
{
self.entries.insert(
Key::Property(JSCalendarProperty::RecurrenceIdTimeZone),
Value::Str(tz),
);
}
}
}
if let Some(uid) = self.uid {
self.entries.insert(
Key::Property(JSCalendarProperty::Uid),
Value::Str(uid.into()),
);
}
if !self.is_recurrence_instance
&& let Some(component_type) = self.component_type.to_jscalendar_type()
{
self.entries.insert(
Key::Property(JSCalendarProperty::Type),
Value::Element(JSCalendarValue::Type(component_type)),
);
}
let mut obj = Value::Object(self.entries.into_map());
let (override_patches, patches): (Vec<_>, Vec<_>) =
self.patch_objects.into_iter().partition(|(pointer, _)| {
!self.recurrence_overrides.is_empty()
&& matches!(
pointer.first(),
Some(JsonPointerItem::Key(Key::Property(
JSCalendarProperty::RecurrenceOverrides
)))
)
});
for (pointer, patch) in patches {
obj.apply_jsprop(pointer.as_slice(), patch);
}
if !self.recurrence_overrides.is_empty()
&& let Value::Object(base) = &mut obj
{
let overrides_key = Key::Property(JSCalendarProperty::RecurrenceOverrides);
let mut overrides = match base.key_position(&overrides_key) {
Some(pos) => base.as_mut_vec().remove(pos).1,
None => Value::new_object(),
};
if let Value::Object(overrides) = &mut overrides {
let diff = OverrideDiff::new(base);
let duration_key = Key::Property(JSCalendarProperty::Duration);
let mut slots = overrides
.as_vec()
.iter()
.enumerate()
.filter_map(|(position, (key, value))| match key {
Key::Property(JSCalendarProperty::DateTime(recurrence_id)) => Some((
*recurrence_id,
(
position,
value
.as_object()
.and_then(|period| period.lookup(&duration_key))
.cloned(),
),
)),
_ => None,
})
.collect::<AHashMap<_, _>>();
for (recurrence_id, recurrence) in self.recurrence_overrides {
let inherited = recurrence.inherited();
let instance = recurrence.into_object().into_object().unwrap_or_default();
let mut patch = diff.diff(recurrence_id, instance, inherited);
match slots.entry(recurrence_id) {
Entry::Occupied(slot) => {
let (position, period_duration) = slot.get();
if inherited == Inherited::StartAndEnd
&& let Some(duration) = period_duration
{
patch.upsert(duration_key.clone(), duration.clone());
}
if let Some((_, value)) = overrides.as_mut_vec().get_mut(*position) {
*value = Value::Object(patch);
}
}
Entry::Vacant(slot) => {
slot.insert((overrides.as_vec().len(), None));
overrides.insert_unchecked(
Key::Property(JSCalendarProperty::DateTime(recurrence_id)),
Value::Object(patch),
);
}
}
}
}
base.insert_unchecked(overrides_key, overrides);
}
for (pointer, patch) in override_patches {
obj.apply_jsprop(pointer.as_slice(), patch);
}
obj
}
pub(super) fn into_root_object(
self,
) -> Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>> {
let has_version = matches!(
self.component_type,
ICalendarComponentType::VCalendar
| ICalendarComponentType::VEvent
| ICalendarComponentType::VTodo
);
let mut obj = self.into_object();
if has_version && let Value::Object(map) = &mut obj {
map.upsert(
Key::Property(JSCalendarProperty::Version),
Value::Str(JSCALENDAR_VERSION.into()),
);
}
obj
}
pub(super) fn set_map_component(&mut self) {
self.map_component = true;
}
pub(super) fn set_is_recurrence_instance(&mut self) {
self.is_recurrence_instance = true;
}
fn inherited(&self) -> Inherited {
if self
.entries
.contains_key(&Key::Property(JSCalendarProperty::Start))
{
Inherited::Nothing
} else if self.has_end {
Inherited::Start
} else {
Inherited::StartAndEnd
}
}
pub(super) fn has_start_or_due(&self) -> bool {
self.entries
.contains_key(&Key::Property(JSCalendarProperty::Start))
|| self
.entries
.contains_key(&Key::Property(JSCalendarProperty::Due))
}
pub(super) fn inherit_time_zone(&mut self, tz: Option<Tz>) {
if self
.entries
.contains_key(&Key::Property(JSCalendarProperty::Start))
|| (tz.is_none() && self.due.is_some())
{
return;
}
if let Some(due) = self.due {
self.entries.insert(
Key::Property(JSCalendarProperty::Due),
Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::local_in(
due, tz,
))),
);
}
self.tz_start = tz;
}
pub(super) fn start_at_recurrence_id(&mut self) {
if let Some(recurrence_id) = self.recurrence_id
&& !self.has_start_or_due()
{
self.entries.insert(
Key::Property(JSCalendarProperty::Start),
Value::Element(JSCalendarValue::DateTime(JSCalendarDateTime::new(
recurrence_id.naive_timestamp(),
true,
))),
);
if self.recurrence_id_is_date {
self.default_to_one_day();
}
}
}
pub(super) fn default_to_one_day(&mut self) {
if self.has_end || self.component_type != ICalendarComponentType::VEvent {
return;
}
self.entries.insert(
Key::Property(JSCalendarProperty::Duration),
Value::Element(JSCalendarValue::Duration(ICalendarDuration::from_days(1))),
);
if self.include_ical_components {
self.ical_converted_properties.insert(
JSCalendarProperty::Duration::<I>.to_string().into_owned(),
ICalendarConvertedProperty {
name: Some(ICalendarProperty::Dtstart),
..Default::default()
},
);
}
}
pub(super) fn recurrence_sequence(&self) -> Option<i64> {
self.recurrence_id.map(|_| {
self.entries
.get(&Key::Property(JSCalendarProperty::Sequence))
.and_then(Value::as_i64)
.unwrap_or_default()
})
}
pub(super) fn privacy(&self) -> Option<JSCalendarPrivacy> {
match self
.entries
.get(&Key::Property(JSCalendarProperty::Privacy))?
{
Value::Element(JSCalendarValue::Privacy(privacy)) => Some(*privacy),
_ => Some(JSCalendarPrivacy::Private),
}
}
pub(super) fn raise_privacy(&mut self, privacy: JSCalendarPrivacy) {
if self.privacy().unwrap_or(JSCalendarPrivacy::Public) < privacy {
self.entries.insert(
Key::Property(JSCalendarProperty::Privacy),
Value::Element(JSCalendarValue::Privacy(privacy)),
);
self.patch_objects.retain(|(pointer, _)| {
!matches!(
pointer.first(),
Some(JsonPointerItem::Key(Key::Property(
JSCalendarProperty::Privacy
)))
)
});
}
}
pub(super) fn merge_privacy(&mut self, privacy: JSCalendarPrivacy) {
let privacy = self
.privacy()
.map_or(privacy, |current| current.max(privacy));
self.entries.insert(
Key::Property(JSCalendarProperty::Privacy),
Value::Element(JSCalendarValue::Privacy(privacy)),
);
}
pub(super) fn remove_forbidden_override_patches(&mut self) {
self.entries.retain(|key, _| {
!matches!(key, Key::Property(property) if property.is_forbidden_override_patch())
});
self.patch_objects
.retain(|(pointer, _)| !JSCalendarProperty::is_forbidden_override_pointer(pointer));
self.ical_converted_properties.retain(|converted_to, _| {
!Key::<JSCalendarProperty<I>>::Borrowed(converted_to).is_series_converted_property()
});
}
pub(super) fn find_participant_by_address(&self, address: &str) -> Option<String> {
self.entries
.get(&Key::Property(JSCalendarProperty::Participants))?
.as_object()?
.iter()
.find_map(|(key, value)| {
match value
.as_object()?
.lookup(&Key::Property(JSCalendarProperty::CalendarAddress))?
{
Value::Str(value) if value == address => Some(key.to_string().into_owned()),
_ => None,
}
})
}
#[inline]
pub(super) fn get_mut_object_or_insert(
&mut self,
key: JSCalendarProperty<I>,
) -> &mut Map<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>> {
self.entries
.entry(Key::Property(key))
.or_insert_with(|| Value::Object(Map::from(Vec::new())))
.as_object_mut()
.unwrap()
}
}
impl JSCalendarDateTime {
pub(super) fn local_in(dt: ZonedDateTime, tz: Option<Tz>) -> Self {
JSCalendarDateTime::new(
if dt.timezone().is_floating() || tz == Some(dt.timezone()) {
dt.naive_timestamp()
} else {
dt.with_timezone(tz.unwrap_or_default()).naive_timestamp()
},
true,
)
}
}
impl EntryState {
pub(super) fn new(entry: ICalendarEntry, keep_converted_path: bool) -> Self {
Self {
entry,
converted_to: None,
map_name: false,
keep_converted_path,
}
}
pub(super) fn set_converted_to(&mut self, converted_to: impl FnOnce() -> String) {
self.converted_to = Some(ConvertedTo::Pointer(
if self.keep_converted_path && (self.map_name || !self.entry.params.is_empty()) {
converted_to()
} else {
String::new()
},
));
}
pub(super) fn set_converted_to_property<I: JSCalendarId>(
&mut self,
property: &JSCalendarProperty<I>,
) {
self.converted_to = Some(match property.static_name() {
Some(name) => ConvertedTo::Name(name),
None => ConvertedTo::Pointer(String::from_pointer([property.to_string().as_ref()])),
});
}
pub(super) fn set_map_name(&mut self) {
self.map_name = true;
}
pub(super) fn into_jcal<I: JSCalendarId, B: JSCalendarId>(
mut self,
) -> Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>> {
let mut value_type = None;
let mut params = ICalendarParams::default();
self.jcal_parameters(&mut params, &mut value_type);
let values = if self.entry.values.len() == 1 {
self.entry
.values
.into_iter()
.next()
.unwrap()
.into_jscalendar_value(value_type.as_ref())
} else {
let mut values = Vec::with_capacity(self.entry.values.len());
for value in self.entry.values {
values.push(value.into_jscalendar_value(value_type.as_ref()));
}
Value::Array(values)
};
Value::Array(vec![
Value::Str(self.entry.name.into_string().into_ascii_lowercase().into()),
Value::Object(
params
.into_jscalendar_value()
.unwrap_or(Map::from(Vec::new())),
),
Value::Str(
value_type
.map(|v| v.into_string())
.unwrap_or(Cow::Borrowed("unknown")),
),
values,
])
}
pub(super) fn jcal_parameters<I: JSCalendarId, B: JSCalendarId>(
&mut self,
params: &mut ICalendarParams<I, B>,
value_type: &mut Option<IanaType<ICalendarValueType, String>>,
) {
if self.entry.params.is_empty() {
return;
}
let (default_type, _) = self.entry.name.default_types();
let default_type = default_type.unwrap_ical();
for param in std::mem::take(&mut self.entry.params) {
if matches!(param.name, ICalendarParameterName::Value) {
if let Some(v) = param
.value
.into_value_type()
.filter(|v| !v.is_iana_and(|v| v == &default_type))
{
*value_type = Some(v);
}
} else if let (ICalendarParameterName::Range, ICalendarParameterValue::Bool(true)) =
(&param.name, &param.value)
{
params.push(param.name, Value::Str("THISANDFUTURE".into()));
} else if let Some(value) = param.value.into_text() {
params.push(param.name, Value::Str(value));
}
}
}
}
impl ConvertedTo {
fn into_string(self) -> String {
match self {
ConvertedTo::Name(name) => String::from_pointer([name]),
ConvertedTo::Pointer(pointer) => pointer,
}
}
}
impl LinkIds {
fn unique(&mut self, base: String) -> String {
let Some(mut suffix) = self.0.get(&base).map(|link| link.next_suffix) else {
return base;
};
let unique = loop {
let candidate = format!("{base}-{suffix}");
suffix = suffix.saturating_add(1);
if !self.0.contains_key(&candidate) {
break candidate;
}
};
if let Some(link) = self.0.get_mut(&base) {
link.next_suffix = suffix;
}
unique
}
fn entry<'x, I: JSCalendarId, B: JSCalendarId>(
&mut self,
links: &'x mut Map<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
key: String,
value: impl FnOnce() -> Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
) -> Option<&'x mut Value<'static, JSCalendarProperty<I>, JSCalendarValue<I, B>>> {
let position = match self.0.get(&key) {
Some(link) => link.position,
None => {
let position = links.len();
links.insert_unchecked(Key::Owned(key.clone()), value());
self.0.insert(
key,
LinkId {
position,
next_suffix: 2,
},
);
position
}
};
links.as_mut_vec().get_mut(position).map(|(_, value)| value)
}
}
impl ICalendar {
pub(super) fn binary_link_sizes(&self) -> impl Iterator<Item = usize> + '_ {
self.blob_binaries().map(<[u8]>::len)
}
}
impl ICalendarEntry {
pub fn participant_id(&self) -> Option<Cow<'_, str>> {
if let Some(jsid) = self.jsid() {
return Some(Cow::Borrowed(jsid));
}
match self.values.first()? {
ICalendarValue::Text(address) => Some(Cow::Owned(uuid5(address))),
ICalendarValue::Uri(Uri::Location(address)) => Some(Cow::Owned(uuid5(address))),
ICalendarValue::Uri(uri) => Some(Cow::Owned(uuid5(uri.to_unwrapped_string()))),
_ => None,
}
}
}
pub(super) struct ICalendarBinary {
pub(super) data: Vec<u8>,
pub(super) content_type: Option<String>,
pub(super) is_data_uri: bool,
}
impl ICalendarBinary {
pub(super) fn into_value(self) -> ICalendarValue {
if self.is_data_uri {
ICalendarValue::Uri(Uri::Data(Box::new(Data {
content_type: self.content_type,
data: self.data,
})))
} else {
ICalendarValue::Binary(self.data)
}
}
}
impl ICalendarValue {
pub(super) fn is_binary(&self) -> bool {
self.binary_bytes().is_some()
}
pub(super) fn into_binary(self) -> Option<ICalendarBinary> {
match self {
ICalendarValue::Binary(data) => Some(ICalendarBinary {
data,
content_type: None,
is_data_uri: false,
}),
ICalendarValue::Uri(Uri::Data(data)) => Some(ICalendarBinary {
data: data.data,
content_type: data.content_type,
is_data_uri: true,
}),
_ => None,
}
}
pub(super) fn uri_to_string(self, media_type: Option<String>) -> Self {
match self {
ICalendarValue::Uri(uri) => ICalendarValue::Text(uri.into_unwrapped_string()),
ICalendarValue::Binary(data) => ICalendarValue::Text(
Uri::Data(Box::new(Data {
content_type: media_type,
data,
}))
.into_unwrapped_string(),
),
other => other,
}
}
}
+510
View File
@@ -0,0 +1,510 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
pub mod export;
pub mod ext;
pub mod import;
#[doc(hidden)]
pub mod overrides;
pub mod parser;
pub mod types;
use crate::{
common::{
CalendarScale, IanaString, LinkRelation, elements::Elements, jsprop::text::AsciiString,
},
icalendar::{
ICalendarComponentType, ICalendarDuration, ICalendarFrequency, ICalendarMethod,
ICalendarMonth, ICalendarSkip, ICalendarWeekday,
},
};
use jmap_tools::{JsonPointer, Key, Map, Value};
use serde::Serialize;
use sha1::{Digest, Sha1};
use std::{borrow::Cow, fmt::Debug, fmt::Display, hash::Hash, str::FromStr};
use uuid::{Builder, fmt::Hyphenated};
pub const JSCALENDAR_VERSION: &str = "2.0";
pub(crate) const MAX_ICAL_COMPONENT_DEPTH: usize = 32;
const RFC3339_CAPACITY: usize = 21;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[repr(transparent)]
pub struct JSCalendar<'x, I: JSCalendarId, B: JSCalendarId>(
pub Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>,
);
pub trait JSCalendarId:
FromStr + Sized + Serialize + Display + Clone + Eq + Hash + Ord + Debug + Default
{
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarValue<I: JSCalendarId, B: JSCalendarId> {
Type(JSCalendarType),
DateTime(JSCalendarDateTime),
Duration(ICalendarDuration),
AlertAction(JSCalendarAlertAction),
FreeBusyStatus(JSCalendarFreeBusyStatus),
ParticipantKind(JSCalendarParticipantKind),
ParticipationStatus(JSCalendarParticipationStatus),
Privacy(JSCalendarPrivacy),
Progress(JSCalendarProgress),
RelativeTo(JSCalendarRelativeTo),
ScheduleAgent(JSCalendarScheduleAgent),
EventStatus(JSCalendarEventStatus),
LinkRelation(LinkRelation),
Frequency(ICalendarFrequency),
CalendarScale(CalendarScale),
Skip(ICalendarSkip),
Weekday(ICalendarWeekday),
Month(ICalendarMonth),
Method(ICalendarMethod),
Id(I),
BlobId(B),
IdReference(String),
}
impl<I: JSCalendarId, B: JSCalendarId> Default for JSCalendarValue<I, B> {
fn default() -> Self {
JSCalendarValue::Type(JSCalendarType::default())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct JSCalendarDateTime {
pub timestamp: i64,
pub is_local: bool,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarProperty<I: JSCalendarId> {
// JMAP for Calendar Properties
Id,
BaseEventId,
CalendarIds,
IsDraft,
IsOrigin,
UtcStart,
UtcEnd,
UseDefaultAlerts,
MayInviteSelf,
MayInviteOthers,
HideAttendees,
BlobId,
Metadata,
PrivateMetadata,
// JSCalendar Properties
#[default]
Type,
Acknowledged,
Action,
Alerts,
ByDay,
ByHour,
ByMinute,
ByMonth,
ByMonthDay,
BySecond,
BySetPosition,
ByWeekNo,
ByYearDay,
CalendarAddress,
Categories,
Color,
ContentType,
Coordinates,
Count,
Created,
Day,
DelegatedFrom,
DelegatedTo,
Description,
DescriptionContentType,
Display,
Due,
Duration,
Email,
Entries,
EstimatedDuration,
Excluded,
ExpectReply,
Features,
FirstDayOfWeek,
FreeBusyStatus,
Frequency,
Href,
Interval,
InvitedBy,
Keywords,
Kind,
Links,
Locale,
Locations,
LocationTypes,
MemberOf,
Method,
Name,
NthOfPeriod,
Offset,
Participants,
ParticipationComment,
ParticipationStatus,
PercentComplete,
Priority,
Privacy,
ProdId,
Progress,
RecurrenceId,
RecurrenceIdTimeZone,
RecurrenceOverrides,
Rel,
RelatedTo,
Relation,
RelativeTo,
ReplyTo,
RequestStatus,
Roles,
Rscale,
SentBy,
ScheduleAgent,
ScheduleForceSend,
ScheduleSequence,
ScheduleStatus,
ScheduleUpdated,
SendTo,
Sequence,
ShowWithoutTime,
Size,
Skip,
Source,
Start,
Status,
TimeZone,
Title,
Trigger,
Uid,
Until,
Updated,
Uri,
Version,
VirtualLocations,
When,
EndTimeZone,
MainLocationId,
OrganizerCalendarAddress,
RecurrenceRule,
ICalendar,
Properties,
Parameters,
ConvertedProperties,
ValueType,
Components,
DateTime(JSCalendarDateTime),
LinkDisplay(JSCalendarLinkDisplay),
VirtualLocationFeature(JSCalendarVirtualLocationFeature),
ParticipantRole(JSCalendarParticipantRole),
RelationValue(JSCalendarRelation),
LinkRelation(LinkRelation),
Pointer(JsonPointer<JSCalendarProperty<I>>),
IdValue(I),
IdReference(String),
}
impl<T> JSCalendarId for T where
T: FromStr + Sized + Serialize + Display + Clone + Eq + Hash + Ord + Debug + Default
{
}
impl<I: JSCalendarId> From<I> for JSCalendarProperty<I> {
fn from(id: I) -> Self {
JSCalendarProperty::IdValue(id)
}
}
impl<I: JSCalendarId, B: JSCalendarId> From<I> for JSCalendarValue<I, B> {
fn from(id: I) -> Self {
JSCalendarValue::Id(id)
}
}
impl<'x, I: JSCalendarId, B: JSCalendarId> Default for JSCalendar<'x, I, B> {
fn default() -> Self {
Self(Value::Object(Map::from(vec![
(
Key::Property(JSCalendarProperty::Type),
Value::Element(JSCalendarValue::Type(JSCalendarType::Group)),
),
(
Key::Property(JSCalendarProperty::Entries),
Value::Array(vec![]),
),
])))
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarType {
#[default]
Event,
Task,
Group,
Alert,
Boolean,
Duration,
Id,
Int,
LocalDateTime,
Link,
Location,
NDay,
Number,
Participant,
PatchObject,
RecurrenceRule,
Relation,
SignedDuration,
String,
TimeZone,
TimeZoneId,
TimeZoneRule,
UnsignedInt,
UTCDateTime,
VirtualLocation,
ICalComponent,
OffsetTrigger,
AbsoluteTrigger,
}
// JSCalendar Enum Values for action (Context: Alert)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarAlertAction {
Display,
Email,
}
// JSCalendar Enum Values for display (Context: Link)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarLinkDisplay {
Badge,
Graphic,
Fullsize,
Thumbnail,
}
// JSCalendar Enum Values for features (Context: VirtualLocation)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarVirtualLocationFeature {
Audio,
Chat,
Feed,
Moderator,
Phone,
Screen,
Video,
}
// JSCalendar Enum Values for freeBusyStatus (Context: Event, Task)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarFreeBusyStatus {
Free,
Busy,
}
// JSCalendar Enum Values for kind (Context: Participant)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarParticipantKind {
Individual,
Group,
Resource,
Location,
}
// JSCalendar Enum Values for participationStatus (Context: Participant)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarParticipationStatus {
NeedsAction,
Accepted,
Declined,
Tentative,
Delegated,
}
// JSCalendar Enum Values for privacy (Context: Event, Task)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarPrivacy {
Public,
Private,
Secret,
}
// JSCalendar Enum Values for progress (Context: Task, Participant)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarProgress {
NeedsAction,
InProcess,
Completed,
Failed,
Cancelled,
}
// JSCalendar Enum Values for relation (Context: Relation)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarRelation {
First,
Next,
Child,
Parent,
Snooze,
}
// JSCalendar Enum Values for relativeTo (Context: OffsetTrigger, Location)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarRelativeTo {
Start,
End,
}
// JSCalendar Enum Values for roles (Context: Participant)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarParticipantRole {
Owner,
Optional,
Informational,
Chair,
Required,
}
// JSCalendar Enum Values for scheduleAgent (Context: Participant)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarScheduleAgent {
Server,
Client,
None,
}
// JSCalendar Enum Values for status (Context: Event)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSCalendarEventStatus {
Confirmed,
Cancelled,
Tentative,
}
impl JSCalendarDateTime {
pub fn new(timestamp: i64, is_local: bool) -> Self {
Self {
timestamp,
is_local,
}
}
pub fn is_start_of_day(&self) -> bool {
self.timestamp.rem_euclid(86400) == 0
}
pub fn to_rfc3339(&self) -> String {
let mut text = AsciiString::with_capacity(RFC3339_CAPACITY);
let _ = self.push_rfc3339(&mut text);
text.into_string()
}
}
impl<I: JSCalendarId, B: JSCalendarId> JSCalendar<'_, I, B> {
pub fn blob_ids(&self) -> impl Iterator<Item = &B> {
Elements::new(&self.0).filter_map(|element| match element {
JSCalendarValue::BlobId(blob_id) => Some(blob_id),
_ => None,
})
}
}
impl ICalendarComponentType {
pub fn to_jscalendar_type(&self) -> Option<JSCalendarType> {
match &self {
ICalendarComponentType::VCalendar => Some(JSCalendarType::Group),
ICalendarComponentType::VEvent => Some(JSCalendarType::Event),
ICalendarComponentType::VTodo => Some(JSCalendarType::Task),
ICalendarComponentType::VAlarm => Some(JSCalendarType::Alert),
ICalendarComponentType::Participant => Some(JSCalendarType::Participant),
ICalendarComponentType::VLocation => Some(JSCalendarType::Location),
ICalendarComponentType::Standard
| ICalendarComponentType::Daylight
| ICalendarComponentType::VAvailability
| ICalendarComponentType::Available
| ICalendarComponentType::VResource
| ICalendarComponentType::VStatus
| ICalendarComponentType::VJournal
| ICalendarComponentType::VFreebusy
| ICalendarComponentType::VTimezone
| ICalendarComponentType::Other(_) => None,
}
}
}
impl JSCalendarType {
pub fn to_icalendar_component_type(&self) -> Option<ICalendarComponentType> {
match &self {
JSCalendarType::Group => Some(ICalendarComponentType::VCalendar),
JSCalendarType::Event => Some(ICalendarComponentType::VEvent),
JSCalendarType::Task => Some(ICalendarComponentType::VTodo),
JSCalendarType::Alert => Some(ICalendarComponentType::VAlarm),
JSCalendarType::Participant => Some(ICalendarComponentType::Participant),
JSCalendarType::Location => Some(ICalendarComponentType::VLocation),
_ => None,
}
}
}
// 7f1e1965-ae73-4454-b088-232c90730ce2
static JSCAL_NAMESPACE: uuid::Uuid = uuid::Uuid::from_bytes([
127, 30, 25, 101, 174, 115, 68, 84, 176, 136, 35, 44, 144, 115, 12, 226,
]);
pub(crate) trait Uuid5 {
fn uuid5_hyphenated<'b>(&self, buffer: &'b mut [u8; Hyphenated::LENGTH]) -> &'b str;
}
#[inline]
pub fn uuid5(text: impl AsRef<[u8]>) -> String {
let mut buffer = [0u8; Hyphenated::LENGTH];
text.as_ref().uuid5_hyphenated(&mut buffer).to_string()
}
impl Uuid5 for [u8] {
#[inline]
fn uuid5_hyphenated<'b>(&self, buffer: &'b mut [u8; Hyphenated::LENGTH]) -> &'b str {
let digest: [u8; 20] = Sha1::new()
.chain_update(JSCAL_NAMESPACE.as_bytes())
.chain_update(self)
.finalize()
.into();
let [bytes @ .., _, _, _, _] = digest;
Builder::from_sha1_bytes(bytes)
.into_uuid()
.hyphenated()
.encode_lower(buffer)
}
}
#[cfg(all(test, inbuxa_tests))]
impl<I: JSCalendarId, B: JSCalendarId> std::fmt::Display for JSCalendar<'_, I, B> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = serde_json::to_string_pretty(&self.0).map_err(|_| std::fmt::Error)?;
write!(f, "{}", s)
}
}
impl<I: JSCalendarId> Display for JSCalendarProperty<I> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.to_string().as_ref())
}
}
+693
View File
@@ -0,0 +1,693 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{IanaParse, elements::Elements, jsprop::text::ConvertedKeys},
icalendar::ICalendarProperty,
jscalendar::{
JSCalendarDateTime, JSCalendarId, JSCalendarProperty, JSCalendarValue,
ext::{JSCalendarKeyExt, JSCalendarObjectExt, JSCalendarPatch},
},
};
use ahash::AHashMap;
use jmap_tools::{JsonPointerHandler, JsonPointerItem, Key, Map, Value};
use std::{borrow::Cow, ptr, str::FromStr};
type JSCalendarMap<'x, I, B> = Map<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>;
type JSCalendarObject<'x, I, B> = Value<'x, JSCalendarProperty<I>, JSCalendarValue<I, B>>;
#[derive(Debug, Clone, Copy, Default)]
struct SeriesDates {
start: Option<JSCalendarDateTime>,
due: Option<JSCalendarDateTime>,
}
pub(crate) struct OverrideTemplate<'x, I: JSCalendarId, B: JSCalendarId> {
entries: JSCalendarMap<'x, I, B>,
dates: SeriesDates,
has_participants: bool,
uses: usize,
}
impl SeriesDates {
fn new<I: JSCalendarId, B: JSCalendarId>(base: &JSCalendarMap<'_, I, B>) -> Self {
let mut dates = SeriesDates::default();
for (key, value) in base.iter() {
match (key, value) {
(
Key::Property(JSCalendarProperty::Start),
Value::Element(JSCalendarValue::DateTime(dt)),
) => dates.start = Some(*dt),
(
Key::Property(JSCalendarProperty::Due),
Value::Element(JSCalendarValue::DateTime(dt)),
) => dates.due = Some(*dt),
_ => {}
}
}
dates
}
fn instance(
&self,
recurrence_id: JSCalendarDateTime,
) -> (Option<JSCalendarDateTime>, Option<JSCalendarDateTime>) {
let Some(start) = self.start else {
return (None, self.due);
};
let rid = JSCalendarDateTime::new(recurrence_id.timestamp, true);
(
Some(rid),
self.due.map(|due| {
JSCalendarDateTime::new(
due.timestamp + (rid.timestamp - start.timestamp),
due.is_local,
)
}),
)
}
}
trait OverrideMapExt<I: JSCalendarId>: Sized {
fn last_object(&self, property: &JSCalendarProperty<I>) -> Option<&Self>;
fn converted_override_dates(&self) -> Vec<JSCalendarDateTime>;
fn reaches_template(&self, is_converted: impl FnMut() -> bool) -> bool;
}
impl<I: JSCalendarId, B: JSCalendarId> OverrideMapExt<I> for JSCalendarMap<'_, I, B> {
fn last_object(&self, property: &JSCalendarProperty<I>) -> Option<&Self> {
self.as_vec()
.iter()
.rev()
.find_map(|(key, value)| match (key, value) {
(Key::Property(key), Value::Object(obj)) if key == property => Some(obj),
_ => None,
})
}
fn converted_override_dates(&self) -> Vec<JSCalendarDateTime> {
self.last_object(&JSCalendarProperty::ICalendar)
.into_iter()
.flat_map(|ical| ical.iter())
.filter_map(|(key, value)| match (key, value) {
(Key::Property(JSCalendarProperty::ConvertedProperties), Value::Object(props)) => {
Some(props.keys())
}
_ => None,
})
.flatten()
.filter_map(|key| match key.clone().into_converted_keys().as_slice() {
[
Key::Property(JSCalendarProperty::RecurrenceOverrides),
Key::Property(JSCalendarProperty::DateTime(dt)),
..,
] => Some(*dt),
_ => None,
})
.collect()
}
fn reaches_template(&self, mut is_converted: impl FnMut() -> bool) -> bool {
let exported = || {
self.iter().filter(|(key, value)| {
!key.is_forbidden_override_key()
&& (!matches!(key, Key::Property(JSCalendarProperty::Excluded))
|| matches!(value, Value::Bool(true)))
})
};
let mut members = exported();
match (members.next(), members.next()) {
(None, _) => false,
(Some((key, Value::Element(JSCalendarValue::Duration(_)))), None)
if key.same_key(&Key::Property(JSCalendarProperty::Duration)) && is_converted() =>
{
false
}
_ => !exported().any(|(key, value)| {
matches!(value, Value::Bool(true))
&& key.same_key(&Key::Property(JSCalendarProperty::Excluded))
}),
}
}
}
impl<'x, I: JSCalendarId, B: JSCalendarId> OverrideTemplate<'x, I, B> {
pub(crate) fn uses(
base: &JSCalendarMap<'x, I, B>,
overrides: &JSCalendarMap<'x, I, B>,
) -> usize {
let instances = overrides
.values()
.filter(|patch| patch.is_instance_patch())
.count();
if instances == 0
|| !base
.last_object(&JSCalendarProperty::RecurrenceOverrides)
.is_some_and(|exported| ptr::eq(exported, overrides))
{
return instances;
}
let mut converted = None;
let has_instance = overrides.iter().any(|(key, patch)| match (key, patch) {
(Key::Property(JSCalendarProperty::DateTime(dt)), Value::Object(patch)) => patch
.reaches_template(|| {
converted
.get_or_insert_with(|| base.converted_override_dates())
.contains(dt)
}),
_ => false,
});
if has_instance { instances } else { 0 }
}
pub(crate) fn new(base: &JSCalendarMap<'x, I, B>, uses: usize) -> Self {
let mut entries = Map::from(Vec::with_capacity(base.len()));
let mut has_participants = false;
for (key, value) in base.iter() {
match (key, value) {
(Key::Property(property), _) if property.is_series_property() => {}
(Key::Property(JSCalendarProperty::ICalendar), Value::Object(obj)) => {
if let Some(obj) = instance_ical_clone(obj) {
entries.insert_unchecked(key.clone(), Value::Object(obj));
}
}
(Key::Property(JSCalendarProperty::Participants), Value::Object(obj)) => {
has_participants = !obj.is_empty();
entries.insert_unchecked(key.clone(), value.clone());
}
_ => {
entries.insert_unchecked(key.clone(), value.clone());
}
}
}
Self {
entries,
dates: SeriesDates::new(base),
has_participants,
uses,
}
}
pub(crate) fn blob_uses(&self) -> impl Iterator<Item = &B> {
Elements::from_values(self.entries.values())
.filter_map(|element| match element {
JSCalendarValue::BlobId(blob_id) => Some(blob_id),
_ => None,
})
.flat_map(|blob_id| std::iter::repeat_n(blob_id, self.uses))
}
pub(crate) fn instance(
&mut self,
recurrence_id: JSCalendarDateTime,
) -> JSCalendarMap<'x, I, B> {
let (start, due) = self.dates.instance(recurrence_id);
for (property, dt) in [
(JSCalendarProperty::Start, start),
(JSCalendarProperty::Due, due),
] {
if let Some(dt) = dt
&& let Some(value) = self.entries.lookup_mut(&Key::Property(property))
{
*value = Value::Element(JSCalendarValue::DateTime(dt));
}
}
self.uses = self.uses.saturating_sub(1);
if self.uses == 0 {
std::mem::take(&mut self.entries)
} else {
self.entries.clone()
}
}
pub(crate) fn apply_patch(
&self,
instance: JSCalendarMap<'x, I, B>,
patch: JSCalendarMap<'x, I, B>,
) -> Result<JSCalendarMap<'x, I, B>, String> {
let mut instance = Value::Object(instance);
let mut invalid_pointer = None;
for (key, value) in patch.into_vec() {
match key {
Key::Property(JSCalendarProperty::Pointer(pointer)) => {
if !instance.patch_jptr(pointer.iter(), value) && invalid_pointer.is_none() {
invalid_pointer = Some(pointer.to_string());
}
}
Key::Property(JSCalendarProperty::Excluded) => {}
key => {
if let Value::Object(entries) = &mut instance {
if value.is_null() {
if let Some(pos) = entries.key_position(&key) {
entries.as_mut_vec().remove(pos);
}
} else {
entries.upsert(key, value);
}
}
}
}
}
let Value::Object(mut instance) = instance else {
return Err(invalid_pointer.unwrap_or_default());
};
if self.has_participants
&& instance
.lookup(&Key::Property(JSCalendarProperty::Participants))
.and_then(Value::as_object)
.is_none_or(|participants| participants.is_empty())
{
instance.as_mut_vec().retain(|(key, _)| {
!matches!(
key,
Key::Property(
JSCalendarProperty::OrganizerCalendarAddress | JSCalendarProperty::SentBy
)
)
});
}
match invalid_pointer {
Some(pointer) => Err(pointer),
None => Ok(instance),
}
}
}
fn instance_ical_clone<'x, I: JSCalendarId, B: JSCalendarId>(
obj: &JSCalendarMap<'x, I, B>,
) -> Option<JSCalendarMap<'x, I, B>> {
let mut obj = obj.clone();
retain_instance_ical(&mut obj).then_some(obj)
}
fn retain_instance_ical<I: JSCalendarId, B: JSCalendarId>(
obj: &mut JSCalendarMap<'_, I, B>,
) -> bool {
let mut has_contents = false;
obj.as_mut_vec().retain_mut(|(key, value)| {
let keep = match (key, value) {
(Key::Property(JSCalendarProperty::ConvertedProperties), Value::Object(props)) => {
props
.as_mut_vec()
.retain(|(key, _)| !key.is_series_converted_property());
!props.is_empty()
}
(Key::Property(JSCalendarProperty::Properties), Value::Array(props)) => {
props.retain(|property| !is_series_ical_property(property));
!props.is_empty()
}
(Key::Property(JSCalendarProperty::Name), _) => return true,
_ => true,
};
has_contents |= keep;
keep
});
has_contents
}
fn is_series_ical_property<I: JSCalendarId, B: JSCalendarId>(
property: &JSCalendarObject<'_, I, B>,
) -> bool {
property
.as_array()
.and_then(|items| items.first())
.and_then(Value::as_str)
.is_some_and(|name| {
matches!(
ICalendarProperty::parse(name.as_bytes()),
Some(
ICalendarProperty::Rrule
| ICalendarProperty::Rdate
| ICalendarProperty::Exdate
| ICalendarProperty::Exrule
| ICalendarProperty::RecurrenceId
)
)
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Inherited {
Nothing,
Start,
StartAndEnd,
}
impl Inherited {
fn covers<I: JSCalendarId>(self, property: &JSCalendarProperty<I>) -> bool {
match (self, property) {
(Inherited::Nothing, _) => false,
(
_,
JSCalendarProperty::Start
| JSCalendarProperty::TimeZone
| JSCalendarProperty::ShowWithoutTime,
) => true,
(
Inherited::StartAndEnd,
JSCalendarProperty::Duration
| JSCalendarProperty::Due
| JSCalendarProperty::EndTimeZone,
) => true,
_ => false,
}
}
fn covers_key<I: JSCalendarId>(self, key: &Key<'_, JSCalendarProperty<I>>) -> bool {
match key {
Key::Property(JSCalendarProperty::Pointer(pointer)) => {
let mut items = pointer
.iter()
.filter(|item| !matches!(item, JsonPointerItem::Root));
matches!(
(items.next(), items.next()),
(Some(JsonPointerItem::Key(Key::Property(property))), None)
if self.covers(property)
)
}
Key::Property(property) => self.covers(property),
Key::Borrowed(_) | Key::Owned(_) => {
JSCalendarProperty::<I>::from_str(key.to_string().as_ref())
.is_ok_and(|property| self.covers(&property))
}
}
}
}
pub struct OverrideDiff<'a, I: JSCalendarId, B: JSCalendarId> {
base: &'a JSCalendarMap<'static, I, B>,
base_ical: Option<JSCalendarMap<'static, I, B>>,
dates: SeriesDates,
}
impl<'a, I: JSCalendarId, B: JSCalendarId> OverrideDiff<'a, I, B> {
pub fn new(base: &'a JSCalendarMap<'static, I, B>) -> Self {
Self {
base,
base_ical: base
.lookup(&Key::Property(JSCalendarProperty::ICalendar))
.and_then(Value::as_object)
.and_then(instance_ical_clone),
dates: SeriesDates::new(base),
}
}
pub fn diff(
&self,
recurrence_id: JSCalendarDateTime,
mut instance: JSCalendarMap<'static, I, B>,
inherited: Inherited,
) -> JSCalendarMap<'static, I, B> {
let (start, due) = self.dates.instance(recurrence_id);
let mut patch = Map::from(Vec::new());
let ical_key = Key::Property(JSCalendarProperty::ICalendar);
for (key, _) in self.base.iter() {
if !key.is_ignored_override_key()
&& !matches!(
key,
Key::Property(JSCalendarProperty::Start | JSCalendarProperty::ICalendar)
)
&& !inherited.covers_key(key)
&& instance.key_position(key).is_none()
{
patch.insert_unchecked(key.to_owned(), Value::Null);
}
}
if inherited != Inherited::Nothing
&& let Some(base_ical) = &self.base_ical
{
inherit_date_conversions(&mut instance, base_ical, inherited);
}
let has_ical = match instance.lookup_mut(&ical_key) {
Some(Value::Object(obj)) => retain_instance_ical(obj),
_ => false,
};
if !has_ical {
if let Some(pos) = instance.key_position(&ical_key) {
instance.as_mut_vec().remove(pos);
}
if self.base_ical.is_some() {
patch.insert_unchecked(ical_key, Value::Null);
}
}
for (key, value) in instance.into_vec() {
if key.is_ignored_override_key() {
continue;
}
let base_value = match (&key, &value) {
(
Key::Property(JSCalendarProperty::Start),
Value::Element(JSCalendarValue::DateTime(dt)),
) if start == Some(*dt) => continue,
(
Key::Property(JSCalendarProperty::Due),
Value::Element(JSCalendarValue::DateTime(dt)),
) if due == Some(*dt) => continue,
(Key::Property(JSCalendarProperty::ICalendar), Value::Object(ical))
if self
.base_ical
.as_ref()
.is_some_and(|base| same_members(base, ical)) =>
{
continue;
}
(
Key::Property(
JSCalendarProperty::Start
| JSCalendarProperty::Due
| JSCalendarProperty::ICalendar,
),
_,
) => None,
(key, _) => self.base.lookup(key),
};
match (base_value, key, value) {
(
Some(Value::Object(base_members)),
Key::Property(property),
Value::Object(members),
) if property.is_member_map() => {
diff_members(&mut patch, property, base_members, members);
}
(Some(base_value), _, value) if same_value(base_value, &value) => {}
(_, key, value) => {
patch.insert_unchecked(key, value);
}
}
}
patch
}
}
fn inherit_date_conversions<I: JSCalendarId, B: JSCalendarId>(
instance: &mut JSCalendarMap<'static, I, B>,
base_ical: &JSCalendarMap<'static, I, B>,
inherited: Inherited,
) {
let converted_key = Key::Property(JSCalendarProperty::ConvertedProperties);
let Some(Value::Object(base_converted)) = base_ical.lookup(&converted_key) else {
return;
};
let mut conversions = base_converted
.iter()
.filter(|(key, _)| inherited.covers_key(key))
.peekable();
if conversions.peek().is_none() {
return;
}
let Value::Object(ical) = instance.upsert_or_get_mut(
Key::Property(JSCalendarProperty::ICalendar),
Value::new_object,
) else {
return;
};
let name_key = Key::Property(JSCalendarProperty::Name);
if ical.key_position(&name_key).is_none()
&& let Some(name) = base_ical.lookup(&name_key)
{
ical.insert_unchecked(name_key, name.clone());
}
let Value::Object(converted) = ical.upsert_or_get_mut(converted_key, Value::new_object) else {
return;
};
for (key, value) in conversions {
if converted.key_position(key).is_none() {
converted.insert_unchecked(key.clone(), value.clone());
}
}
}
fn diff_members<I: JSCalendarId, B: JSCalendarId>(
patch: &mut JSCalendarMap<'static, I, B>,
property: JSCalendarProperty<I>,
base_members: &JSCalendarMap<'static, I, B>,
members: JSCalendarMap<'static, I, B>,
) {
let base_members = base_members.as_vec();
let mut matched = vec![false; base_members.len()];
let mut next_pos = 0;
let mut index: Option<AHashMap<Cow<'_, str>, usize>> = None;
for (id, member) in members.into_vec() {
let pos = if base_members
.get(next_pos)
.is_some_and(|(base_id, _)| base_id.same_key(&id))
{
Some(next_pos)
} else {
index
.get_or_insert_with(|| {
base_members
.iter()
.enumerate()
.map(|(pos, (id, _))| (id.to_string(), pos))
.collect()
})
.get(id.to_string().as_ref())
.copied()
};
let Some((pos, base_member)) =
pos.and_then(|pos| base_members.get(pos).map(|(_, member)| (pos, member)))
else {
patch.insert_unchecked(property.member_pointer([id]), member);
continue;
};
if let Some(matched) = matched.get_mut(pos) {
*matched = true;
}
next_pos = pos + 1;
match (base_member, member) {
(base_member, member) if same_value(base_member, &member) => {}
(Value::Object(base_member), Value::Object(member))
if property == JSCalendarProperty::Participants
&& base_member.lookup(&Key::Property(JSCalendarProperty::CalendarAddress))
!= member.lookup(&Key::Property(JSCalendarProperty::CalendarAddress)) =>
{
patch.insert_unchecked(property.member_pointer([id]), Value::Object(member));
}
(Value::Object(base_member), Value::Object(member)) => {
for (sub_property, _) in base_member.iter() {
if member.key_position(sub_property).is_none() {
patch.insert_unchecked(
property.member_pointer([id.clone(), sub_property.to_owned()]),
Value::Null,
);
}
}
for (sub_property, value) in member.into_vec() {
match (base_member.lookup(&sub_property), value) {
(Some(base_value), value) if same_value(base_value, &value) => {}
(Some(Value::Object(base_set)), Value::Object(set))
if is_boolean_set(base_set) && is_boolean_set(&set) =>
{
for (key, _) in base_set.iter() {
if set.key_position(key).is_none() {
patch.insert_unchecked(
property.member_pointer([
id.clone(),
sub_property.clone(),
key.to_owned(),
]),
Value::Null,
);
}
}
for (key, value) in set.into_vec() {
if !base_set
.lookup(&key)
.is_some_and(|base| same_value(base, &value))
{
patch.insert_unchecked(
property.member_pointer([
id.clone(),
sub_property.clone(),
key,
]),
value,
);
}
}
}
(_, value) => {
patch.insert_unchecked(
property.member_pointer([id.clone(), sub_property]),
value,
);
}
}
}
}
(_, member) => {
patch.insert_unchecked(property.member_pointer([id]), member);
}
}
}
for ((id, _), matched) in base_members.iter().zip(matched) {
if !matched {
patch.insert_unchecked(property.member_pointer([id.to_owned()]), Value::Null);
}
}
}
fn same_value<I: JSCalendarId, B: JSCalendarId>(
a: &JSCalendarObject<'_, I, B>,
b: &JSCalendarObject<'_, I, B>,
) -> bool {
match (a, b) {
(Value::Object(a), Value::Object(b)) => same_members(a, b),
(Value::Array(a), Value::Array(b)) => {
a.len() == b.len() && a.iter().zip(b).all(|(a, b)| same_value(a, b))
}
_ => a == b,
}
}
fn same_members<I: JSCalendarId, B: JSCalendarId>(
a: &JSCalendarMap<'_, I, B>,
b: &JSCalendarMap<'_, I, B>,
) -> bool {
if a.len() != b.len() {
return false;
}
for (pos, ((a_key, a_value), (b_key, b_value))) in a.iter().zip(b.iter()).enumerate() {
if !a_key.same_key(b_key) {
let index = b
.iter()
.skip(pos)
.map(|(key, value)| (key.to_string(), value))
.collect::<AHashMap<_, _>>();
return index.len() == a.len() - pos
&& a.iter().skip(pos).all(|(key, a_value)| {
index
.get(key.to_string().as_ref())
.is_some_and(|b_value| same_value(a_value, b_value))
});
} else if !same_value(a_value, b_value) {
return false;
}
}
true
}
fn is_boolean_set<I: JSCalendarId, B: JSCalendarId>(set: &JSCalendarMap<'_, I, B>) -> bool {
set.values().all(|value| matches!(value, Value::Bool(_)))
}
+550
View File
@@ -0,0 +1,550 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{
CalendarScale, IanaParse, IanaString, IdReference, LinkRelation,
format::AsciiPush,
jsprop::text::{DisplayEq, Expect, IdReferenceText, PointerText, PointerTextExt},
},
icalendar::{
ICalendarDuration, ICalendarFrequency, ICalendarMethod, ICalendarMonth, ICalendarSkip,
ICalendarWeekday,
},
jscalendar::{
JSCalendar, JSCalendarAlertAction, JSCalendarDateTime, JSCalendarEventStatus,
JSCalendarFreeBusyStatus, JSCalendarId, JSCalendarLinkDisplay, JSCalendarParticipantKind,
JSCalendarParticipantRole, JSCalendarParticipationStatus, JSCalendarPrivacy,
JSCalendarProgress, JSCalendarProperty, JSCalendarRelation, JSCalendarRelativeTo,
JSCalendarScheduleAgent, JSCalendarType, JSCalendarValue, JSCalendarVirtualLocationFeature,
},
};
use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, PointerDepth, Property as _, Value};
use mail_parser::DateTime;
use serde::Serializer;
use std::{
borrow::Cow,
fmt::{self, Write},
mem::discriminant,
ops::RangeInclusive,
str::{FromStr, from_utf8},
};
const FOUR_DIGIT_YEARS: RangeInclusive<i64> = -62_167_219_200..=253_402_300_799;
pub(crate) const VALUE_TEXT: usize = 64;
pub(crate) const KEY_TEXT: usize = 128;
impl<'x, I: JSCalendarId, B: JSCalendarId> JSCalendar<'x, I, B> {
pub fn parse(json: &'x str) -> Result<Self, String> {
Value::parse_json(json).map(JSCalendar)
}
pub fn to_string_pretty(&self) -> String {
serde_json::to_string_pretty(&self.0).unwrap_or_default()
}
}
impl<I: JSCalendarId, B: JSCalendarId> Element for JSCalendarValue<I, B> {
type Property = JSCalendarProperty<I>;
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
if let Key::Property(prop) = key {
match prop.patch_or_prop() {
JSCalendarProperty::Type => JSCalendarType::from_str(value)
.ok()
.map(JSCalendarValue::Type),
JSCalendarProperty::Created
| JSCalendarProperty::Updated
| JSCalendarProperty::Acknowledged
| JSCalendarProperty::ScheduleUpdated
| JSCalendarProperty::When
| JSCalendarProperty::UtcStart
| JSCalendarProperty::UtcEnd => {
JSCalendarDateTime::from_rfc3339(value, false).map(JSCalendarValue::DateTime)
}
JSCalendarProperty::Due
| JSCalendarProperty::RecurrenceId
| JSCalendarProperty::Start
| JSCalendarProperty::Until => {
JSCalendarDateTime::from_rfc3339(value, true).map(JSCalendarValue::DateTime)
}
JSCalendarProperty::Duration
| JSCalendarProperty::EstimatedDuration
| JSCalendarProperty::Offset => {
ICalendarDuration::parse(value.as_bytes()).map(JSCalendarValue::Duration)
}
JSCalendarProperty::Action => JSCalendarAlertAction::from_str(value)
.ok()
.map(JSCalendarValue::AlertAction),
JSCalendarProperty::FreeBusyStatus => JSCalendarFreeBusyStatus::from_str(value)
.ok()
.map(JSCalendarValue::FreeBusyStatus),
JSCalendarProperty::Kind => JSCalendarParticipantKind::from_str(value)
.ok()
.map(JSCalendarValue::ParticipantKind),
JSCalendarProperty::ParticipationStatus => {
JSCalendarParticipationStatus::from_str(value)
.ok()
.map(JSCalendarValue::ParticipationStatus)
}
JSCalendarProperty::Privacy => JSCalendarPrivacy::from_str(value)
.ok()
.map(JSCalendarValue::Privacy),
JSCalendarProperty::Progress => JSCalendarProgress::from_str(value)
.ok()
.map(JSCalendarValue::Progress),
JSCalendarProperty::RelativeTo => JSCalendarRelativeTo::from_str(value)
.ok()
.map(JSCalendarValue::RelativeTo),
JSCalendarProperty::ScheduleAgent => JSCalendarScheduleAgent::from_str(value)
.ok()
.map(JSCalendarValue::ScheduleAgent),
JSCalendarProperty::Status => JSCalendarEventStatus::from_str(value)
.ok()
.map(JSCalendarValue::EventStatus),
JSCalendarProperty::Rel => {
LinkRelation::parse(value.as_bytes()).map(JSCalendarValue::LinkRelation)
}
JSCalendarProperty::Frequency => {
ICalendarFrequency::parse(value.as_bytes()).map(JSCalendarValue::Frequency)
}
JSCalendarProperty::FirstDayOfWeek | JSCalendarProperty::Day => {
ICalendarWeekday::parse(value.as_bytes()).map(JSCalendarValue::Weekday)
}
JSCalendarProperty::Skip => {
ICalendarSkip::parse(value.as_bytes()).map(JSCalendarValue::Skip)
}
JSCalendarProperty::Rscale => {
CalendarScale::parse(value.as_bytes()).map(JSCalendarValue::CalendarScale)
}
JSCalendarProperty::ByMonth => {
ICalendarMonth::parse(value.as_bytes()).map(JSCalendarValue::Month)
}
JSCalendarProperty::Method => {
ICalendarMethod::parse(value.as_bytes()).map(JSCalendarValue::Method)
}
JSCalendarProperty::Id | JSCalendarProperty::BaseEventId => {
match IdReference::parse(value) {
IdReference::Value(value) => JSCalendarValue::Id(value).into(),
IdReference::Reference(value) => JSCalendarValue::IdReference(value).into(),
IdReference::Error => None,
}
}
JSCalendarProperty::BlobId => match IdReference::parse(value) {
IdReference::Value(value) => JSCalendarValue::BlobId(value).into(),
IdReference::Reference(value) => JSCalendarValue::IdReference(value).into(),
IdReference::Error => None,
},
_ => None,
}
} else if key
.as_string_key()
.is_some_and(|name| name.ends_with(BLOB_ID_SUFFIX))
{
match IdReference::parse(value) {
IdReference::Value(value) => JSCalendarValue::BlobId(value).into(),
IdReference::Reference(value) => JSCalendarValue::IdReference(value).into(),
IdReference::Error => None,
}
} else {
None
}
}
fn to_cow(&self) -> Cow<'static, str> {
match self {
JSCalendarValue::Type(v) => v.as_str().into(),
JSCalendarValue::DateTime(v) => v.to_rfc3339().into(),
JSCalendarValue::Duration(v) => v.to_string().into(),
JSCalendarValue::AlertAction(v) => v.as_str().into(),
JSCalendarValue::FreeBusyStatus(v) => v.as_str().into(),
JSCalendarValue::ParticipantKind(v) => v.as_str().into(),
JSCalendarValue::ParticipationStatus(v) => v.as_str().into(),
JSCalendarValue::Privacy(v) => v.as_str().into(),
JSCalendarValue::Progress(v) => v.as_str().into(),
JSCalendarValue::RelativeTo(v) => v.as_str().into(),
JSCalendarValue::ScheduleAgent(v) => v.as_str().into(),
JSCalendarValue::EventStatus(v) => v.as_str().into(),
JSCalendarValue::LinkRelation(v) => v.as_str().into(),
JSCalendarValue::Frequency(v) => v.as_js_str().into(),
JSCalendarValue::CalendarScale(v) => v.as_js_str().into(),
JSCalendarValue::Skip(v) => v.as_js_str().into(),
JSCalendarValue::Weekday(v) => v.as_js_str().into(),
JSCalendarValue::Month(v) => v.to_string().into(),
JSCalendarValue::Method(v) => v.as_js_str().into(),
JSCalendarValue::Id(v) => v.to_string().into(),
JSCalendarValue::BlobId(v) => v.to_string().into(),
JSCalendarValue::IdReference(s) => s.to_id_reference().into(),
}
}
#[inline]
fn serialize_text<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
JSCalendarValue::DateTime(_)
| JSCalendarValue::Duration(_)
| JSCalendarValue::Month(_)
| JSCalendarValue::Id(_)
| JSCalendarValue::BlobId(_)
| JSCalendarValue::IdReference(_) => self.serialize_data_text(serializer),
_ => serializer.serialize_str(&self.to_cow()),
}
}
}
impl<I: JSCalendarId, B: JSCalendarId> JSCalendarValue<I, B> {
#[inline(never)]
fn serialize_data_text<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
StackText::<VALUE_TEXT>::serialize(
serializer,
|text| match self {
JSCalendarValue::DateTime(date_time) => date_time.push_rfc3339(text),
JSCalendarValue::Duration(duration) => text.push_duration(duration.parts()),
JSCalendarValue::Month(month) => write!(text, "{month}"),
JSCalendarValue::Id(id) => write!(text, "{id}"),
JSCalendarValue::BlobId(id) => write!(text, "{id}"),
JSCalendarValue::IdReference(id) => text.push_id_reference(id),
_ => Err(fmt::Error),
},
|| self.to_cow(),
)
}
}
impl<I: JSCalendarId> jmap_tools::Property for JSCalendarProperty<I> {
fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
Self::try_parse_nested(key, value, PointerDepth::default())
}
fn try_parse_nested(
key: Option<&Key<'_, Self>>,
value: &str,
depth: PointerDepth,
) -> Option<Self> {
match key {
Some(Key::Property(key)) if key.is_metadata() => None,
Some(Key::Property(key)) => match key.patch_or_prop() {
JSCalendarProperty::RecurrenceOverrides => {
JSCalendarDateTime::from_rfc3339(value, true).map(JSCalendarProperty::DateTime)
}
JSCalendarProperty::Display => JSCalendarLinkDisplay::from_str(value)
.ok()
.map(JSCalendarProperty::LinkDisplay),
JSCalendarProperty::Features => JSCalendarVirtualLocationFeature::from_str(value)
.ok()
.map(JSCalendarProperty::VirtualLocationFeature),
JSCalendarProperty::Roles => JSCalendarParticipantRole::from_str(value)
.ok()
.map(JSCalendarProperty::ParticipantRole),
JSCalendarProperty::Relation => JSCalendarRelation::from_str(value)
.ok()
.map(JSCalendarProperty::RelationValue),
JSCalendarProperty::ConvertedProperties => {
JsonPointer::parse_nested(value, depth).map(JSCalendarProperty::Pointer)
}
JSCalendarProperty::DateTime(_) if value.contains('/') => {
JsonPointer::parse_nested(value, depth).map(JSCalendarProperty::Pointer)
}
JSCalendarProperty::DateTime(_) | JSCalendarProperty::Entries => {
JSCalendarProperty::parse_name(value)
}
JSCalendarProperty::CalendarIds => match IdReference::parse(value) {
IdReference::Value(value) => JSCalendarProperty::IdValue(value).into(),
IdReference::Reference(value) => JSCalendarProperty::IdReference(value).into(),
IdReference::Error => None,
},
_ => JSCalendarProperty::parse_nested_name(value),
},
None if value.contains('/') => {
JsonPointer::parse_nested(value, depth).map(JSCalendarProperty::Pointer)
}
None => JSCalendarProperty::parse_name(value),
Some(_) => JSCalendarProperty::parse_nested_name(value),
}
}
fn is_opaque(&self) -> bool {
self.is_metadata()
}
fn to_cow(&self) -> Cow<'static, str> {
match self {
JSCalendarProperty::DateTime(dt) => dt.to_rfc3339().into(),
JSCalendarProperty::Pointer(pointer) => pointer.to_text().into(),
JSCalendarProperty::IdReference(id) => id.to_id_reference().into(),
_ => self.to_string(),
}
}
#[inline]
fn key_eq(&self, other: &Self) -> bool {
match (self, other) {
(a, b) if !a.has_data() && !b.has_data() => discriminant(a) == discriminant(b),
(JSCalendarProperty::DateTime(a), JSCalendarProperty::DateTime(b)) => a.same_rfc3339(b),
_ => self.data_key_eq(other),
}
}
#[inline]
fn key_eq_str(&self, other: &str) -> bool {
if !self.has_data() {
self.to_string() == other
} else {
self.data_key_eq_str(other)
}
}
#[inline]
fn serialize_text<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
JSCalendarProperty::DateTime(_)
| JSCalendarProperty::Pointer(_)
| JSCalendarProperty::IdValue(_)
| JSCalendarProperty::IdReference(_) => self.serialize_data_text(serializer),
_ => serializer.serialize_str(&self.to_cow()),
}
}
}
pub(crate) const BLOB_ID_SUFFIX: &str = "/blobId";
impl<I: JSCalendarId> JSCalendarProperty<I> {
#[inline(never)]
fn data_key_eq(&self, other: &Self) -> bool {
match (self, other) {
(JSCalendarProperty::Pointer(a), JSCalendarProperty::Pointer(b)) if a == b => true,
_ => match (self.static_name(), other.static_name()) {
(Some(a), Some(b)) => a == b,
(Some(a), None) => other.data_key_eq_str(a),
(None, Some(b)) => self.data_key_eq_str(b),
(None, None) => other.data_key_eq_str(&self.to_string()),
},
}
}
#[inline(never)]
fn data_key_eq_str(&self, other: &str) -> bool {
match self {
JSCalendarProperty::DateTime(date_time) => date_time.rfc3339_eq(other),
JSCalendarProperty::IdValue(id) => id.display_eq(other),
JSCalendarProperty::Pointer(pointer) => pointer.text_eq(other),
JSCalendarProperty::IdReference(reference) => {
other.strip_prefix('#') == Some(reference.as_str())
}
JSCalendarProperty::LinkRelation(relation) => relation.as_str() == other,
_ => self.to_string() == other,
}
}
#[inline(never)]
fn serialize_data_text<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
StackText::<KEY_TEXT>::serialize(
serializer,
|text| match self {
JSCalendarProperty::DateTime(date_time) => date_time.push_rfc3339(text),
JSCalendarProperty::Pointer(pointer) => {
text.push_pointer(pointer, Self::push_pointer_key)
}
JSCalendarProperty::IdValue(id) => write!(text, "{id}"),
JSCalendarProperty::IdReference(id) => text.push_id_reference(id),
_ => Err(fmt::Error),
},
|| self.to_cow(),
)
}
fn push_pointer_key<const N: usize>(&self, text: &mut StackText<N>) -> fmt::Result {
match self {
JSCalendarProperty::DateTime(date_time) => date_time.push_rfc3339(text),
property => text.push_pointer_token(&property.to_cow()),
}
}
fn patch_or_prop(&self) -> &JSCalendarProperty<I> {
let mut property = self;
while let JSCalendarProperty::Pointer(ptr) = property {
match ptr.last() {
Some(JsonPointerItem::Key(Key::Property(inner))) => property = inner,
_ => break,
}
}
property
}
}
impl JSCalendarDateTime {
pub(crate) fn from_rfc3339(value: &str, is_local: bool) -> Option<Self> {
let timestamp = match value.as_bytes() {
[
y0,
y1,
y2,
y3,
b'-',
m0,
m1,
b'-',
d0,
d1,
b'T',
h0,
h1,
b':',
i0,
i1,
b':',
s0,
s1,
rest @ ..,
] if matches!(rest, [] | [b'Z'])
&& [y0, y1, y2, y3, m0, m1, d0, d1, h0, h1, i0, i1, s0, s1]
.iter()
.all(|digit| digit.is_ascii_digit()) =>
{
let pair = |high: &u8, low: &u8| (high - b'0') * 10 + (low - b'0');
DateTime {
year: u16::from(pair(y0, y1)) * 100 + u16::from(pair(y2, y3)),
month: pair(m0, m1),
day: pair(d0, d1),
hour: pair(h0, h1),
minute: pair(i0, i1),
second: pair(s0, s1),
tz_before_gmt: false,
tz_hour: 0,
tz_minute: 0,
}
.to_timestamp_local()
}
_ => DateTime::parse_rfc3339(value)?.to_timestamp_local(),
};
Some(JSCalendarDateTime {
timestamp,
is_local,
})
}
#[inline]
fn same_rfc3339(&self, other: &Self) -> bool {
self.is_local == other.is_local
&& (self.timestamp == other.timestamp
|| (!FOUR_DIGIT_YEARS.contains(&self.timestamp)
|| !FOUR_DIGIT_YEARS.contains(&other.timestamp))
&& self.same_wide_rfc3339(other))
}
#[inline(never)]
fn same_wide_rfc3339(&self, other: &Self) -> bool {
self.to_rfc3339() == other.to_rfc3339()
}
fn rfc3339_eq(&self, text: &str) -> bool {
let mut expect = Expect::new(text);
self.push_rfc3339(&mut expect).is_ok() && expect.is_complete()
}
pub(crate) fn push_rfc3339(&self, text: &mut impl AsciiPush) -> fmt::Result {
let dt = DateTime::from_timestamp(self.timestamp);
text.push_4_digits(dt.year)?;
text.push_byte(b'-')?;
text.push_2_digits(dt.month)?;
text.push_byte(b'-')?;
text.push_2_digits(dt.day)?;
text.push_byte(b'T')?;
text.push_2_digits(dt.hour)?;
text.push_byte(b':')?;
text.push_2_digits(dt.minute)?;
text.push_byte(b':')?;
text.push_2_digits(dt.second)?;
if self.is_local {
Ok(())
} else {
text.push_byte(b'Z')
}
}
}
pub(crate) struct StackText<const N: usize> {
len: usize,
bytes: [u8; N],
}
impl<const N: usize> StackText<N> {
pub(crate) fn serialize<S: Serializer>(
serializer: S,
push: impl FnOnce(&mut Self) -> fmt::Result,
fallback: impl FnOnce() -> Cow<'static, str>,
) -> Result<S::Ok, S::Error> {
let mut text = StackText {
len: 0,
bytes: [0; N],
};
match push(&mut text).ok().and_then(|()| text.as_str()) {
Some(text) => serializer.serialize_str(text),
None => serializer.serialize_str(&fallback()),
}
}
fn as_str(&self) -> Option<&str> {
self.bytes
.get(..self.len)
.and_then(|bytes| from_utf8(bytes).ok())
}
pub(crate) fn push_pointer<P: jmap_tools::Property>(
&mut self,
pointer: &JsonPointer<P>,
push_key: impl Fn(&P, &mut Self) -> fmt::Result,
) -> fmt::Result {
for (pos, item) in pointer.as_slice().iter().enumerate() {
if pos > 0 {
self.push_byte(b'/')?;
}
match item {
JsonPointerItem::Root => {}
JsonPointerItem::Wildcard => self.push_byte(b'*')?,
JsonPointerItem::Invalid(invalid) => self.write_str(invalid)?,
JsonPointerItem::Key(Key::Property(property)) => push_key(property, self)?,
JsonPointerItem::Key(Key::Borrowed(key)) => self.push_pointer_token(key)?,
JsonPointerItem::Key(Key::Owned(key)) => self.push_pointer_token(key)?,
JsonPointerItem::Number(number) => self.push_u64(*number)?,
}
}
Ok(())
}
pub(crate) fn push_id_reference(&mut self, id: &str) -> fmt::Result {
self.push_byte(b'#')?;
self.write_str(id)
}
#[inline(always)]
fn push_bytes(&mut self, bytes: &[u8]) -> fmt::Result {
let end = self.len + bytes.len();
self.bytes
.get_mut(self.len..end)
.ok_or(fmt::Error)?
.copy_from_slice(bytes);
self.len = end;
Ok(())
}
}
impl<const N: usize> PointerText for StackText<N> {}
impl<const N: usize> AsciiPush for StackText<N> {
#[inline(always)]
fn push_ascii(&mut self, bytes: &[u8]) -> fmt::Result {
self.push_bytes(bytes)
}
}
impl<const N: usize> Write for StackText<N> {
#[inline(always)]
fn write_str(&mut self, text: &str) -> fmt::Result {
self.push_bytes(text.as_bytes())
}
}
+995
View File
@@ -0,0 +1,995 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::jsprop::{property_names, text::PointerProperty},
jscalendar::*,
};
use jmap_tools::{JsonPointer, JsonPointerItem, Key};
use std::str::FromStr;
impl<I: JSCalendarId> PointerProperty for JSCalendarProperty<I> {
fn into_pointer(self) -> Result<JsonPointer<Self>, Self> {
match self {
JSCalendarProperty::Pointer(pointer) => Ok(pointer),
property => Err(property),
}
}
}
impl<I: JSCalendarId> FromStr for JSCalendarProperty<I> {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
JSCalendarProperty::parse_name(s)
.or_else(|| JSCalendarProperty::parse_metadata_selector(s))
.ok_or(())
}
}
impl<I: JSCalendarId> JSCalendarProperty<I> {
property_names!(
JSCalendarProperty,
"@type" => Type,
"acknowledged" => Acknowledged,
"action" => Action,
"alerts" => Alerts,
"baseEventId" => BaseEventId,
"byDay" => ByDay,
"byHour" => ByHour,
"byMinute" => ByMinute,
"byMonth" => ByMonth,
"byMonthDay" => ByMonthDay,
"bySecond" => BySecond,
"bySetPosition" => BySetPosition,
"byWeekNo" => ByWeekNo,
"byYearDay" => ByYearDay,
"calendarAddress" => CalendarAddress,
"calendarIds" => CalendarIds,
"categories" => Categories,
"color" => Color,
"contentType" => ContentType,
"coordinates" => Coordinates,
"count" => Count,
"created" => Created,
"day" => Day,
"delegatedFrom" => DelegatedFrom,
"delegatedTo" => DelegatedTo,
"description" => Description,
"descriptionContentType" => DescriptionContentType,
"display" => Display,
"due" => Due,
"duration" => Duration,
"email" => Email,
"entries" => Entries,
"estimatedDuration" => EstimatedDuration,
"excluded" => Excluded,
"expectReply" => ExpectReply,
"features" => Features,
"firstDayOfWeek" => FirstDayOfWeek,
"freeBusyStatus" => FreeBusyStatus,
"frequency" => Frequency,
"hideAttendees" => HideAttendees,
"href" => Href,
"id" => Id,
"interval" => Interval,
"invitedBy" => InvitedBy,
"isDraft" => IsDraft,
"isOrigin" => IsOrigin,
"keywords" => Keywords,
"kind" => Kind,
"links" => Links,
"locale" => Locale,
"locations" => Locations,
"locationTypes" => LocationTypes,
"mayInviteOthers" => MayInviteOthers,
"mayInviteSelf" => MayInviteSelf,
"memberOf" => MemberOf,
"method" => Method,
"name" => Name,
"nthOfPeriod" => NthOfPeriod,
"offset" => Offset,
"participants" => Participants,
"participationComment" => ParticipationComment,
"participationStatus" => ParticipationStatus,
"percentComplete" => PercentComplete,
"priority" => Priority,
"privacy" => Privacy,
"prodId" => ProdId,
"progress" => Progress,
"recurrenceId" => RecurrenceId,
"recurrenceIdTimeZone" => RecurrenceIdTimeZone,
"recurrenceOverrides" => RecurrenceOverrides,
"rel" => Rel,
"relatedTo" => RelatedTo,
"relation" => Relation,
"relativeTo" => RelativeTo,
"replyTo" => ReplyTo,
"requestStatus" => RequestStatus,
"roles" => Roles,
"rscale" => Rscale,
"sentBy" => SentBy,
"scheduleAgent" => ScheduleAgent,
"scheduleForceSend" => ScheduleForceSend,
"scheduleSequence" => ScheduleSequence,
"scheduleStatus" => ScheduleStatus,
"scheduleUpdated" => ScheduleUpdated,
"sendTo" => SendTo,
"sequence" => Sequence,
"showWithoutTime" => ShowWithoutTime,
"size" => Size,
"skip" => Skip,
"source" => Source,
"start" => Start,
"status" => Status,
"timeZone" => TimeZone,
"title" => Title,
"trigger" => Trigger,
"uid" => Uid,
"until" => Until,
"updated" => Updated,
"uri" => Uri,
"useDefaultAlerts" => UseDefaultAlerts,
"utcEnd" => UtcEnd,
"utcStart" => UtcStart,
"version" => Version,
"virtualLocations" => VirtualLocations,
"when" => When,
"endTimeZone" => EndTimeZone,
"mainLocationId" => MainLocationId,
"organizerCalendarAddress" => OrganizerCalendarAddress,
"recurrenceRule" => RecurrenceRule,
"properties" => Properties,
"components" => Components,
"valueType" => ValueType,
"convertedProperties" => ConvertedProperties,
"parameters" => Parameters,
"iCalendar" => ICalendar,
"blobId" => BlobId,
);
fn parse_metadata_selector(s: &str) -> Option<Self> {
s.split_once('/')
.and_then(|(root, _)| JSCalendarProperty::<I>::parse_name(root))
.filter(JSCalendarProperty::is_metadata)
.map(|_| JSCalendarProperty::Pointer(JsonPointer::parse(s)))
}
pub(crate) fn is_metadata(&self) -> bool {
match self {
JSCalendarProperty::Metadata | JSCalendarProperty::PrivateMetadata => true,
JSCalendarProperty::Pointer(pointer) => {
JSCalendarProperty::is_metadata_pointer(pointer)
}
_ => false,
}
}
pub(crate) fn sets_misplaced_excluded(pointer: &JsonPointer<Self>) -> bool {
let items = match pointer.as_slice() {
[JsonPointerItem::Root, items @ ..] => items,
items => items,
};
let Some((JsonPointerItem::Key(Key::Property(JSCalendarProperty::Excluded)), parents)) =
items.split_last()
else {
return false;
};
if matches!(
parents,
[
JsonPointerItem::Key(Key::Property(JSCalendarProperty::RecurrenceOverrides)),
JsonPointerItem::Key(Key::Property(JSCalendarProperty::DateTime(_)))
]
) {
return false;
}
let mut is_member_key = false;
parents.iter().all(|item| {
let is_structural = is_member_key
|| matches!(
item,
JsonPointerItem::Key(Key::Property(_)) | JsonPointerItem::Number(_)
);
is_member_key = matches!(
item,
JsonPointerItem::Key(Key::Property(property))
if property.is_member_map() || *property == JSCalendarProperty::RelatedTo
);
is_structural
})
}
pub(crate) fn is_metadata_pointer(pointer: &JsonPointer<Self>) -> bool {
pointer
.first()
.and_then(JsonPointerItem::as_property_key)
.is_some_and(JSCalendarProperty::is_metadata)
}
pub fn to_string(&self) -> Cow<'static, str> {
match self {
JSCalendarProperty::Type => "@type",
JSCalendarProperty::Acknowledged => "acknowledged",
JSCalendarProperty::Action => "action",
JSCalendarProperty::Alerts => "alerts",
JSCalendarProperty::BaseEventId => "baseEventId",
JSCalendarProperty::ByDay => "byDay",
JSCalendarProperty::ByHour => "byHour",
JSCalendarProperty::ByMinute => "byMinute",
JSCalendarProperty::ByMonth => "byMonth",
JSCalendarProperty::ByMonthDay => "byMonthDay",
JSCalendarProperty::BySecond => "bySecond",
JSCalendarProperty::BySetPosition => "bySetPosition",
JSCalendarProperty::ByWeekNo => "byWeekNo",
JSCalendarProperty::ByYearDay => "byYearDay",
JSCalendarProperty::CalendarAddress => "calendarAddress",
JSCalendarProperty::CalendarIds => "calendarIds",
JSCalendarProperty::Categories => "categories",
JSCalendarProperty::Color => "color",
JSCalendarProperty::ContentType => "contentType",
JSCalendarProperty::Coordinates => "coordinates",
JSCalendarProperty::Count => "count",
JSCalendarProperty::Created => "created",
JSCalendarProperty::Day => "day",
JSCalendarProperty::DelegatedFrom => "delegatedFrom",
JSCalendarProperty::DelegatedTo => "delegatedTo",
JSCalendarProperty::Description => "description",
JSCalendarProperty::DescriptionContentType => "descriptionContentType",
JSCalendarProperty::Display => "display",
JSCalendarProperty::Due => "due",
JSCalendarProperty::Duration => "duration",
JSCalendarProperty::Email => "email",
JSCalendarProperty::Entries => "entries",
JSCalendarProperty::EstimatedDuration => "estimatedDuration",
JSCalendarProperty::Excluded => "excluded",
JSCalendarProperty::ExpectReply => "expectReply",
JSCalendarProperty::Features => "features",
JSCalendarProperty::FirstDayOfWeek => "firstDayOfWeek",
JSCalendarProperty::FreeBusyStatus => "freeBusyStatus",
JSCalendarProperty::Frequency => "frequency",
JSCalendarProperty::HideAttendees => "hideAttendees",
JSCalendarProperty::Href => "href",
JSCalendarProperty::Id => "id",
JSCalendarProperty::Interval => "interval",
JSCalendarProperty::InvitedBy => "invitedBy",
JSCalendarProperty::IsDraft => "isDraft",
JSCalendarProperty::IsOrigin => "isOrigin",
JSCalendarProperty::Keywords => "keywords",
JSCalendarProperty::Kind => "kind",
JSCalendarProperty::Links => "links",
JSCalendarProperty::Locale => "locale",
JSCalendarProperty::Locations => "locations",
JSCalendarProperty::LocationTypes => "locationTypes",
JSCalendarProperty::MayInviteOthers => "mayInviteOthers",
JSCalendarProperty::MayInviteSelf => "mayInviteSelf",
JSCalendarProperty::MemberOf => "memberOf",
JSCalendarProperty::Method => "method",
JSCalendarProperty::Name => "name",
JSCalendarProperty::NthOfPeriod => "nthOfPeriod",
JSCalendarProperty::Offset => "offset",
JSCalendarProperty::Participants => "participants",
JSCalendarProperty::ParticipationComment => "participationComment",
JSCalendarProperty::ParticipationStatus => "participationStatus",
JSCalendarProperty::PercentComplete => "percentComplete",
JSCalendarProperty::Priority => "priority",
JSCalendarProperty::Privacy => "privacy",
JSCalendarProperty::ProdId => "prodId",
JSCalendarProperty::Progress => "progress",
JSCalendarProperty::RecurrenceId => "recurrenceId",
JSCalendarProperty::RecurrenceIdTimeZone => "recurrenceIdTimeZone",
JSCalendarProperty::RecurrenceOverrides => "recurrenceOverrides",
JSCalendarProperty::Rel => "rel",
JSCalendarProperty::RelatedTo => "relatedTo",
JSCalendarProperty::Relation => "relation",
JSCalendarProperty::RelativeTo => "relativeTo",
JSCalendarProperty::ReplyTo => "replyTo",
JSCalendarProperty::RequestStatus => "requestStatus",
JSCalendarProperty::Roles => "roles",
JSCalendarProperty::Rscale => "rscale",
JSCalendarProperty::SentBy => "sentBy",
JSCalendarProperty::ScheduleAgent => "scheduleAgent",
JSCalendarProperty::ScheduleForceSend => "scheduleForceSend",
JSCalendarProperty::ScheduleSequence => "scheduleSequence",
JSCalendarProperty::ScheduleStatus => "scheduleStatus",
JSCalendarProperty::ScheduleUpdated => "scheduleUpdated",
JSCalendarProperty::SendTo => "sendTo",
JSCalendarProperty::Sequence => "sequence",
JSCalendarProperty::ShowWithoutTime => "showWithoutTime",
JSCalendarProperty::Size => "size",
JSCalendarProperty::Skip => "skip",
JSCalendarProperty::Source => "source",
JSCalendarProperty::Start => "start",
JSCalendarProperty::Status => "status",
JSCalendarProperty::TimeZone => "timeZone",
JSCalendarProperty::Title => "title",
JSCalendarProperty::Trigger => "trigger",
JSCalendarProperty::Uid => "uid",
JSCalendarProperty::Until => "until",
JSCalendarProperty::Updated => "updated",
JSCalendarProperty::Uri => "uri",
JSCalendarProperty::UseDefaultAlerts => "useDefaultAlerts",
JSCalendarProperty::UtcEnd => "utcEnd",
JSCalendarProperty::UtcStart => "utcStart",
JSCalendarProperty::Version => "version",
JSCalendarProperty::VirtualLocations => "virtualLocations",
JSCalendarProperty::When => "when",
JSCalendarProperty::EndTimeZone => "endTimeZone",
JSCalendarProperty::MainLocationId => "mainLocationId",
JSCalendarProperty::OrganizerCalendarAddress => "organizerCalendarAddress",
JSCalendarProperty::RecurrenceRule => "recurrenceRule",
JSCalendarProperty::Properties => "properties",
JSCalendarProperty::Components => "components",
JSCalendarProperty::ValueType => "valueType",
JSCalendarProperty::ConvertedProperties => "convertedProperties",
JSCalendarProperty::Parameters => "parameters",
JSCalendarProperty::ICalendar => "iCalendar",
JSCalendarProperty::BlobId => "blobId",
JSCalendarProperty::Metadata => "metadata",
JSCalendarProperty::PrivateMetadata => "privateMetadata",
JSCalendarProperty::LinkDisplay(v) => v.as_str(),
JSCalendarProperty::VirtualLocationFeature(v) => v.as_str(),
JSCalendarProperty::ParticipantRole(v) => v.as_str(),
JSCalendarProperty::RelationValue(v) => v.as_str(),
JSCalendarProperty::LinkRelation(v) => v.as_str(),
JSCalendarProperty::DateTime(dt) => return dt.to_rfc3339().into(),
JSCalendarProperty::Pointer(pointer) => return Cow::Owned(pointer.to_string()),
JSCalendarProperty::IdValue(id) => return id.to_string().into(),
JSCalendarProperty::IdReference(s) => return format!("#{}", s).into(),
}
.into()
}
#[inline]
pub(crate) fn static_name(&self) -> Option<&'static str> {
match self {
JSCalendarProperty::DateTime(_)
| JSCalendarProperty::Pointer(_)
| JSCalendarProperty::IdValue(_)
| JSCalendarProperty::IdReference(_) => None,
JSCalendarProperty::LinkRelation(v) => Some(v.as_str()),
property => match property.to_string() {
Cow::Borrowed(name) => Some(name),
Cow::Owned(_) => None,
},
}
}
#[inline]
pub(crate) fn has_data(&self) -> bool {
match self {
JSCalendarProperty::DateTime(_)
| JSCalendarProperty::LinkDisplay(_)
| JSCalendarProperty::VirtualLocationFeature(_)
| JSCalendarProperty::ParticipantRole(_)
| JSCalendarProperty::RelationValue(_)
| JSCalendarProperty::LinkRelation(_)
| JSCalendarProperty::Pointer(_)
| JSCalendarProperty::IdValue(_)
| JSCalendarProperty::IdReference(_) => true,
JSCalendarProperty::Id
| JSCalendarProperty::BaseEventId
| JSCalendarProperty::CalendarIds
| JSCalendarProperty::IsDraft
| JSCalendarProperty::IsOrigin
| JSCalendarProperty::UtcStart
| JSCalendarProperty::UtcEnd
| JSCalendarProperty::UseDefaultAlerts
| JSCalendarProperty::MayInviteSelf
| JSCalendarProperty::MayInviteOthers
| JSCalendarProperty::HideAttendees
| JSCalendarProperty::BlobId
| JSCalendarProperty::Metadata
| JSCalendarProperty::PrivateMetadata
| JSCalendarProperty::Type
| JSCalendarProperty::Acknowledged
| JSCalendarProperty::Action
| JSCalendarProperty::Alerts
| JSCalendarProperty::ByDay
| JSCalendarProperty::ByHour
| JSCalendarProperty::ByMinute
| JSCalendarProperty::ByMonth
| JSCalendarProperty::ByMonthDay
| JSCalendarProperty::BySecond
| JSCalendarProperty::BySetPosition
| JSCalendarProperty::ByWeekNo
| JSCalendarProperty::ByYearDay
| JSCalendarProperty::CalendarAddress
| JSCalendarProperty::Categories
| JSCalendarProperty::Color
| JSCalendarProperty::ContentType
| JSCalendarProperty::Coordinates
| JSCalendarProperty::Count
| JSCalendarProperty::Created
| JSCalendarProperty::Day
| JSCalendarProperty::DelegatedFrom
| JSCalendarProperty::DelegatedTo
| JSCalendarProperty::Description
| JSCalendarProperty::DescriptionContentType
| JSCalendarProperty::Display
| JSCalendarProperty::Due
| JSCalendarProperty::Duration
| JSCalendarProperty::Email
| JSCalendarProperty::Entries
| JSCalendarProperty::EstimatedDuration
| JSCalendarProperty::Excluded
| JSCalendarProperty::ExpectReply
| JSCalendarProperty::Features
| JSCalendarProperty::FirstDayOfWeek
| JSCalendarProperty::FreeBusyStatus
| JSCalendarProperty::Frequency
| JSCalendarProperty::Href
| JSCalendarProperty::Interval
| JSCalendarProperty::InvitedBy
| JSCalendarProperty::Keywords
| JSCalendarProperty::Kind
| JSCalendarProperty::Links
| JSCalendarProperty::Locale
| JSCalendarProperty::Locations
| JSCalendarProperty::LocationTypes
| JSCalendarProperty::MemberOf
| JSCalendarProperty::Method
| JSCalendarProperty::Name
| JSCalendarProperty::NthOfPeriod
| JSCalendarProperty::Offset
| JSCalendarProperty::Participants
| JSCalendarProperty::ParticipationComment
| JSCalendarProperty::ParticipationStatus
| JSCalendarProperty::PercentComplete
| JSCalendarProperty::Priority
| JSCalendarProperty::Privacy
| JSCalendarProperty::ProdId
| JSCalendarProperty::Progress
| JSCalendarProperty::RecurrenceId
| JSCalendarProperty::RecurrenceIdTimeZone
| JSCalendarProperty::RecurrenceOverrides
| JSCalendarProperty::Rel
| JSCalendarProperty::RelatedTo
| JSCalendarProperty::Relation
| JSCalendarProperty::RelativeTo
| JSCalendarProperty::ReplyTo
| JSCalendarProperty::RequestStatus
| JSCalendarProperty::Roles
| JSCalendarProperty::Rscale
| JSCalendarProperty::SentBy
| JSCalendarProperty::ScheduleAgent
| JSCalendarProperty::ScheduleForceSend
| JSCalendarProperty::ScheduleSequence
| JSCalendarProperty::ScheduleStatus
| JSCalendarProperty::ScheduleUpdated
| JSCalendarProperty::SendTo
| JSCalendarProperty::Sequence
| JSCalendarProperty::ShowWithoutTime
| JSCalendarProperty::Size
| JSCalendarProperty::Skip
| JSCalendarProperty::Source
| JSCalendarProperty::Start
| JSCalendarProperty::Status
| JSCalendarProperty::TimeZone
| JSCalendarProperty::Title
| JSCalendarProperty::Trigger
| JSCalendarProperty::Uid
| JSCalendarProperty::Until
| JSCalendarProperty::Updated
| JSCalendarProperty::Uri
| JSCalendarProperty::Version
| JSCalendarProperty::VirtualLocations
| JSCalendarProperty::When
| JSCalendarProperty::EndTimeZone
| JSCalendarProperty::MainLocationId
| JSCalendarProperty::OrganizerCalendarAddress
| JSCalendarProperty::RecurrenceRule
| JSCalendarProperty::ICalendar
| JSCalendarProperty::Properties
| JSCalendarProperty::Parameters
| JSCalendarProperty::ConvertedProperties
| JSCalendarProperty::ValueType
| JSCalendarProperty::Components => false,
}
}
pub(crate) fn is_forbidden_override_patch(&self) -> bool {
matches!(
self,
JSCalendarProperty::Type
| JSCalendarProperty::Method
| JSCalendarProperty::OrganizerCalendarAddress
| JSCalendarProperty::Privacy
| JSCalendarProperty::ProdId
| JSCalendarProperty::RecurrenceId
| JSCalendarProperty::RecurrenceIdTimeZone
| JSCalendarProperty::SentBy
| JSCalendarProperty::Uid
| JSCalendarProperty::RecurrenceOverrides
| JSCalendarProperty::RecurrenceRule
)
}
pub(crate) fn is_forbidden_override_pointer(pointer: &JsonPointer<Self>) -> bool {
let mut items = pointer
.iter()
.filter(|item| !matches!(item, JsonPointerItem::Root));
match (items.next(), items.next(), items.next(), items.next()) {
(
Some(JsonPointerItem::Key(Key::Property(
JSCalendarProperty::RecurrenceOverrides | JSCalendarProperty::RecurrenceRule,
))),
..,
) => true,
(Some(JsonPointerItem::Key(Key::Property(property))), None, ..) => {
property.is_forbidden_override_patch()
}
(
Some(JsonPointerItem::Key(Key::Property(JSCalendarProperty::Participants))),
Some(_),
Some(JsonPointerItem::Key(Key::Property(JSCalendarProperty::CalendarAddress))),
None,
) => true,
_ => false,
}
}
}
impl FromStr for JSCalendarType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSCalendarType,
"Event" => JSCalendarType::Event,
"Task" => JSCalendarType::Task,
"Group" => JSCalendarType::Group,
"Alert" => JSCalendarType::Alert,
"Boolean" => JSCalendarType::Boolean,
"Duration" => JSCalendarType::Duration,
"Id" => JSCalendarType::Id,
"Int" => JSCalendarType::Int,
"LocalDateTime" => JSCalendarType::LocalDateTime,
"Link" => JSCalendarType::Link,
"Location" => JSCalendarType::Location,
"NDay" => JSCalendarType::NDay,
"Number" => JSCalendarType::Number,
"Participant" => JSCalendarType::Participant,
"PatchObject" => JSCalendarType::PatchObject,
"RecurrenceRule" => JSCalendarType::RecurrenceRule,
"Relation" => JSCalendarType::Relation,
"SignedDuration" => JSCalendarType::SignedDuration,
"String" => JSCalendarType::String,
"TimeZone" => JSCalendarType::TimeZone,
"TimeZoneId" => JSCalendarType::TimeZoneId,
"TimeZoneRule" => JSCalendarType::TimeZoneRule,
"UnsignedInt" => JSCalendarType::UnsignedInt,
"UTCDateTime" => JSCalendarType::UTCDateTime,
"VirtualLocation" => JSCalendarType::VirtualLocation,
"ICalComponent" => JSCalendarType::ICalComponent,
"AbsoluteTrigger" => JSCalendarType::AbsoluteTrigger,
"OffsetTrigger" => JSCalendarType::OffsetTrigger,
)
.copied()
.ok_or(())
}
}
impl JSCalendarType {
pub fn as_str(&self) -> &'static str {
match self {
JSCalendarType::Event => "Event",
JSCalendarType::Task => "Task",
JSCalendarType::Group => "Group",
JSCalendarType::Alert => "Alert",
JSCalendarType::Boolean => "Boolean",
JSCalendarType::Duration => "Duration",
JSCalendarType::Id => "Id",
JSCalendarType::Int => "Int",
JSCalendarType::LocalDateTime => "LocalDateTime",
JSCalendarType::Link => "Link",
JSCalendarType::Location => "Location",
JSCalendarType::NDay => "NDay",
JSCalendarType::Number => "Number",
JSCalendarType::Participant => "Participant",
JSCalendarType::PatchObject => "PatchObject",
JSCalendarType::RecurrenceRule => "RecurrenceRule",
JSCalendarType::Relation => "Relation",
JSCalendarType::SignedDuration => "SignedDuration",
JSCalendarType::String => "String",
JSCalendarType::TimeZone => "TimeZone",
JSCalendarType::TimeZoneId => "TimeZoneId",
JSCalendarType::TimeZoneRule => "TimeZoneRule",
JSCalendarType::UnsignedInt => "UnsignedInt",
JSCalendarType::UTCDateTime => "UTCDateTime",
JSCalendarType::VirtualLocation => "VirtualLocation",
JSCalendarType::ICalComponent => "ICalComponent",
JSCalendarType::OffsetTrigger => "OffsetTrigger",
JSCalendarType::AbsoluteTrigger => "AbsoluteTrigger",
}
}
}
impl FromStr for JSCalendarAlertAction {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSCalendarAlertAction,
"display" => JSCalendarAlertAction::Display,
"email" => JSCalendarAlertAction::Email
)
.copied()
.ok_or(())
}
}
impl JSCalendarAlertAction {
pub fn as_str(&self) -> &'static str {
match self {
JSCalendarAlertAction::Display => "display",
JSCalendarAlertAction::Email => "email",
}
}
}
impl FromStr for JSCalendarLinkDisplay {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSCalendarLinkDisplay,
"badge" => JSCalendarLinkDisplay::Badge,
"graphic" => JSCalendarLinkDisplay::Graphic,
"fullsize" => JSCalendarLinkDisplay::Fullsize,
"thumbnail" => JSCalendarLinkDisplay::Thumbnail
)
.copied()
.ok_or(())
}
}
impl JSCalendarLinkDisplay {
pub fn as_str(&self) -> &'static str {
match self {
JSCalendarLinkDisplay::Badge => "badge",
JSCalendarLinkDisplay::Graphic => "graphic",
JSCalendarLinkDisplay::Fullsize => "fullsize",
JSCalendarLinkDisplay::Thumbnail => "thumbnail",
}
}
}
impl FromStr for JSCalendarVirtualLocationFeature {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSCalendarVirtualLocationFeature,
"audio" => JSCalendarVirtualLocationFeature::Audio,
"chat" => JSCalendarVirtualLocationFeature::Chat,
"feed" => JSCalendarVirtualLocationFeature::Feed,
"moderator" => JSCalendarVirtualLocationFeature::Moderator,
"phone" => JSCalendarVirtualLocationFeature::Phone,
"screen" => JSCalendarVirtualLocationFeature::Screen,
"video" => JSCalendarVirtualLocationFeature::Video
)
.copied()
.ok_or(())
}
}
impl JSCalendarVirtualLocationFeature {
pub fn as_str(&self) -> &'static str {
match self {
JSCalendarVirtualLocationFeature::Audio => "audio",
JSCalendarVirtualLocationFeature::Chat => "chat",
JSCalendarVirtualLocationFeature::Feed => "feed",
JSCalendarVirtualLocationFeature::Moderator => "moderator",
JSCalendarVirtualLocationFeature::Phone => "phone",
JSCalendarVirtualLocationFeature::Screen => "screen",
JSCalendarVirtualLocationFeature::Video => "video",
}
}
}
impl FromStr for JSCalendarFreeBusyStatus {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSCalendarFreeBusyStatus,
"free" => JSCalendarFreeBusyStatus::Free,
"busy" => JSCalendarFreeBusyStatus::Busy
)
.copied()
.ok_or(())
}
}
impl JSCalendarFreeBusyStatus {
pub fn as_str(&self) -> &'static str {
match self {
JSCalendarFreeBusyStatus::Free => "free",
JSCalendarFreeBusyStatus::Busy => "busy",
}
}
}
impl FromStr for JSCalendarParticipantKind {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSCalendarParticipantKind,
"individual" => JSCalendarParticipantKind::Individual,
"group" => JSCalendarParticipantKind::Group,
"resource" => JSCalendarParticipantKind::Resource,
"location" => JSCalendarParticipantKind::Location
)
.copied()
.ok_or(())
}
}
impl JSCalendarParticipantKind {
pub fn as_str(&self) -> &'static str {
match self {
JSCalendarParticipantKind::Individual => "individual",
JSCalendarParticipantKind::Group => "group",
JSCalendarParticipantKind::Resource => "resource",
JSCalendarParticipantKind::Location => "location",
}
}
}
impl FromStr for JSCalendarParticipationStatus {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSCalendarParticipationStatus,
"needs-action" => JSCalendarParticipationStatus::NeedsAction,
"accepted" => JSCalendarParticipationStatus::Accepted,
"declined" => JSCalendarParticipationStatus::Declined,
"tentative" => JSCalendarParticipationStatus::Tentative,
"delegated" => JSCalendarParticipationStatus::Delegated
)
.copied()
.ok_or(())
}
}
impl JSCalendarParticipationStatus {
pub fn as_str(&self) -> &'static str {
match self {
JSCalendarParticipationStatus::NeedsAction => "needs-action",
JSCalendarParticipationStatus::Accepted => "accepted",
JSCalendarParticipationStatus::Declined => "declined",
JSCalendarParticipationStatus::Tentative => "tentative",
JSCalendarParticipationStatus::Delegated => "delegated",
}
}
}
impl FromStr for JSCalendarPrivacy {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSCalendarPrivacy,
"public" => JSCalendarPrivacy::Public,
"private" => JSCalendarPrivacy::Private,
"secret" => JSCalendarPrivacy::Secret
)
.copied()
.ok_or(())
}
}
impl JSCalendarPrivacy {
pub fn as_str(&self) -> &'static str {
match self {
JSCalendarPrivacy::Public => "public",
JSCalendarPrivacy::Private => "private",
JSCalendarPrivacy::Secret => "secret",
}
}
}
impl FromStr for JSCalendarProgress {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSCalendarProgress,
"needs-action" => JSCalendarProgress::NeedsAction,
"in-process" => JSCalendarProgress::InProcess,
"completed" => JSCalendarProgress::Completed,
"failed" => JSCalendarProgress::Failed,
"cancelled" => JSCalendarProgress::Cancelled,
)
.copied()
.ok_or(())
}
}
impl JSCalendarProgress {
pub fn as_str(&self) -> &'static str {
match self {
JSCalendarProgress::NeedsAction => "needs-action",
JSCalendarProgress::InProcess => "in-process",
JSCalendarProgress::Completed => "completed",
JSCalendarProgress::Failed => "failed",
JSCalendarProgress::Cancelled => "cancelled",
}
}
}
impl FromStr for JSCalendarRelation {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSCalendarRelation,
"first" => JSCalendarRelation::First,
"next" => JSCalendarRelation::Next,
"child" => JSCalendarRelation::Child,
"parent" => JSCalendarRelation::Parent,
"snooze" => JSCalendarRelation::Snooze,
)
.copied()
.ok_or(())
}
}
impl JSCalendarRelation {
pub fn as_str(&self) -> &'static str {
match self {
JSCalendarRelation::First => "first",
JSCalendarRelation::Next => "next",
JSCalendarRelation::Child => "child",
JSCalendarRelation::Parent => "parent",
JSCalendarRelation::Snooze => "snooze",
}
}
}
impl FromStr for JSCalendarRelativeTo {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSCalendarRelativeTo,
"start" => JSCalendarRelativeTo::Start,
"end" => JSCalendarRelativeTo::End
)
.copied()
.ok_or(())
}
}
impl JSCalendarRelativeTo {
pub fn as_str(&self) -> &'static str {
match self {
JSCalendarRelativeTo::Start => "start",
JSCalendarRelativeTo::End => "end",
}
}
}
impl FromStr for JSCalendarParticipantRole {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSCalendarParticipantRole,
"owner" => JSCalendarParticipantRole::Owner,
"optional" => JSCalendarParticipantRole::Optional,
"informational" => JSCalendarParticipantRole::Informational,
"chair" => JSCalendarParticipantRole::Chair,
"required" => JSCalendarParticipantRole::Required,
)
.copied()
.ok_or(())
}
}
impl JSCalendarParticipantRole {
pub fn as_str(&self) -> &'static str {
match self {
JSCalendarParticipantRole::Owner => "owner",
JSCalendarParticipantRole::Optional => "optional",
JSCalendarParticipantRole::Informational => "informational",
JSCalendarParticipantRole::Chair => "chair",
JSCalendarParticipantRole::Required => "required",
}
}
}
impl FromStr for JSCalendarScheduleAgent {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSCalendarScheduleAgent,
"server" => JSCalendarScheduleAgent::Server,
"client" => JSCalendarScheduleAgent::Client,
"none" => JSCalendarScheduleAgent::None
)
.copied()
.ok_or(())
}
}
impl JSCalendarScheduleAgent {
pub fn as_str(&self) -> &'static str {
match self {
JSCalendarScheduleAgent::Server => "server",
JSCalendarScheduleAgent::Client => "client",
JSCalendarScheduleAgent::None => "none",
}
}
}
impl FromStr for JSCalendarEventStatus {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSCalendarEventStatus,
"confirmed" => JSCalendarEventStatus::Confirmed,
"cancelled" => JSCalendarEventStatus::Cancelled,
"tentative" => JSCalendarEventStatus::Tentative
)
.copied()
.ok_or(())
}
}
impl JSCalendarEventStatus {
pub fn as_str(&self) -> &'static str {
match self {
JSCalendarEventStatus::Confirmed => "confirmed",
JSCalendarEventStatus::Cancelled => "cancelled",
JSCalendarEventStatus::Tentative => "tentative",
}
}
}
impl ICalendarFrequency {
pub fn as_js_str(&self) -> &'static str {
match self {
ICalendarFrequency::Secondly => "secondly",
ICalendarFrequency::Minutely => "minutely",
ICalendarFrequency::Hourly => "hourly",
ICalendarFrequency::Daily => "daily",
ICalendarFrequency::Weekly => "weekly",
ICalendarFrequency::Monthly => "monthly",
ICalendarFrequency::Yearly => "yearly",
}
}
}
impl ICalendarSkip {
pub fn as_js_str(&self) -> &'static str {
match self {
ICalendarSkip::Omit => "omit",
ICalendarSkip::Backward => "backward",
ICalendarSkip::Forward => "forward",
}
}
}
impl ICalendarWeekday {
pub fn as_js_str(&self) -> &'static str {
match self {
ICalendarWeekday::Sunday => "su",
ICalendarWeekday::Monday => "mo",
ICalendarWeekday::Tuesday => "tu",
ICalendarWeekday::Wednesday => "we",
ICalendarWeekday::Thursday => "th",
ICalendarWeekday::Friday => "fr",
ICalendarWeekday::Saturday => "sa",
}
}
}
impl ICalendarMethod {
pub fn as_js_str(&self) -> &'static str {
match self {
ICalendarMethod::Publish => "publish",
ICalendarMethod::Request => "request",
ICalendarMethod::Reply => "reply",
ICalendarMethod::Add => "add",
ICalendarMethod::Cancel => "cancel",
ICalendarMethod::Refresh => "refresh",
ICalendarMethod::Counter => "counter",
ICalendarMethod::Declinecounter => "declinecounter",
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,155 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{
CalendarScale, IanaParse,
parser::{Boolean, Timestamp},
},
jscontact::{JSContactId, JSContactProperty, JSContactValue, export::params::ParamValue},
vcard::{
VCardEntry, VCardLevel, VCardParameter, VCardParameterName, VCardParameterValue,
VCardPhonetic, VCardProperty, VCardType, VCardValueType,
},
};
use jmap_tools::{Key, Value};
use std::mem;
impl VCardEntry {
pub(super) fn import_converted_properties<I, B>(
&mut self,
props: Value<'_, JSContactProperty<I>, JSContactValue<I, B>>,
) where
I: JSContactId,
B: JSContactId,
{
for (key, value) in props.into_expanded_object() {
match key {
Key::Property(JSContactProperty::Name) => {
if let Some(name) = value.into_string() {
self.name = VCardProperty::parse(name.as_bytes())
.unwrap_or(VCardProperty::Other(name.to_ascii_uppercase()));
}
}
Key::Property(JSContactProperty::Parameters) => {
self.import_jcard_params(value);
}
_ => {}
}
}
}
pub(super) fn import_jcard_params<I, B>(
&mut self,
params: Value<'_, JSContactProperty<I>, JSContactValue<I, B>>,
) where
I: JSContactId,
B: JSContactId,
{
for (key, value) in params.into_expanded_object() {
let (single, many) = match value {
Value::Array(values) => (None, values),
value => (Some(value), Vec::new()),
};
let mut values = single
.into_iter()
.chain(many)
.filter_map(ParamValue::try_from_value)
.peekable();
if values.peek().is_none() {
continue;
}
let key = key.to_string();
let Some(param) = VCardParameterName::try_parse(key.as_bytes()) else {
let mut name = key.to_ascii_uppercase();
while let Some(value) = values.next() {
let name = if values.peek().is_some() {
name.clone()
} else {
mem::take(&mut name)
};
self.params.push(VCardParameter {
name: VCardParameterName::Other(name),
value: value.into_string().into_owned().into(),
});
}
continue;
};
for value in values {
let value = match &param {
VCardParameterName::Value => {
let value = value.into_string();
VCardValueType::parse(value.as_bytes())
.map(VCardParameterValue::ValueType)
.unwrap_or_else(|| VCardParameterValue::Text(value.into_owned()))
}
VCardParameterName::Pref | VCardParameterName::Index => {
match value.into_number() {
Ok(n) => u32::try_from(n).map_or_else(
|_| VCardParameterValue::Text(n.to_string()),
VCardParameterValue::Integer,
),
Err(value) => {
VCardParameterValue::Text(value.into_string().into_owned())
}
}
}
VCardParameterName::Calscale => {
let value = value.into_string();
CalendarScale::parse(value.as_bytes())
.map(VCardParameterValue::Calscale)
.unwrap_or_else(|| VCardParameterValue::Text(value.into_owned()))
}
VCardParameterName::Phonetic => {
let value = value.into_string();
VCardPhonetic::parse(value.as_bytes())
.map(VCardParameterValue::Phonetic)
.unwrap_or_else(|| VCardParameterValue::Text(value.into_owned()))
}
VCardParameterName::Level => {
let value = value.into_string();
VCardLevel::parse(value.as_bytes())
.map(VCardParameterValue::Level)
.unwrap_or_else(|| VCardParameterValue::Text(value.into_owned()))
}
VCardParameterName::Created => {
let value = value.into_string();
Timestamp::parse(value.as_bytes())
.map(|v| VCardParameterValue::Timestamp(v.0))
.unwrap_or_else(|| VCardParameterValue::Text(value.into_owned()))
}
VCardParameterName::Derived => {
let value = value.into_string();
Boolean::parse(value.as_bytes())
.map(|v| VCardParameterValue::Bool(v.0))
.unwrap_or_else(|| VCardParameterValue::Text(value.into_owned()))
}
VCardParameterName::Type => {
let value = value.into_string();
VCardType::parse(value.as_bytes())
.map(VCardParameterValue::Type)
.unwrap_or_else(|| VCardParameterValue::Text(value.into_owned()))
}
VCardParameterName::Group => {
self.group = Some(value.into_string().into_owned());
continue;
}
_ => VCardParameterValue::Text(value.into_string().into_owned()),
};
self.params.push(VCardParameter {
name: param.clone(),
value,
});
}
}
}
}
+121
View File
@@ -0,0 +1,121 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{
blob::{BlobOptions, BlobResolver, BlobResolverFn, NoBlobIds, ResolvedBlobs},
export::{EmbeddedBudget, ExportError},
jsprop::ordered::OrderedMap,
},
jscontact::{JSContactId, JSContactProperty, JSContactValue},
vcard::VCard,
};
use jmap_tools::{Key, Value};
use std::hash::Hash;
pub mod convert;
pub mod entry;
pub mod params;
pub mod props;
#[derive(Default)]
#[allow(clippy::type_complexity)]
struct State<'x, I, B>
where
I: JSContactId,
B: JSContactId,
{
pub(super) vcard: VCard,
pub(super) converted_props: Vec<(
Vec<Key<'static, JSContactProperty<I>>>,
Value<'x, JSContactProperty<I>, JSContactValue<I, B>>,
)>,
pub(super) converted_props_count: usize,
pub(super) converted_heads: OrderedMap<JSContactProperty<I>, ()>,
pub(super) language: Option<String>,
}
#[derive(Debug, Clone, Copy)]
pub struct ExportOptions<R = NoBlobIds> {
blobs: BlobOptions<R>,
}
impl Default for ExportOptions {
fn default() -> Self {
Self {
blobs: BlobOptions::default(),
}
}
}
pub(super) struct ExportContext<'a, B> {
blobs: Option<ResolvedBlobs<'a, B>>,
budget: EmbeddedBudget,
error: Option<ExportError>,
}
impl ExportOptions {
pub fn new() -> Self {
Self::default()
}
}
impl<R> ExportOptions<R> {
pub fn with_blob_resolver<B, F>(self, blob_resolver: F) -> ExportOptions<BlobResolverFn<F>>
where
F: FnMut(&B) -> Option<Vec<u8>>,
{
self.with_resolver(BlobResolverFn(blob_resolver))
}
pub fn with_resolver<T>(self, blob_resolver: T) -> ExportOptions<T> {
ExportOptions {
blobs: self.blobs.with_handler(blob_resolver),
}
}
pub fn max_embedded_size(mut self, max_embedded_size: usize) -> Self {
self.blobs = self.blobs.with_max_embedded_size(max_embedded_size);
self
}
pub(super) fn context<B: Clone + Eq + Hash>(&mut self) -> ExportContext<'_, B>
where
R: BlobResolver<B>,
{
ExportContext {
budget: self.blobs.budget(),
blobs: self.blobs.resolved_blobs(),
error: None,
}
}
}
impl<B> ExportContext<'_, B> {
fn fail(&mut self, error: ExportError) {
if self.error.is_none() {
self.error = Some(error);
}
}
fn embed(&mut self, data: &[u8]) -> bool {
match self.budget.take(data) {
Err(error) => {
self.blobs = None;
self.fail(error);
false
}
Ok(()) => true,
}
}
fn into_result(self, vcard: VCard) -> Result<VCard, ExportError> {
match self.error {
Some(error) => Err(error),
None => Ok(vcard),
}
}
}
@@ -0,0 +1,365 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{IanaParse, IanaType},
jscontact::{
Context, Feature, JSContactId, JSContactProperty, JSContactValue,
export::{State, props::convert_value},
},
vcard::{
VCard, VCardEntry, VCardParameter, VCardParameterName, VCardParameterValue, VCardProperty,
VCardType, VCardValueType, ValueType,
},
};
use jmap_tools::{Element, JsonPointer, Key, Property, Value};
use smallvec::{SmallVec, smallvec};
use std::{borrow::Cow, mem};
impl<'x, I, B> State<'x, I, B>
where
I: JSContactId,
B: JSContactId,
{
pub(super) fn insert_vcard(&mut self, path: &[JSContactProperty<I>], mut entry: VCardEntry) {
if self.converted_props_count < self.converted_props.len() {
// Obtain propId
let mut prop_id =
if matches!(entry.name, VCardProperty::Member | VCardProperty::Related) {
entry.values.first().and_then(|v| v.as_text())
} else {
entry.prop_id()
};
// Try mapping X-ABLabel
if let Some(prop_id_) = prop_id {
let mut remove_pos = None;
for (param_pos, param) in entry.params.iter().enumerate() {
if let (VCardParameterName::Label, VCardParameterValue::Text(label)) =
(&param.name, &param.value)
{
if self.converted_props.iter().any(|(prop, _)| {
prop.len() == 3
&& prop[0].to_string() == path[0].to_string()
&& prop[1] == prop_id_
&& prop[2] == Key::Property(JSContactProperty::Label)
}) {
self.insert_vcard(
&[path[0].clone(), JSContactProperty::Label],
VCardEntry::new(VCardProperty::Other("X-ABLabel".into()))
.with_value(label.to_string()),
);
remove_pos = Some(param_pos);
}
break;
}
}
if let Some(pos) = remove_pos {
entry.params.swap_remove(pos);
prop_id =
if matches!(entry.name, VCardProperty::Member | VCardProperty::Related) {
entry.values.first().and_then(|v| v.as_text())
} else {
entry.prop_id()
};
}
}
let skip_tz_geo = matches!(entry.name, VCardProperty::Adr);
let mut matched_once = false;
let may_match = path
.first()
.is_none_or(|head| self.converted_heads.contains_key(head));
if may_match {
'outer: for (keys, value) in self.converted_props.iter_mut() {
let is_localized_key = keys.first().is_some_and(|k| {
matches!(k, Key::Property(JSContactProperty::Localizations))
});
if let Some(lang) = &self.language {
if !is_localized_key || keys.get(1).is_none_or(|k| *k != lang.as_str()) {
continue;
}
} else if is_localized_key {
continue;
}
if matches!(value, Value::Null) {
continue;
}
for (pos, item) in path.iter().enumerate() {
if !keys
.iter()
.any(|k| matches!(k, Key::Property(p) if p == item))
{
if pos == 0 && matched_once {
break 'outer;
} else {
continue 'outer;
}
} else {
matched_once = true;
}
}
if prop_id
.map(Key::Borrowed)
.is_none_or(|prop_id| keys.iter().any(|k| k == &prop_id))
&& (!skip_tz_geo
|| !keys.iter().any(|k| {
matches!(
k,
Key::Property(
JSContactProperty::TimeZone
| JSContactProperty::Coordinates
)
)
}))
{
entry.import_converted_properties(mem::take(value));
self.converted_props_count += 1;
break;
}
}
}
}
if let Some(lang) = &self.language {
entry.params.push(VCardParameter::language(lang.clone()));
}
self.vcard.entries.push(entry);
}
pub(super) fn convert_types(
&mut self,
vcard_property: &VCardProperty,
[property, id]: [&str; 2],
bucket: JSContactProperty<I>,
value: Value<'x, JSContactProperty<I>, JSContactValue<I, B>>,
params: &mut Vec<VCardParameter>,
) {
let mut has_mapped = false;
let mut has_unmapped = false;
let mut has_unquotable_key = false;
let is_mapped =
|key: &Key<'_, JSContactProperty<I>>,
value: &Value<'x, JSContactProperty<I>, JSContactValue<I, B>>| {
value.as_bool() == Some(true)
&& bucket
.vcard_type(vcard_property, key.to_string().as_ref())
.is_some()
};
for (key, item) in value.as_object().into_iter().flat_map(|obj| obj.iter()) {
let key = key.to_string();
match bucket
.vcard_type(vcard_property, key.as_ref())
.filter(|_| item.as_bool() == Some(true))
{
Some(IanaType::Iana(typ)) => {
params.push(VCardParameter::typ(typ));
has_mapped = true;
}
Some(IanaType::Other(typ)) => {
params.push(VCardParameter::typ(typ.to_ascii_uppercase()));
has_mapped = true;
}
None => {
has_unmapped = true;
has_unquotable_key |= key
.chars()
.any(|ch| ch.is_control() || matches!(ch, '"' | '\\'));
}
}
}
if !has_unmapped {
return;
}
let bucket_name = bucket.to_string();
if has_mapped && !has_unquotable_key && self.language.is_none() {
for (key, item) in value.into_expanded_object() {
if !is_mapped(&key, &item) {
self.insert_jsprop(
&[property, id, bucket_name.as_ref(), key.to_string().as_ref()],
item,
);
}
}
} else {
self.insert_jsprop(
&[property, id, bucket_name.as_ref()],
Value::Object(value.into_expanded_object().collect()),
);
}
}
pub(super) fn insert_jsprop(
&mut self,
path: &[&str],
value: Value<'x, JSContactProperty<I>, JSContactValue<I, B>>,
) {
let path = if let Some(lang) = &self.language {
JsonPointer::<JSContactProperty<I>>::encode([
JSContactProperty::Localizations::<I>.to_string().as_ref(),
lang.as_str(),
JsonPointer::<JSContactProperty<I>>::encode(path).as_str(),
])
} else {
JsonPointer::<JSContactProperty<I>>::encode(path)
};
self.vcard.entries.push(
VCardEntry::new(VCardProperty::Jsprop)
.with_param(VCardParameter::jsptr(path))
.with_value(serde_json::to_string(&value).unwrap_or_default()),
);
}
pub(super) fn import_properties(
&mut self,
props: Vec<Value<'x, JSContactProperty<I>, JSContactValue<I, B>>>,
) {
for prop in props.into_iter().flat_map(|prop| prop.into_array()) {
let mut prop = prop.into_iter();
let Some(name) = prop
.next()
.and_then(|v| v.into_string())
.map(|name| {
VCardProperty::parse(name.as_bytes()).unwrap_or_else(|| {
let mut name = name.into_owned();
name.make_ascii_uppercase();
VCardProperty::Other(name)
})
})
.filter(|name| !matches!(name, VCardProperty::Begin | VCardProperty::End))
else {
continue;
};
let Some(params) = prop.next() else {
continue;
};
let Some(value_type) = prop
.next()
.and_then(|v| v.into_string())
.map(|v| VCardValueType::parse(v.as_bytes()))
else {
continue;
};
let (default_type, _) = name.default_types();
let convert_type = value_type.map(ValueType::Vcard).unwrap_or(default_type);
let Some(values) = prop.next().and_then(|v| match v {
Value::Array(arr) => Some(
arr.into_iter()
.filter_map(|v| convert_value(v, &convert_type).ok())
.collect::<SmallVec<_>>(),
),
v => convert_value(v, &convert_type).ok().map(|v| smallvec![v]),
}) else {
continue;
};
let mut entry = VCardEntry::new(name);
entry.import_jcard_params(params);
entry.values = values;
if convert_type != default_type
&& let Some(value_type) = value_type
{
entry.params.push(VCardParameter::value(
IanaType::<VCardValueType, String>::Iana(value_type),
));
}
self.vcard.entries.push(entry);
}
}
pub(super) fn into_vcard(self) -> VCard {
self.vcard
}
}
impl<I: JSContactId> JSContactProperty<I> {
fn vcard_type<'k>(
&self,
property: &VCardProperty,
key: &'k str,
) -> Option<IanaType<VCardType, &'k str>> {
if key.is_empty()
|| !key
.bytes()
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == b'-')
{
return None;
}
match (self, VCardType::parse(key.as_bytes())) {
(JSContactProperty::Features, Some(typ)) => Feature::from_vcard_type(property, &typ)
.is_some_and(|feature| feature.as_str() == key)
.then_some(IanaType::Iana(typ)),
(JSContactProperty::Features, None) => {
Feature::from_vcard_type(property, &VCardType::Cell)
.is_some_and(|feature| feature.as_str() == key)
.then_some(IanaType::Iana(VCardType::Cell))
}
(JSContactProperty::Contexts, Some(typ)) => (Feature::from_vcard_type(property, &typ)
.is_none()
&& !matches!(typ, VCardType::Home | VCardType::Cell)
&& (!matches!(typ, VCardType::Billing | VCardType::Delivery)
|| property == &VCardProperty::Adr))
.then_some(IanaType::Iana(typ)),
(JSContactProperty::Contexts, None) if key == Context::Private.as_str() => {
Some(IanaType::Iana(VCardType::Home))
}
(JSContactProperty::Contexts, None) => Some(IanaType::Other(key)),
_ => None,
}
}
}
pub(crate) enum ParamValue<'x> {
Text(Cow<'x, str>),
Number(i64),
Bool(bool),
}
impl<'x> ParamValue<'x> {
pub(crate) fn try_from_value<P: Property, E: Element>(value: Value<'x, P, E>) -> Option<Self> {
match value {
Value::Str(s) => Some(Self::Text(s)),
Value::Number(n) => Some(match n.as_i64() {
Some(number) => Self::Number(number),
None => Self::Text(Value::<P, E>::Number(n).to_string().into()),
}),
Value::Bool(b) => Some(Self::Bool(b)),
Value::Element(e) => Some(Self::Text(e.to_cow().to_string().into())),
_ => None,
}
}
pub(crate) fn into_string(self) -> Cow<'x, str> {
match self {
Self::Text(s) => s,
Self::Number(n) => n.to_string().into(),
Self::Bool(b) => if b { "true" } else { "false" }.to_string().into(),
}
}
pub(crate) fn into_number(self) -> Result<i64, Self> {
match self {
Self::Number(n) => Ok(n),
Self::Text(s) => s.parse().map_err(|_| Self::Text(s)),
_ => Err(self),
}
}
}
@@ -0,0 +1,426 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{CalendarScale, IanaParse, IanaType, PartialDateTime, parser::Integer},
icalendar::Uri,
jscontact::{
JSContactGrammaticalGender, JSContactId, JSContactKind, JSContactLevel,
JSContactPhoneticSystem, JSContactProperty, JSContactValue,
},
vcard::{
VCardGramGender, VCardKind, VCardLevel, VCardPhonetic, VCardSex, VCardValue,
VCardValueType, ValueType,
},
};
use jmap_tools::{JsonPointerItem, Key, Map, Value};
use std::{borrow::Cow, iter::Peekable, vec::IntoIter};
impl VCardValue {
pub(super) fn from_components(components: Option<Vec<String>>) -> Self {
match components {
Some(components) if components.len() > 1 => VCardValue::Component(components),
Some(components) => VCardValue::Text(components.into_iter().next().unwrap_or_default()),
None => VCardValue::Text(String::new()),
}
}
}
pub(super) fn build_path<'x, I, B>(
obj: &mut Value<'x, JSContactProperty<I>, JSContactValue<I, B>>,
mut ptr: Peekable<IntoIter<JsonPointerItem<JSContactProperty<I>>>>,
value: Value<'x, JSContactProperty<I>, JSContactValue<I, B>>,
) -> Option<Value<'x, JSContactProperty<I>, JSContactValue<I, B>>>
where
I: JSContactId,
B: JSContactId,
{
if let Some(item) = ptr.next() {
match item {
JsonPointerItem::Root | JsonPointerItem::Wildcard | JsonPointerItem::Invalid(_) => {}
JsonPointerItem::Key(key) => match obj {
Value::Object(obj) => {
return build_path(
obj.insert_or_get_mut(
key,
if matches!(ptr.peek(), Some(JsonPointerItem::Key(_))) {
Value::Object(Map::from(Vec::new()))
} else {
Value::Array(Vec::new())
},
),
ptr,
value,
);
}
Value::Null => {
*obj = Value::Object(Map::from(vec![(key, Value::Null)]));
return build_path(
&mut obj
.as_object_mut()
.unwrap()
.as_mut_vec()
.last_mut()
.unwrap()
.1,
ptr,
value,
);
}
_ => {}
},
JsonPointerItem::Number(idx) => {
if let Some(arr) = obj.as_array_mut() {
if (idx as usize) < arr.len() {
return build_path(&mut arr[idx as usize], ptr, value);
}
if idx < 20 {
arr.resize_with(idx as usize + 1, || Value::Null);
return build_path(&mut arr[idx as usize], ptr, value);
}
}
}
}
Some(value)
} else {
*obj = value;
None
}
}
#[allow(clippy::type_complexity)]
pub(super) fn convert_anniversary<I, B>(
value: Value<'_, JSContactProperty<I>, JSContactValue<I, B>>,
) -> Result<
(PartialDateTime, Option<CalendarScale>),
Value<'_, JSContactProperty<I>, JSContactValue<I, B>>,
>
where
I: JSContactId,
B: JSContactId,
{
let mut date = PartialDateTime::default();
let mut calendar_scale = None;
let mut is_valid = true;
let Some(object) = value.as_object() else {
return Err(value);
};
for (key, value) in object.as_vec() {
match key {
Key::Property(JSContactProperty::Day) => {
if let Value::Number(day) = value {
date.day = day
.as_u64()
.filter(|day| (1..=31).contains(day))
.map(|day| day as u8);
is_valid &= date.day.is_some();
}
}
Key::Property(JSContactProperty::Month) => {
if let Value::Number(month) = value {
date.month = month
.as_u64()
.filter(|month| (1..=12).contains(month))
.map(|month| month as u8);
is_valid &= date.month.is_some();
}
}
Key::Property(JSContactProperty::Year) => {
if let Value::Number(year) = value {
date.year = year
.as_u64()
.filter(|year| *year <= 9999)
.map(|year| year as u16);
is_valid &= date.year.is_some();
}
}
Key::Property(JSContactProperty::CalendarScale) => {
if let Value::Element(JSContactValue::CalendarScale(scale)) = value {
calendar_scale = Some(scale.clone());
}
}
Key::Property(JSContactProperty::Utc) => {
if let Value::Element(JSContactValue::Timestamp(timestamp)) = value {
return Ok((PartialDateTime::from_utc_timestamp(*timestamp), None));
}
}
_ => {}
}
}
if is_valid && (date.year.is_some() || date.month.is_some() || date.day.is_some()) {
Ok((date, calendar_scale))
} else {
Err(value)
}
}
pub(super) fn convert_value<'x, I, B>(
value: Value<'x, JSContactProperty<I>, JSContactValue<I, B>>,
value_type: &'_ ValueType,
) -> Result<VCardValue, Value<'x, JSContactProperty<I>, JSContactValue<I, B>>>
where
I: JSContactId,
B: JSContactId,
{
match value {
Value::Element(e) => match e {
JSContactValue::Timestamp(t) => Ok(VCardValue::PartialDateTime(
PartialDateTime::from_utc_timestamp(t),
)),
JSContactValue::GrammaticalGender(g) => Ok(VCardValue::GramGender(match g {
JSContactGrammaticalGender::Animate => VCardGramGender::Animate,
JSContactGrammaticalGender::Common => VCardGramGender::Common,
JSContactGrammaticalGender::Feminine => VCardGramGender::Feminine,
JSContactGrammaticalGender::Inanimate => VCardGramGender::Inanimate,
JSContactGrammaticalGender::Masculine => VCardGramGender::Masculine,
JSContactGrammaticalGender::Neuter => VCardGramGender::Neuter,
})),
JSContactValue::Kind(k) => match k {
JSContactKind::Individual => Ok(VCardValue::Kind(VCardKind::Individual)),
JSContactKind::Group => Ok(VCardValue::Kind(VCardKind::Group)),
JSContactKind::Location => Ok(VCardValue::Kind(VCardKind::Location)),
JSContactKind::Org => Ok(VCardValue::Kind(VCardKind::Org)),
JSContactKind::Application => Ok(VCardValue::Kind(VCardKind::Application)),
JSContactKind::Device => Ok(VCardValue::Kind(VCardKind::Device)),
_ => Err(Value::Element(JSContactValue::Kind(k))),
},
JSContactValue::Level(_)
| JSContactValue::Type(_)
| JSContactValue::Relation(_)
| JSContactValue::PhoneticSystem(_)
| JSContactValue::CalendarScale(_)
| JSContactValue::BlobId(_)
| JSContactValue::Id(_)
| JSContactValue::IdReference(_) => Err(Value::Element(e)),
},
Value::Str(s) => {
match value_type {
ValueType::Kind => {
if let Some(kind) = VCardKind::parse(s.as_ref().as_bytes()) {
return Ok(VCardValue::Kind(kind));
}
}
ValueType::Sex => {
if let Some(sex) = VCardSex::parse(s.as_ref().as_bytes()) {
return Ok(VCardValue::Sex(sex));
}
}
ValueType::GramGender => {
if let Some(gender) = VCardGramGender::parse(s.as_ref().as_bytes()) {
return Ok(VCardValue::GramGender(gender));
}
}
ValueType::Vcard(typ) => match typ {
VCardValueType::Boolean => {
if s.eq_ignore_ascii_case("true") {
return Ok(VCardValue::Boolean(true));
} else if s.eq_ignore_ascii_case("false") {
return Ok(VCardValue::Boolean(false));
}
}
VCardValueType::Date => {
let mut dt = PartialDateTime::default();
dt.parse_vcard_date(&mut s.as_ref().as_bytes().iter().peekable());
if !dt.is_null() {
return Ok(VCardValue::PartialDateTime(dt));
}
}
VCardValueType::DateAndOrTime => {
let mut dt = PartialDateTime::default();
dt.parse_vcard_date_and_or_time(
&mut s.as_ref().as_bytes().iter().peekable(),
);
if !dt.is_null() {
return Ok(VCardValue::PartialDateTime(dt));
}
}
VCardValueType::DateTime => {
let mut dt = PartialDateTime::default();
dt.parse_vcard_date_time(&mut s.as_ref().as_bytes().iter().peekable());
if !dt.is_null() {
return Ok(VCardValue::PartialDateTime(dt));
}
}
VCardValueType::Time => {
let mut dt = PartialDateTime::default();
dt.parse_vcard_time(&mut s.as_ref().as_bytes().iter().peekable(), false);
if !dt.is_null() {
return Ok(VCardValue::PartialDateTime(dt));
}
}
VCardValueType::Timestamp => {
let mut dt = PartialDateTime::default();
if dt.parse_timestamp(&mut s.as_ref().as_bytes().iter().peekable(), true) {
return Ok(VCardValue::PartialDateTime(dt));
}
}
VCardValueType::UtcOffset => {
let mut dt = PartialDateTime::default();
dt.parse_zone(&mut s.as_ref().as_bytes().iter().peekable());
if !dt.is_null() {
return Ok(VCardValue::PartialDateTime(dt));
}
}
VCardValueType::Float => {
if let Ok(float) = s.as_ref().parse::<f64>()
&& float.is_finite()
{
return Ok(VCardValue::Float(float));
}
}
VCardValueType::Integer => {
if let Some(integer) = Integer::parse(s.as_ref().as_bytes()) {
return Ok(VCardValue::Integer(integer.0));
}
}
VCardValueType::Uri => {
return Ok(match Uri::parse(s) {
Uri::Data(data) => VCardValue::Binary(data),
Uri::Location(text) => VCardValue::Text(text),
});
}
VCardValueType::LanguageTag | VCardValueType::Text => (),
},
}
Ok(VCardValue::Text(s.into_owned()))
}
Value::Bool(b) => Ok(VCardValue::Boolean(b)),
Value::Number(n) => match n.as_i64() {
Some(integer) => Ok(VCardValue::Integer(integer)),
None => n
.as_f64()
.filter(|float| n.is_f64() && float.is_finite())
.map(VCardValue::Float)
.ok_or(Value::Number(n)),
},
value => Err(value),
}
}
pub(super) trait U32Value {
fn as_u32(&self) -> Option<u32>;
fn as_pref(&self) -> Option<u32>;
fn as_index(&self) -> Option<u32>;
}
impl<I, B> U32Value for Value<'_, JSContactProperty<I>, JSContactValue<I, B>>
where
I: JSContactId,
B: JSContactId,
{
fn as_u32(&self) -> Option<u32> {
self.as_u64().and_then(|value| u32::try_from(value).ok())
}
fn as_pref(&self) -> Option<u32> {
self.as_u32().filter(|pref| (1..=100).contains(pref))
}
fn as_index(&self) -> Option<u32> {
self.as_u32().filter(|index| *index >= 1)
}
}
pub(super) fn map_kind<T, I, B>(
value: &Value<'_, JSContactProperty<I>, JSContactValue<I, B>>,
types: impl IntoIterator<Item = (JSContactKind, T)>,
) -> Option<T>
where
I: JSContactId,
B: JSContactId,
{
value
.as_object()
.and_then(|obj| obj.get(&Key::Property(JSContactProperty::Kind)))
.and_then(|v| match v {
Value::Element(JSContactValue::Kind(kind)) => {
types.into_iter().find_map(|(js_kind, vcard_property)| {
if js_kind == *kind {
Some(vcard_property)
} else {
None
}
})
}
_ => None,
})
}
impl<I, B> TryFrom<Value<'_, JSContactProperty<I>, JSContactValue<I, B>>>
for IanaType<VCardPhonetic, String>
where
I: JSContactId,
B: JSContactId,
{
type Error = ();
fn try_from(
value: Value<'_, JSContactProperty<I>, JSContactValue<I, B>>,
) -> Result<Self, Self::Error> {
match value {
Value::Element(JSContactValue::PhoneticSystem(system)) => {
Ok(IanaType::Iana(match system {
JSContactPhoneticSystem::Ipa => VCardPhonetic::Ipa,
JSContactPhoneticSystem::Jyut => VCardPhonetic::Jyut,
JSContactPhoneticSystem::Piny => VCardPhonetic::Piny,
JSContactPhoneticSystem::Script => VCardPhonetic::Script,
}))
}
Value::Str(text) => match VCardPhonetic::parse(text.as_ref().as_bytes()) {
Some(phonetic) => Ok(IanaType::Iana(phonetic)),
None => Ok(IanaType::Other(text.to_ascii_uppercase())),
},
_ => Err(()),
}
}
}
impl<I, B> TryFrom<Value<'_, JSContactProperty<I>, JSContactValue<I, B>>>
for IanaType<VCardLevel, String>
where
I: JSContactId,
B: JSContactId,
{
type Error = ();
fn try_from(
value: Value<'_, JSContactProperty<I>, JSContactValue<I, B>>,
) -> Result<Self, Self::Error> {
match value {
Value::Element(JSContactValue::Level(level)) => Ok(IanaType::Iana(match level {
JSContactLevel::High => VCardLevel::High,
JSContactLevel::Low => VCardLevel::Low,
JSContactLevel::Medium => VCardLevel::Medium,
})),
Value::Str(text) => match VCardLevel::parse(text.as_ref().as_bytes()) {
Some(level) => Ok(IanaType::Iana(level)),
None => Ok(IanaType::Other(text.to_ascii_uppercase())),
},
_ => Err(()),
}
}
}
pub(super) fn find_text_param<'x, I, B>(
value: &'x Value<'x, JSContactProperty<I>, JSContactValue<I, B>>,
name: &str,
) -> Option<Cow<'x, str>>
where
I: JSContactId,
B: JSContactId,
{
value
.as_object()
.and_then(|obj| obj.get(&Key::Property(JSContactProperty::Parameters)))
.and_then(|obj| obj.as_object())
.and_then(|obj| obj.get_ignore_case(name))
.and_then(|obj| obj.as_str())
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,280 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{IanaString, IanaType, writer::write_jscomps},
jscontact::{
JSContactGrammaticalGender, JSContactId, JSContactKind, JSContactProperty, JSContactType,
JSContactValue,
import::{EntryState, VCardParams},
},
vcard::{
VCardEntry, VCardGramGender, VCardKind, VCardParameterName, VCardValue, VCardValueType,
},
};
use jmap_tools::{Key, Map, Value};
use std::borrow::Cow;
#[allow(clippy::wrong_self_convention)]
impl EntryState {
pub(super) fn new(entry: VCardEntry, keep_converted_path: bool) -> Self {
Self {
entry,
converted_to: None,
map_name: false,
keep_converted_path,
}
}
pub(super) fn has_conversion_params(&self) -> bool {
self.map_name || !self.entry.params.is_empty() || self.entry.group.is_some()
}
pub(super) fn jcal_parameters<I: JSContactId, B: JSContactId>(
&mut self,
params: &mut VCardParams<I, B>,
value_type: &mut Option<IanaType<VCardValueType, String>>,
) {
if self.entry.params.is_empty() && self.entry.group.is_none() {
return;
}
let (default_type, _) = self.entry.name.default_types();
let default_type = default_type.unwrap_vcard();
for param in std::mem::take(&mut self.entry.params) {
let value = match &param.name {
VCardParameterName::Value => {
if let Some(v) = param
.value
.into_value_type()
.filter(|v| !v.is_iana_and(|v| v == &default_type))
{
*value_type = Some(v);
}
continue;
}
VCardParameterName::Created => {
let Some(IanaType::Iana(timestamp)) = param.value.into_timestamp() else {
continue;
};
Value::Element(JSContactValue::Timestamp(timestamp))
}
VCardParameterName::Jscomps => {
let Some(v) = param.value.into_jscomps() else {
continue;
};
let mut jscomps = String::new();
let _ = write_jscomps(&mut jscomps, &v);
Value::Str(jscomps.into())
}
VCardParameterName::Other(name) if name.eq_ignore_ascii_case("group") => {
// The GROUP parameter (Section 7.1 of [RFC7095]) does not convert to JSContact.
// It exclusively is for use in jCard and MUST NOT be set in a vCard.
continue;
}
VCardParameterName::Group => {
// The GROUP parameter (Section 7.1 of [RFC7095]) does not convert to JSContact.
// It exclusively is for use in jCard and MUST NOT be set in a vCard.
continue;
}
_ => Value::Str(param.value.into_text()),
};
params.push(param.name, value);
}
if let Some(group) = self.entry.group.take() {
params.set(VCardParameterName::Group, vec![group.into()]);
}
}
pub(super) fn set_converted_to(&mut self, converted_to: impl FnOnce() -> String) {
self.converted_to = Some(
if self.keep_converted_path && self.has_conversion_params() {
converted_to()
} else {
String::new()
},
);
}
pub(super) fn set_map_name(&mut self) {
self.map_name = true;
}
pub(super) fn to_text<I: JSContactId, B: JSContactId>(
&mut self,
) -> Option<Value<'static, JSContactProperty<I>, JSContactValue<I, B>>> {
self.to_string().map(Into::into)
}
pub(super) fn to_string(&mut self) -> Option<Cow<'static, str>> {
let mut values = std::mem::take(&mut self.entry.values).into_iter();
match values.next()? {
VCardValue::Text(v) => Some(Cow::Owned(v)),
VCardValue::Component(v) => Some(Cow::Owned(v.join(","))),
VCardValue::Binary(data) => Some(Cow::Owned(data.to_unwrapped_string())),
VCardValue::Sex(v) => Some(v.as_str().into()),
VCardValue::GramGender(v) => Some(v.as_str().into()),
VCardValue::Kind(v) => Some(v.as_str().into()),
other => {
self.entry.values.push(other);
self.entry.values.extend(values);
None
}
}
}
pub(super) fn text_parts_borrowed(&self) -> impl Iterator<Item = &str> + '_ {
self.entry.values.iter().filter_map(|value| value.as_text())
}
pub(super) fn to_kind<I: JSContactId, B: JSContactId>(
&mut self,
) -> Option<Value<'static, JSContactProperty<I>, JSContactValue<I, B>>> {
let mut values = std::mem::take(&mut self.entry.values).into_iter();
match values.next()? {
VCardValue::Kind(v) => Value::Element(JSContactValue::Kind(match v {
VCardKind::Application => JSContactKind::Application,
VCardKind::Device => JSContactKind::Device,
VCardKind::Group => JSContactKind::Group,
VCardKind::Individual => JSContactKind::Individual,
VCardKind::Location => JSContactKind::Location,
VCardKind::Org => JSContactKind::Org,
}))
.into(),
VCardValue::Text(v) => Value::Str(v.into()).into(),
VCardValue::Component(v) => Value::Str(v.join(",").into()).into(),
other => {
self.entry.values.push(other);
self.entry.values.extend(values);
None
}
}
}
pub(super) fn to_gram_gender<I: JSContactId, B: JSContactId>(
&mut self,
) -> Option<Value<'static, JSContactProperty<I>, JSContactValue<I, B>>> {
let mut values = std::mem::take(&mut self.entry.values).into_iter();
match values.next()? {
VCardValue::GramGender(v) => {
Value::Element(JSContactValue::GrammaticalGender(match v {
VCardGramGender::Animate => JSContactGrammaticalGender::Animate,
VCardGramGender::Common => JSContactGrammaticalGender::Common,
VCardGramGender::Feminine => JSContactGrammaticalGender::Feminine,
VCardGramGender::Inanimate => JSContactGrammaticalGender::Inanimate,
VCardGramGender::Masculine => JSContactGrammaticalGender::Masculine,
VCardGramGender::Neuter => JSContactGrammaticalGender::Neuter,
}))
.into()
}
VCardValue::Text(v) => Value::Str(v.into()).into(),
VCardValue::Component(v) => Value::Str(v.join(",").into()).into(),
other => {
self.entry.values.push(other);
self.entry.values.extend(values);
None
}
}
}
pub(super) fn to_timestamp<I: JSContactId, B: JSContactId>(
&mut self,
) -> Option<Value<'static, JSContactProperty<I>, JSContactValue<I, B>>> {
if let Some(VCardValue::PartialDateTime(dt)) = self.entry.values.first()
&& let Some(timestamp) = dt.to_timestamp()
{
return Value::Element(JSContactValue::Timestamp(timestamp)).into();
}
None
}
pub(super) fn to_tz<I: JSContactId, B: JSContactId>(
&mut self,
) -> Option<Value<'static, JSContactProperty<I>, JSContactValue<I, B>>> {
let mut values = std::mem::take(&mut self.entry.values).into_iter();
match values.next()? {
VCardValue::PartialDateTime(v) if v.has_zone() => {
let hour = v.tz_hour.unwrap_or_default();
Value::Str(
if hour != 0 {
format!("Etc/GMT{}{}", if v.tz_minus { "+" } else { "-" }, hour)
} else {
"Etc/UTC".to_string()
}
.into(),
)
.into()
}
VCardValue::Text(v) => Value::Str(v.into()).into(),
VCardValue::Component(v) => Value::Str(v.join(",").into()).into(),
other => {
self.entry.values.push(other);
self.entry.values.extend(values);
None
}
}
}
pub(super) fn to_anniversary<I: JSContactId, B: JSContactId>(
&mut self,
) -> Option<Value<'static, JSContactProperty<I>, JSContactValue<I, B>>> {
if let Some(VCardValue::PartialDateTime(dt)) = self.entry.values.first() {
if let Some(timestamp) = dt.to_timestamp() {
Some(Value::Object(Map::from(vec![
(
Key::Property(JSContactProperty::Type),
Value::Element(JSContactValue::Type(JSContactType::Timestamp)),
),
(
Key::Property(JSContactProperty::Utc),
Value::Element(JSContactValue::Timestamp(timestamp)),
),
])))
} else if dt.day.is_some() || dt.month.is_some() || dt.year.is_some() {
let mut props = Map::from(vec![(
Key::Property(JSContactProperty::Type),
Value::Element(JSContactValue::Type(JSContactType::PartialDate)),
)]);
if let Some(year) = dt.year {
props.insert_unchecked(
Key::Property(JSContactProperty::Year),
Value::Number((year as u64).into()),
);
}
if let Some(month) = dt.month {
props.insert_unchecked(
Key::Property(JSContactProperty::Month),
Value::Number((month as u64).into()),
);
}
if let Some(day) = dt.day {
props.insert_unchecked(
Key::Property(JSContactProperty::Day),
Value::Number((day as u64).into()),
);
}
Some(Value::Object(props))
} else {
None
}
} else {
None
}
}
pub(super) fn non_default_language(&self, default_language: Option<&str>) -> Option<&str> {
self.entry
.language()
.filter(|&lang| Some(lang) != default_language)
}
}
+260
View File
@@ -0,0 +1,260 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{
CalendarScale, IanaType,
blob::{BlobIdFn, BlobIdGenerator, BlobIds, BlobOptions, NoBlobIds},
jsprop::ordered::OrderedMap,
},
jscontact::{JSContactId, JSContactProperty, JSContactValue},
vcard::{
Jscomp, VCardEntry, VCardLevel, VCardParameterName, VCardPhonetic, VCardProperty, VCardType,
},
};
use jmap_tools::{Element, JsonPointer, Key, Map, Property, Value};
pub mod convert;
pub mod entry;
pub mod params;
pub mod props;
type JSValue<I, B> = Value<'static, JSContactProperty<I>, JSContactValue<I, B>>;
type Member<I, B> = (Key<'static, JSContactProperty<I>>, JSValue<I, B>);
type PatchObject<I, B> = (JsonPointer<JSContactProperty<I>>, JSValue<I, B>);
type Localization<I, B> = Vec<(String, JSValue<I, B>)>;
struct CardProperties<I, B>(Vec<Member<I, B>>)
where
I: JSContactId,
B: JSContactId;
struct State<I, B>
where
I: JSContactId,
B: JSContactId,
{
entries: CardProperties<I, B>,
vcard_converted_properties: OrderedMap<String, VCardConvertedProperty<I, B>>,
vcard_properties: Vec<JSValue<I, B>>,
patch_objects: Vec<PatchObject<I, B>>,
localizations: OrderedMap<String, Localization<I, B>>,
default_language: Option<String>,
prop_ids: PropIds<I>,
name_alt_id: Option<String>,
has_fn: bool,
has_fn_localization: bool,
has_n: bool,
has_n_localization: bool,
has_gram_gender: bool,
include_vcard_converted: bool,
}
#[derive(Debug, Clone)]
struct EntryState {
entry: VCardEntry,
converted_to: Option<String>,
map_name: bool,
keep_converted_path: bool,
}
struct PropIdKey<I>
where
I: JSContactId,
{
prop: VCardProperty,
prop_js: JSContactProperty<I>,
group: Option<String>,
alt_id: Option<String>,
prop_id: String,
}
struct PropIds<I>
where
I: JSContactId,
{
keys: Vec<PropIdKey<I>>,
is_enabled: bool,
}
#[derive(Debug, Default)]
struct VCardConvertedProperty<I, B>
where
I: JSContactId,
B: JSContactId,
{
name: Option<VCardProperty>,
params: VCardParams<I, B>,
}
#[derive(Debug, Default)]
struct VCardParams<I, B>(OrderedMap<VCardParameterName, Vec<JSValue<I, B>>>)
where
I: JSContactId,
B: JSContactId;
#[derive(Default)]
struct ExtractedParams {
language: Option<String>,
pref: Option<u32>,
author: Option<String>,
author_name: Option<String>,
phonetic_system: Option<IanaType<VCardPhonetic, String>>,
phonetic_script: Option<String>,
media_type: Option<String>,
calscale: Option<IanaType<CalendarScale, String>>,
sort_as: Option<String>,
prop_id: Option<String>,
alt_id: Option<String>,
types: Vec<IanaType<VCardType, String>>,
geo: Option<String>,
tz: Option<String>,
index: Option<u32>,
level: Option<IanaType<VCardLevel, String>>,
country_code: Option<String>,
created: Option<i64>,
label: Option<String>,
service_type: Option<String>,
username: Option<String>,
jscomps: Vec<Jscomp>,
}
impl<I, B> CardProperties<I, B>
where
I: JSContactId,
B: JSContactId,
{
fn with_capacity(capacity: usize) -> Self {
Self(Vec::with_capacity(capacity))
}
fn position(&self, property: &JSContactProperty<I>) -> Option<usize> {
self.0
.iter()
.position(|(key, _)| matches!(key, Key::Property(existing) if existing == property))
}
fn contains(&self, property: &JSContactProperty<I>) -> bool {
self.position(property).is_some()
}
fn get_mut(&mut self, property: &JSContactProperty<I>) -> Option<&mut JSValue<I, B>> {
self.0.iter_mut().find_map(|(key, value)| {
matches!(key, Key::Property(existing) if existing == property).then_some(value)
})
}
fn insert(&mut self, property: JSContactProperty<I>, value: JSValue<I, B>) {
match self.get_mut(&property) {
Some(existing) => *existing = value,
None => self.0.push((Key::Property(property), value)),
}
}
fn get_mut_object_or_insert(
&mut self,
property: JSContactProperty<I>,
) -> &mut Map<'static, JSContactProperty<I>, JSContactValue<I, B>> {
let index = self.position(&property).unwrap_or_else(|| {
self.0.push((
Key::Property(property),
Value::Object(Map::from(Vec::new())),
));
self.0.len() - 1
});
self.0[index].1.object_or_replace()
}
fn into_map(self) -> Map<'static, JSContactProperty<I>, JSContactValue<I, B>> {
Map::from(self.0)
}
}
impl<I, B> VCardParams<I, B>
where
I: JSContactId,
B: JSContactId,
{
fn push(&mut self, name: VCardParameterName, value: JSValue<I, B>) {
match self.0.get_mut(&name) {
Some(values) => values.push(value),
None => self.0.push(name, vec![value]),
}
}
fn set(&mut self, name: VCardParameterName, values: Vec<JSValue<I, B>>) {
self.0.insert(name, values);
}
}
trait ObjectValue<'x, P: Property, E: Element> {
fn object_or_replace(&mut self) -> &mut Map<'x, P, E>;
}
impl<'x, P: Property, E: Element> ObjectValue<'x, P, E> for Value<'x, P, E> {
#[inline]
fn object_or_replace(&mut self) -> &mut Map<'x, P, E> {
match self {
Value::Object(object) => object,
value => {
*value = Value::Object(Map::from(Vec::new()));
value.object_or_replace()
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ImportOptions<G = NoBlobIds> {
include_vcard_parameters: bool,
blobs: BlobOptions<G>,
}
impl Default for ImportOptions {
fn default() -> Self {
Self {
include_vcard_parameters: true,
blobs: BlobOptions::default(),
}
}
}
impl ImportOptions {
pub fn new() -> Self {
Self::default()
}
}
impl<G> ImportOptions<G> {
pub fn include_vcard_parameters(mut self, include: bool) -> Self {
self.include_vcard_parameters = include;
self
}
pub fn with_blob_ids<B, F>(self, blob_ids: F) -> ImportOptions<BlobIdFn<F>>
where
F: FnMut(&[u8]) -> Option<B>,
{
self.with_blob_id_generator(BlobIdFn(blob_ids))
}
pub fn with_blob_id_generator<T>(self, blob_id_generator: T) -> ImportOptions<T> {
ImportOptions {
include_vcard_parameters: self.include_vcard_parameters,
blobs: self.blobs.with_handler(blob_id_generator),
}
}
pub(super) fn blob_ids<B: Clone>(&mut self) -> Option<BlobIds<'_, B>>
where
G: BlobIdGenerator<B>,
{
self.blobs.blob_ids()
}
}
@@ -0,0 +1,410 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{IanaType, jsprop::text::IntoAsciiLowercase},
jscontact::{
Context, Feature, JSContactId, JSContactKind, JSContactLevel, JSContactPhoneticSystem,
JSContactProperty, JSContactValue,
import::{ExtractedParams, Member, VCardParams},
},
vcard::{
VCardLevel, VCardParameter, VCardParameterName, VCardParameterValue, VCardPhonetic,
VCardProperty, VCardType,
},
};
use jmap_tools::{Key, Map, Value};
use std::{mem, str::FromStr};
impl Feature {
pub(crate) fn from_vcard_type(property: &VCardProperty, typ: &VCardType) -> Option<Self> {
match (property, typ) {
(VCardProperty::Tel, VCardType::Cell) => Some(Feature::Mobile),
(VCardProperty::Tel, VCardType::Fax) => Some(Feature::Fax),
(VCardProperty::Tel, VCardType::MainNumber) => Some(Feature::MainNumber),
(VCardProperty::Tel, VCardType::Pager) => Some(Feature::Pager),
(VCardProperty::Tel, VCardType::Text) => Some(Feature::Text),
(VCardProperty::Tel, VCardType::Textphone) => Some(Feature::TextPhone),
(VCardProperty::Tel, VCardType::Video) => Some(Feature::Video),
(VCardProperty::Tel, VCardType::Voice) => Some(Feature::Voice),
_ => None,
}
}
}
impl<I: JSContactId> JSContactProperty<I> {
pub(crate) fn from_vcard_type(
property: &VCardProperty,
typ: IanaType<VCardType, String>,
) -> (Self, Key<'static, Self>) {
let feature = match &typ {
IanaType::Iana(typ) => Feature::from_vcard_type(property, typ),
IanaType::Other(_) => None,
};
if let Some(feature) = feature {
return (
JSContactProperty::Features,
Key::Property(JSContactProperty::Feature(feature)),
);
}
let key = match typ {
IanaType::Iana(VCardType::Home) => {
Key::Property(JSContactProperty::Context(Context::Private))
}
IanaType::Iana(VCardType::Work) => {
Key::Property(JSContactProperty::Context(Context::Work))
}
IanaType::Iana(VCardType::Billing) => {
Key::Property(JSContactProperty::Context(Context::Billing))
}
IanaType::Iana(VCardType::Delivery) => {
Key::Property(JSContactProperty::Context(Context::Delivery))
}
IanaType::Iana(VCardType::Cell) => Key::Borrowed("mobile"),
typ => {
let key = typ.into_string().to_ascii_lowercase();
match Context::from_str(&key) {
Ok(context) => Key::Property(JSContactProperty::Context(context)),
Err(()) => Key::Owned(key),
}
}
};
(JSContactProperty::Contexts, key)
}
}
impl VCardParameter {
pub(super) fn take_value(&mut self) -> VCardParameterValue {
mem::replace(&mut self.value, VCardParameterValue::Null)
}
pub(super) fn as_media_type(&self) -> Option<&str> {
match (&self.name, &self.value) {
(VCardParameterName::Mediatype, VCardParameterValue::Text(media_type))
if !media_type.is_empty() =>
{
Some(media_type)
}
_ => None,
}
}
}
impl ExtractedParams {
pub(super) fn prop_id(&mut self) -> Option<String> {
self.prop_id.take()
}
pub(super) fn alt_id(&mut self) -> Option<String> {
self.alt_id.take()
}
pub(super) fn language(&mut self) -> Option<String> {
self.language.take()
}
pub(super) fn types(&mut self) -> Vec<IanaType<VCardType, String>> {
std::mem::take(&mut self.types)
}
pub(super) fn members<I: JSContactId, B: JSContactId>(
&mut self,
property: &VCardProperty,
) -> ParamMembers<'_, I, B> {
let mut contexts: Option<Vec<Member<I, B>>> = None;
let mut features: Option<Vec<Member<I, B>>> = None;
for typ in std::mem::take(&mut self.types) {
let (bucket, key) = JSContactProperty::<I>::from_vcard_type(property, typ);
let keys = if bucket == JSContactProperty::Features {
features.get_or_insert_default()
} else {
contexts.get_or_insert_default()
};
if keys.iter().all(|(existing, _)| existing != &key) {
keys.push((key, Value::Bool(true)));
}
}
ParamMembers {
contexts,
features,
is_name: matches!(property, VCardProperty::N),
is_address: matches!(property, VCardProperty::Adr),
params: self,
}
}
}
pub(super) struct ParamMembers<'x, I: JSContactId, B: JSContactId> {
contexts: Option<Vec<Member<I, B>>>,
features: Option<Vec<Member<I, B>>>,
is_name: bool,
is_address: bool,
params: &'x mut ExtractedParams,
}
impl<I: JSContactId, B: JSContactId> ParamMembers<'_, I, B> {
pub(super) fn len(&self) -> usize {
let p = &*self.params;
[
self.contexts.is_some(),
self.features.is_some(),
p.language.is_some(),
p.pref.is_some(),
p.author.is_some() || p.author_name.is_some(),
p.media_type.is_some(),
p.phonetic_system.is_some(),
p.phonetic_script.is_some(),
p.calscale.is_some(),
p.sort_as.is_some(),
p.geo.is_some(),
p.tz.is_some(),
p.index.is_some(),
p.level.is_some(),
p.country_code.is_some(),
p.created.is_some(),
p.label.is_some(),
p.service_type.is_some(),
p.username.is_some(),
]
.into_iter()
.filter(|&present| present)
.count()
}
pub(super) fn is_empty(&self) -> bool {
self.len() == 0
}
pub(super) fn append_to(self, members: &mut Vec<Member<I, B>>) {
self.for_each(|key, value| members.push((key, value)));
}
pub(super) fn for_each(
self,
mut emit: impl FnMut(
Key<'static, JSContactProperty<I>>,
Value<'static, JSContactProperty<I>, JSContactValue<I, B>>,
),
) {
let p = self.params;
if let Some(contexts) = self.contexts {
emit(
Key::Property(JSContactProperty::Contexts),
Value::Object(Map::from(contexts)),
);
}
if let Some(features) = self.features {
emit(
Key::Property(JSContactProperty::Features),
Value::Object(Map::from(features)),
);
}
if let Some(language) = p.language.take() {
emit(
Key::Property(JSContactProperty::Language),
Value::Str(language.into()),
);
}
if let Some(pref) = p.pref.take() {
emit(
Key::Property(JSContactProperty::Pref),
Value::Number((pref as u64).into()),
);
}
if p.author.is_some() || p.author_name.is_some() {
emit(
Key::Property(JSContactProperty::Author),
Value::Object(Map::from_iter(
[
p.author_name.take().map(|name| {
(
Key::Property(JSContactProperty::Name),
Value::Str(name.into()),
)
}),
p.author.take().map(|author| {
(
Key::Property(JSContactProperty::Uri),
Value::Str(author.into()),
)
}),
]
.into_iter()
.flatten(),
)),
);
}
if let Some(media_type) = p.media_type.take() {
emit(
Key::Property(JSContactProperty::MediaType),
Value::Str(media_type.into()),
);
}
if let Some(phonetic_system) = p.phonetic_system.take() {
emit(
Key::Property(JSContactProperty::PhoneticSystem),
match phonetic_system {
IanaType::Iana(value) => {
Value::Element(JSContactValue::PhoneticSystem(match value {
VCardPhonetic::Ipa => JSContactPhoneticSystem::Ipa,
VCardPhonetic::Jyut => JSContactPhoneticSystem::Jyut,
VCardPhonetic::Piny => JSContactPhoneticSystem::Piny,
VCardPhonetic::Script => JSContactPhoneticSystem::Script,
}))
}
IanaType::Other(value) => Value::Str(value.into()),
},
);
}
if let Some(phonetic_script) = p.phonetic_script.take() {
emit(
Key::Property(JSContactProperty::PhoneticScript),
Value::Str(phonetic_script.into()),
);
}
if let Some(calscale) = p.calscale.take() {
emit(
Key::Property(JSContactProperty::CalendarScale),
match calscale {
IanaType::Iana(value) => Value::Element(JSContactValue::CalendarScale(value)),
IanaType::Other(value) => Value::Str(value.into()),
},
);
}
if let Some(sort_as) = p.sort_as.take() {
emit(
Key::Property(JSContactProperty::SortAs),
if self.is_name {
if let Some((surname, given)) =
sort_as.split_once(',').and_then(|(surname, given)| {
let surname = surname.trim();
let given = given.trim();
if surname.is_empty() || given.is_empty() {
None
} else {
Some((surname.to_string(), given.to_string()))
}
})
{
Value::Object(Map::from_iter(vec![
(
Key::Property(JSContactProperty::SortAsKind(
JSContactKind::Surname,
)),
Value::Str(surname.into()),
),
(
Key::Property(JSContactProperty::SortAsKind(JSContactKind::Given)),
Value::Str(given.into()),
),
]))
} else {
Value::Object(Map::from_iter(vec![(
Key::Property(JSContactProperty::SortAsKind(JSContactKind::Surname)),
Value::Str(sort_as.into()),
)]))
}
} else {
Value::Str(sort_as.into())
},
);
}
if let Some(geo) = p.geo.take() {
emit(
Key::Property(JSContactProperty::Coordinates),
Value::Str(geo.into()),
);
}
if let Some(tz) = p.tz.take() {
emit(
Key::Property(JSContactProperty::TimeZone),
Value::Str(tz.into()),
);
}
if let Some(index) = p.index.take() {
emit(
Key::Property(JSContactProperty::ListAs),
Value::Number((index as u64).into()),
);
}
if let Some(level) = p.level.take() {
emit(
Key::Property(JSContactProperty::Level),
match level {
IanaType::Iana(value) => Value::Element(JSContactValue::Level(match value {
VCardLevel::Beginner | VCardLevel::Low => JSContactLevel::Low,
VCardLevel::Average | VCardLevel::Medium => JSContactLevel::Medium,
VCardLevel::Expert | VCardLevel::High => JSContactLevel::High,
})),
IanaType::Other(value) => Value::Str(value.into()),
},
);
}
if let Some(country_code) = p.country_code.take() {
emit(
Key::Property(JSContactProperty::CountryCode),
Value::Str(country_code.into()),
);
}
if let Some(created) = p.created.take() {
emit(
Key::Property(JSContactProperty::Created),
Value::Element(JSContactValue::Timestamp(created)),
);
}
if let Some(label) = p.label.take() {
emit(
Key::Property(if self.is_address {
JSContactProperty::Full
} else {
JSContactProperty::Label
}),
Value::Str(label.into()),
);
}
if let Some(service_type) = p.service_type.take() {
emit(
Key::Property(JSContactProperty::Service),
Value::Str(service_type.into()),
);
}
if let Some(username) = p.username.take() {
emit(
Key::Property(JSContactProperty::User),
Value::Str(username.into()),
);
}
}
}
impl<I: JSContactId, B: JSContactId> VCardParams<I, B> {
pub(super) fn into_jscontact_value(
self,
) -> Option<Map<'static, JSContactProperty<I>, JSContactValue<I, B>>> {
if !self.0.is_empty() {
let mut obj = Map::from(Vec::with_capacity(self.0.len()));
for (param, value) in self.0 {
let value = match <[_; 1]>::try_from(value) {
Ok([value]) => value,
Err(values) => Value::Array(values),
};
obj.insert_unchecked(
Key::Owned(param.into_string().into_ascii_lowercase()),
value,
);
}
Some(obj)
} else {
None
}
}
}
@@ -0,0 +1,965 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{
Data, IanaString, IanaType,
blob::{BlobIds, sniff_media_type},
format::AsciiPush,
jsprop::{
ordered::OrderedMap,
text::{AsciiString, PointerString},
},
},
jscontact::{
JSContact, JSContactId, JSContactKind, JSContactProperty, JSContactType, JSContactValue,
import::{
CardProperties, EntryState, ExtractedParams, Localization, Member, PropIdKey, PropIds,
State, VCardConvertedProperty, VCardParams,
},
},
vcard::{
VCard, VCardEntry, VCardParameter, VCardParameterName, VCardParameterValue, VCardProperty,
VCardValue, VCardValueType,
},
};
use jmap_tools::{JsonPointerHandler, JsonPointerItem, Key, Map, Property, Value};
use std::{
borrow::Cow,
hash::{Hash, Hasher},
iter::once,
mem,
};
const NUMBERED_KEY_CAPACITY: usize = 8;
#[derive(Clone, Copy)]
struct LanguageTag<'x>(&'x str);
impl<I, B> State<I, B>
where
I: JSContactId,
B: JSContactId,
{
pub(super) fn new(vcard: &mut VCard, include_vcard_converted: bool) -> Self {
let mut entries = CardProperties::with_capacity(vcard.entries.len().min(20) + 4);
entries.insert(
JSContactProperty::Type,
Value::Element(JSContactValue::Type(JSContactType::Card)),
);
entries.insert(JSContactProperty::Version, Value::Str("1.0".into()));
let mut default_language = None;
let mut language_counts: OrderedMap<LanguageTag<'_>, usize> = OrderedMap::default();
let mut language_count = 0;
let mut language_first_found = None;
let mut langful_names: OrderedMap<&VCardProperty, ()> = OrderedMap::default();
let mut name_alt_ids: OrderedMap<&str, usize> = OrderedMap::default();
let mut has_prop_id_lookups = false;
for entry in &vcard.entries {
has_prop_id_lookups |= entry.looks_up_prop_ids();
if let Some(lang) = entry.language() {
language_counts.upsert(LanguageTag(lang), || 0, |count| *count += 1);
if language_first_found.is_none() {
language_first_found = Some(lang);
}
language_count += 1;
langful_names.insert_if_absent(&entry.name, ());
}
match &entry.name {
VCardProperty::Language => {
if let Some(VCardValue::Text(lang)) = entry.values.first() {
default_language = Some(lang.to_ascii_lowercase());
}
}
VCardProperty::N => {
if let Some(alt_id) = entry.alt_id() {
name_alt_ids.upsert(alt_id, || 0, |count| *count += 1);
}
}
_ => (),
}
}
let mut name_alt_id = None;
let mut name_alt_id_count = 0;
for (&alt_id, &count) in name_alt_ids.iter() {
if count > name_alt_id_count {
name_alt_id = Some(alt_id);
name_alt_id_count = count;
}
}
let name_alt_id = name_alt_id.map(str::to_string);
if default_language.is_none()
&& language_count > 1
&& let Some(min_count) = language_counts.iter().map(|(_, &count)| count).min()
&& let Some((LanguageTag(most_used), max_count)) =
language_counts.into_iter().max_by_key(|&(_, count)| count)
&& max_count
> vcard
.entries
.iter()
.filter(|entry| {
entry.language().is_none() && langful_names.contains_key(&entry.name)
})
.count()
{
let lang = if max_count == min_count
&& let Some(first) = language_first_found
{
first
} else {
most_used
}
.to_ascii_lowercase();
default_language = Some(lang.clone());
vcard
.entries
.push(VCardEntry::new(VCardProperty::Language).with_value(lang));
}
let sort_key = |entry: &VCardEntry| {
let lang = entry.language();
let weight = u32::from(lang.is_some() && default_language.as_deref() != lang);
match &entry.name {
VCardProperty::Birthplace | VCardProperty::Deathplace | VCardProperty::Role => {
weight + 2
}
VCardProperty::Other(name) if name.eq_ignore_ascii_case("X-ABLabel") => weight + 3,
_ => weight,
}
};
if !vcard.entries.is_sorted_by_key(sort_key) {
vcard.entries.sort_by_key(sort_key);
}
Self {
entries,
default_language,
localizations: Default::default(),
prop_ids: PropIds::new(has_prop_id_lookups, &vcard.entries),
vcard_converted_properties: Default::default(),
vcard_properties: Default::default(),
patch_objects: Default::default(),
name_alt_id,
has_fn: false,
has_n: false,
has_n_localization: false,
has_fn_localization: false,
has_gram_gender: false,
include_vcard_converted,
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn map_named_entry(
&mut self,
entry: &mut EntryState,
extract: &[VCardParameterName],
top_property_name: JSContactProperty<I>,
value_property_name: JSContactProperty<I>,
extra_properties: [Option<Member<I, B>>; 2],
) {
let value = if !matches!(
entry.entry.name,
VCardProperty::Anniversary | VCardProperty::Bday | VCardProperty::Deathdate
) {
entry.to_text()
} else {
entry.to_anniversary()
};
if let Some(value) = value {
self.map_named_value(
entry,
extract,
top_property_name,
value_property_name,
value,
extra_properties,
);
}
}
pub(super) fn map_blob_media(
&mut self,
entry: &mut EntryState,
kind: JSContactKind,
blob_ids: &mut BlobIds<'_, B>,
) -> bool {
if !matches!(
entry.entry.values.first(),
Some(VCardValue::Binary(data)) if !data.data.is_empty()
) {
return false;
}
let mut values = mem::take(&mut entry.entry.values).into_iter();
let Some(VCardValue::Binary(data)) = values.next() else {
return false;
};
let media_type_param = entry
.entry
.params
.iter()
.find_map(|param| param.as_media_type());
let media_type = match media_type_param.or(data
.content_type
.as_deref()
.filter(|media_type| !media_type.is_empty()))
{
Some(media_type) => Cow::Owned(media_type.to_string()),
None => Cow::Borrowed(sniff_media_type(&data.data)),
};
let has_media_type_param = media_type_param.is_some();
match blob_ids.blob_id(data.data, Some(media_type.as_ref())) {
Ok(generated) => {
entry.entry.params.retain(|param| {
param.name != VCardParameterName::Mediatype || param.as_media_type().is_some()
});
self.map_named_value(
entry,
&[
VCardParameterName::Mediatype,
VCardParameterName::Pref,
VCardParameterName::PropId,
VCardParameterName::Label,
],
JSContactProperty::Media,
JSContactProperty::BlobId,
Value::Element(JSContactValue::BlobId(generated.blob_id)),
[
Some((
Key::Property(JSContactProperty::Kind),
Value::Element(JSContactValue::Kind(kind)),
)),
(!has_media_type_param).then(|| {
(
Key::Property(JSContactProperty::MediaType),
Value::Str(media_type.into_owned().into()),
)
}),
],
);
true
}
Err(bytes) => {
entry.entry.values = once(VCardValue::Binary(Box::new(Data {
content_type: data.content_type,
data: bytes,
})))
.chain(values)
.collect();
false
}
}
}
fn map_named_value(
&mut self,
entry: &mut EntryState,
extract: &[VCardParameterName],
top_property_name: JSContactProperty<I>,
value_property_name: JSContactProperty<I>,
value: Value<'static, JSContactProperty<I>, JSContactValue<I, B>>,
extra_properties: [Option<Member<I, B>>; 2],
) {
let mut params = ExtractedParams::default();
params.extract(
&mut entry.entry.params,
extract,
self.default_language.as_deref(),
);
let prop_id = params.prop_id();
let alt_id = params.alt_id();
let sub_property = top_property_name.sub_property();
if let Some(language) = params.language() {
if let Some(patch) = prop_id
.as_deref()
.filter(|prop_id| self.has_prop_id(&entry.entry.name, prop_id))
.or_else(|| {
self.find_prop_id(
&entry.entry.name,
entry.entry.group.as_deref(),
alt_id.as_deref(),
)
})
.map(|prop_id| {
if let Some(sub_property) = sub_property.as_ref() {
format!(
"{}/{}/{}/{}",
top_property_name.to_cow().as_ref(),
sub_property.to_cow().as_ref(),
prop_id,
value_property_name.to_cow().as_ref()
)
} else {
format!(
"{}/{}/{}",
top_property_name.to_cow().as_ref(),
prop_id,
value_property_name.to_cow().as_ref()
)
}
})
{
entry.set_converted_to(|| {
String::from_pointer([
JSContactProperty::Localizations::<I>.to_cow().as_ref(),
language.as_str(),
patch.as_str(),
])
});
self.localize(language, |localizations| {
let base_path = patch
.rsplit_once('/')
.map_or(patch.as_str(), |(base, _)| base);
params
.members::<I, B>(&entry.entry.name)
.for_each(|prop, value| {
localizations
.push((format!("{}/{}", base_path, prop.to_string()), value));
});
localizations.push((patch, value));
});
return;
} else {
entry.entry.params.push(VCardParameter::language(language));
}
}
let mut entries = self
.entries
.get_mut_object_or_insert(top_property_name.clone());
if let Some(sub_property) = sub_property.clone() {
let Some(sub_entries) = entries
.insert_or_get_mut(sub_property, Value::Object(Map::from(vec![])))
.as_object_mut()
else {
return;
};
entries = sub_entries;
}
let members = params.members::<I, B>(&entry.entry.name);
let mut obj =
Vec::with_capacity(1 + extra_properties.iter().flatten().count() + members.len());
obj.push((Key::Property(value_property_name.clone()), value));
obj.extend(extra_properties.into_iter().flatten());
members.append_to(&mut obj);
let prop_id = entries.named_key(prop_id);
if let Some(sub_property) = sub_property {
entry.set_converted_to(|| {
String::from_pointer([
top_property_name.to_cow().as_ref(),
sub_property.to_cow().as_ref(),
prop_id.as_str(),
value_property_name.to_cow().as_ref(),
])
});
} else {
entry.set_converted_to(|| {
String::from_pointer([
top_property_name.to_cow().as_ref(),
prop_id.as_str(),
value_property_name.to_cow().as_ref(),
])
});
}
self.prop_ids
.track(&entry.entry, top_property_name, alt_id, &prop_id);
entries.insert_unchecked(Key::Owned(prop_id), Value::Object(Map::from(obj)));
}
#[inline]
pub(super) fn get_mut_object_or_insert(
&mut self,
key: JSContactProperty<I>,
) -> &mut Map<'static, JSContactProperty<I>, JSContactValue<I, B>> {
self.entries.get_mut_object_or_insert(key)
}
#[inline]
pub(super) fn has_property(&self, key: JSContactProperty<I>) -> bool {
self.entries.contains(&key)
}
pub(super) fn localize(
&mut self,
language: String,
update: impl FnOnce(&mut Localization<I, B>),
) {
self.localizations.upsert(language, Vec::new, update);
}
pub(super) fn add_conversion_props(&mut self, mut entry: EntryState) {
if self.include_vcard_converted {
if let Some(converted_to) = entry.converted_to.take() {
if entry.has_conversion_params() {
let mut value_type = None;
match self.vcard_converted_properties.get_mut(&converted_to) {
Some(conv_prop) => {
entry.jcal_parameters(&mut conv_prop.params, &mut value_type);
}
None => {
let mut params = VCardParams::default();
entry.jcal_parameters(&mut params, &mut value_type);
if let Some(value_type) = value_type {
params.set(
VCardParameterName::Value,
vec![Value::Str(value_type.into_string())],
);
}
if !params.0.is_empty() || entry.map_name {
self.vcard_converted_properties.push(
converted_to,
VCardConvertedProperty {
name: if entry.map_name {
Some(entry.entry.name)
} else {
None
},
params,
},
);
}
}
}
}
} else {
let mut value_type = None;
let mut params = VCardParams::default();
entry.jcal_parameters(&mut params, &mut value_type);
let mut values = entry.entry.values.into_iter();
let values = match (values.next(), values.len()) {
(Some(value), 0) => value.into_jscontact_value(value_type.as_ref()),
(first, _) => Value::Array(
first
.into_iter()
.chain(values)
.map(|value| value.into_jscontact_value(value_type.as_ref()))
.collect(),
),
};
let name = match entry.entry.name {
VCardProperty::Other(mut name) => {
name.make_ascii_lowercase();
name
}
name => name.as_str().to_ascii_lowercase(),
};
self.vcard_properties.push(Value::Array(vec![
Value::Str(name.into()),
Value::Object(
params
.into_jscontact_value()
.unwrap_or(Map::from(Vec::new())),
),
Value::Str(
value_type
.map(|v| v.into_string())
.unwrap_or(Cow::Borrowed("unknown")),
),
values,
]));
}
}
}
pub(super) fn find_prop_id(
&self,
prop: &VCardProperty,
group: Option<&str>,
alt_id: Option<&str>,
) -> Option<&str> {
self.prop_ids
.keys
.iter()
.find(|p| {
p.prop == *prop && p.group.as_deref() == group && p.alt_id.as_deref() == alt_id
})
.map(|p| p.prop_id.as_str())
}
pub(super) fn find_entry_by_group(&self, group: Option<&str>) -> Option<&PropIdKey<I>> {
self.prop_ids
.keys
.iter()
.find(|p| p.group.as_deref() == group)
}
pub(super) fn has_prop_id(&self, prop: &VCardProperty, prop_id: &str) -> bool {
self.prop_ids
.keys
.iter()
.any(|p| p.prop == *prop && p.prop_id == prop_id)
}
pub(super) fn into_jscontact(mut self) -> JSContact<'static, I, B> {
if !self.localizations.is_empty() {
self.entries.insert(
JSContactProperty::Localizations,
Value::Object(
self.localizations
.into_iter()
.map(|(lang, locals)| {
(
Key::Owned(lang),
Value::Object(
locals
.into_iter()
.map(|(key, value)| (Key::Owned(key), value))
.collect(),
),
)
})
.collect(),
),
);
}
let mut vcard_obj = Map::from(Vec::new());
if !self.vcard_converted_properties.is_empty() {
let mut converted_properties =
Map::from(Vec::with_capacity(self.vcard_converted_properties.len()));
for (converted_to, props) in self.vcard_converted_properties {
let mut obj = Map::from(Vec::with_capacity(2));
if let Some(params) = props.params.into_jscontact_value() {
obj.insert(
Key::Property(JSContactProperty::Parameters),
Value::Object(params),
);
}
if let Some(name) = props.name {
obj.insert(
Key::Property(JSContactProperty::Name),
Value::Str(name.as_str().to_ascii_lowercase().into()),
);
}
converted_properties.insert_unchecked(Key::Owned(converted_to), Value::Object(obj));
}
vcard_obj.insert_unchecked(
Key::Property(JSContactProperty::ConvertedProperties),
Value::Object(converted_properties),
);
}
if !self.vcard_properties.is_empty() {
vcard_obj.insert_unchecked(
Key::Property(JSContactProperty::Properties),
Value::Array(self.vcard_properties),
);
}
if !vcard_obj.is_empty() {
self.entries
.insert(JSContactProperty::VCard, Value::Object(vcard_obj));
}
let mut obj = Value::Object(self.entries.into_map());
if !self.patch_objects.is_empty() {
for (ptr, patch) in self.patch_objects {
patch_card(&mut obj, ptr.as_slice(), patch);
}
}
JSContact(obj)
}
}
impl PartialEq for LanguageTag<'_> {
fn eq(&self, other: &Self) -> bool {
self.0.eq_ignore_ascii_case(other.0)
}
}
impl Eq for LanguageTag<'_> {}
impl Hash for LanguageTag<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
for byte in self.0.bytes() {
state.write_u8(byte.to_ascii_lowercase());
}
state.write_usize(self.0.len());
}
}
impl<I> JSContactProperty<I>
where
I: JSContactId,
{
pub(super) fn sub_property(&self) -> Option<JSContactProperty<I>> {
match self {
JSContactProperty::SpeakToAs => Some(JSContactProperty::Pronouns),
_ => None,
}
}
}
impl VCardValue {
pub(super) fn into_jscontact_value<I: JSContactId, B: JSContactId>(
self,
value_type: Option<&IanaType<VCardValueType, String>>,
) -> Value<'static, JSContactProperty<I>, JSContactValue<I, B>> {
match self {
VCardValue::Text(v) => Value::Str(v.into()),
VCardValue::Component(v) => Value::Str(v.join(",").into()),
VCardValue::Integer(v) => Value::Number(v.into()),
VCardValue::Float(v) => Value::Number(v.into()),
VCardValue::Boolean(v) => Value::Bool(v),
VCardValue::PartialDateTime(v) => {
let mut out = String::new();
let _ = v.format_as_vcard(
&mut out,
value_type
.and_then(|v| v.iana())
.unwrap_or(if v.has_date() && v.has_time() {
&VCardValueType::Timestamp
} else {
&VCardValueType::DateAndOrTime
}),
);
Value::Str(out.into())
}
VCardValue::Binary(v) => Value::Str(v.to_unwrapped_string().into()),
VCardValue::Sex(v) => Value::Str(v.as_str().into()),
VCardValue::GramGender(v) => Value::Str(v.as_str().into()),
VCardValue::Kind(v) => Value::Str(v.as_str().into()),
}
}
}
impl ExtractedParams {
pub(super) fn extract(
&mut self,
params: &mut Vec<VCardParameter>,
extract: &[VCardParameterName],
default_language: Option<&str>,
) {
let p = self;
params.retain_mut(|param| match &param.name {
VCardParameterName::Language => {
let v = param.take_value().into_text().to_ascii_lowercase();
if p.language.is_none()
&& default_language.is_none_or(|lang| lang != v)
&& extract.contains(&VCardParameterName::Language)
{
p.language = Some(v);
false
} else {
param.value = VCardParameterValue::Text(v);
true
}
}
VCardParameterName::Pref
if p.pref.is_none() && extract.contains(&VCardParameterName::Pref) =>
{
p.pref = param.value.as_integer().and_then(|v| v.into_iana());
false
}
VCardParameterName::Author
if p.author.is_none() && extract.contains(&VCardParameterName::Author) =>
{
p.author = Some(param.take_value().into_text().into_owned());
false
}
VCardParameterName::AuthorName
if p.author_name.is_none() && extract.contains(&VCardParameterName::AuthorName) =>
{
p.author_name = Some(param.take_value().into_text().into_owned());
false
}
VCardParameterName::Mediatype
if p.media_type.is_none() && extract.contains(&VCardParameterName::Mediatype) =>
{
p.media_type = Some(param.take_value().into_text().into_owned());
false
}
VCardParameterName::Calscale
if p.calscale.is_none() && extract.contains(&VCardParameterName::Calscale) =>
{
p.calscale = param.take_value().into_calscale();
false
}
VCardParameterName::SortAs
if p.sort_as.is_none() && extract.contains(&VCardParameterName::SortAs) =>
{
p.sort_as = Some(param.take_value().into_text().into_owned());
false
}
VCardParameterName::Geo
if p.geo.is_none() && extract.contains(&VCardParameterName::Geo) =>
{
p.geo = Some(param.take_value().into_text().into_owned());
false
}
VCardParameterName::Tz
if p.tz.is_none() && extract.contains(&VCardParameterName::Tz) =>
{
p.tz = Some(param.take_value().into_text().into_owned());
false
}
VCardParameterName::Index
if p.index.is_none() && extract.contains(&VCardParameterName::Index) =>
{
p.index = param.value.as_integer().and_then(|v| v.into_iana());
false
}
VCardParameterName::Level
if p.level.is_none() && extract.contains(&VCardParameterName::Level) =>
{
p.level = param.take_value().into_level();
false
}
VCardParameterName::Cc
if p.country_code.is_none() && extract.contains(&VCardParameterName::Cc) =>
{
p.country_code = Some(param.take_value().into_text().into_owned());
false
}
VCardParameterName::Created
if p.created.is_none() && extract.contains(&VCardParameterName::Created) =>
{
p.created = param
.take_value()
.into_timestamp()
.and_then(|v| v.into_iana());
false
}
VCardParameterName::Label
if p.label.is_none() && extract.contains(&VCardParameterName::Label) =>
{
p.label = Some(param.take_value().into_text().into_owned());
false
}
VCardParameterName::Phonetic
if p.phonetic_system.is_none()
&& extract.contains(&VCardParameterName::Phonetic) =>
{
p.phonetic_system = param.take_value().into_phonetic();
false
}
VCardParameterName::Script
if p.phonetic_script.is_none() && extract.contains(&VCardParameterName::Script) =>
{
p.phonetic_script = Some(param.take_value().into_text().into_owned());
false
}
VCardParameterName::ServiceType
if p.service_type.is_none()
&& extract.contains(&VCardParameterName::ServiceType) =>
{
p.service_type = Some(param.take_value().into_text().into_owned());
false
}
VCardParameterName::Username
if p.username.is_none() && extract.contains(&VCardParameterName::Username) =>
{
p.username = Some(param.take_value().into_text().into_owned());
false
}
VCardParameterName::PropId
if p.prop_id.is_none() && extract.contains(&VCardParameterName::PropId) =>
{
p.prop_id = Some(param.take_value().into_text().into_owned());
false
}
VCardParameterName::Altid if p.alt_id.is_none() => {
p.alt_id = param.value.as_text().map(|v| v.to_string());
true
}
VCardParameterName::Type if extract.contains(&VCardParameterName::Type) => {
if let Some(typ) = param.take_value().into_type() {
p.types.push(typ);
}
false
}
VCardParameterName::Jscomps if extract.contains(&VCardParameterName::Jscomps) => {
if let Some(jscomps) = param.take_value().into_jscomps() {
p.jscomps = jscomps;
}
false
}
_ => true,
});
}
}
pub(super) trait NamedKey {
fn named_key(&self, key: Option<String>) -> String;
}
impl<I: JSContactId, B: JSContactId> NamedKey
for Map<'static, JSContactProperty<I>, JSContactValue<I, B>>
{
fn named_key(&self, key: Option<String>) -> String {
let number = self.len() as u64 + 1;
let key = key.unwrap_or_else(|| {
let mut key = AsciiString::with_capacity(NUMBERED_KEY_CAPACITY);
let _ = key.push_byte(b'k').and_then(|()| key.push_u64(number));
key.into_string()
});
if self.contains_key(&Key::Borrowed(key.as_str())) {
let mut key = AsciiString::from(key);
let _ = key.push_byte(b'-').and_then(|()| key.push_u64(number));
key.into_string()
} else {
key
}
}
}
impl<I: JSContactId> PropIds<I> {
fn new(is_enabled: bool, entries: &[VCardEntry]) -> Self {
Self {
keys: if is_enabled {
Vec::with_capacity(
entries
.iter()
.filter(|entry| entry.name.is_named_property())
.count(),
)
} else {
Vec::new()
},
is_enabled,
}
}
pub(super) fn track(
&mut self,
entry: &VCardEntry,
prop_js: JSContactProperty<I>,
alt_id: Option<String>,
prop_id: &str,
) {
if self.is_enabled {
self.keys.push(PropIdKey {
prop_id: prop_id.to_string(),
prop_js,
prop: entry.name.clone(),
group: entry.group.clone(),
alt_id,
});
}
}
}
impl VCardEntry {
fn looks_up_prop_ids(&self) -> bool {
match &self.name {
VCardProperty::Birthplace
| VCardProperty::Deathplace
| VCardProperty::Role
| VCardProperty::Tz
| VCardProperty::Geo => true,
VCardProperty::Other(name)
if self.group.is_some() && name.eq_ignore_ascii_case("X-ABLabel") =>
{
true
}
_ => self
.params
.iter()
.any(|param| param.name == VCardParameterName::Language),
}
}
}
impl VCardProperty {
fn is_named_property(&self) -> bool {
matches!(
self,
VCardProperty::Source
| VCardProperty::OrgDirectory
| VCardProperty::Anniversary
| VCardProperty::Bday
| VCardProperty::Deathdate
| VCardProperty::Birthplace
| VCardProperty::Deathplace
| VCardProperty::Pronouns
| VCardProperty::Nickname
| VCardProperty::Photo
| VCardProperty::Logo
| VCardProperty::Sound
| VCardProperty::Adr
| VCardProperty::Email
| VCardProperty::Impp
| VCardProperty::Lang
| VCardProperty::Socialprofile
| VCardProperty::Tel
| VCardProperty::ContactUri
| VCardProperty::Title
| VCardProperty::Role
| VCardProperty::Hobby
| VCardProperty::Interest
| VCardProperty::Expertise
| VCardProperty::Org
| VCardProperty::Note
| VCardProperty::Url
| VCardProperty::Key
| VCardProperty::Caladruri
| VCardProperty::Fburl
| VCardProperty::Caluri
)
}
}
fn patch_card<I: JSContactId, B: JSContactId>(
value: &mut Value<'static, JSContactProperty<I>, JSContactValue<I, B>>,
pointer: &[JsonPointerItem<JSContactProperty<I>>],
patch: Value<'static, JSContactProperty<I>, JSContactValue<I, B>>,
) -> bool {
match (pointer.split_first(), value) {
(Some((JsonPointerItem::Root, rest)), value) => patch_card(value, rest, patch),
(Some((JsonPointerItem::Number(index), rest)), Value::Array(items)) => {
match (items.get_mut(*index as usize), rest.is_empty()) {
(Some(item), false) => patch_card(item, rest, patch),
(Some(item), true) if !patch.is_null() => {
*item = patch;
true
}
_ => false,
}
}
(Some((JsonPointerItem::Key(_) | JsonPointerItem::Number(_), rest)), value)
if rest
.iter()
.all(|item| !matches!(item, JsonPointerItem::Number(_))) =>
{
value.patch_jptr(pointer.iter().peekable(), patch)
}
(Some((JsonPointerItem::Key(key), rest)), Value::Object(obj)) => obj
.get_mut(key)
.is_some_and(|item| patch_card(item, rest, patch)),
(Some((JsonPointerItem::Number(index), rest)), Value::Object(obj)) => {
let index = index.to_string();
obj.iter_mut()
.find(|(key, _)| key.to_string() == index)
.is_some_and(|(_, item)| patch_card(item, rest, patch))
}
_ => false,
}
}
+442
View File
@@ -0,0 +1,442 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use crate::common::{CalendarScale, elements::Elements};
use jmap_tools::{JsonPointer, Value};
use serde::Serialize;
use std::{
borrow::Cow,
fmt::{Debug, Display},
hash::Hash,
str::FromStr,
};
pub mod export;
pub mod import;
pub mod parser;
pub mod types;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[repr(transparent)]
pub struct JSContact<'x, I, B>(pub Value<'x, JSContactProperty<I>, JSContactValue<I, B>>)
where
I: JSContactId,
B: JSContactId;
pub trait JSContactId:
FromStr + Sized + Serialize + Display + Clone + Eq + Hash + Ord + Debug + Default
{
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSContactValue<I: JSContactId, B: JSContactId> {
Id(I),
BlobId(B),
IdReference(String),
Timestamp(i64),
Type(JSContactType),
GrammaticalGender(JSContactGrammaticalGender),
Kind(JSContactKind),
Level(JSContactLevel),
Relation(JSContactRelation),
PhoneticSystem(JSContactPhoneticSystem),
CalendarScale(CalendarScale),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSContactProperty<I: JSContactId> {
Type,
Address,
AddressBookIds,
Addresses,
Anniversaries,
Author,
BlobId,
Calendars,
CalendarScale,
Components,
Contexts,
ConvertedProperties,
Coordinates,
CountryCode,
Created,
CryptoKeys,
Date,
Day,
DefaultSeparator,
Directories,
Emails,
Extra,
Features,
Full,
GrammaticalGender,
Id,
IsOrdered,
Keywords,
Kind,
Label,
Language,
Level,
Links,
ListAs,
Localizations,
Media,
MediaType,
Members,
Metadata,
Month,
Name,
Nicknames,
Note,
Notes,
Number,
OnlineServices,
OrganizationId,
Organizations,
Parameters,
PersonalInfo,
Phones,
Phonetic,
PhoneticScript,
PhoneticSystem,
Place,
Pref,
PreferredLanguages,
PrivateMetadata,
ProdId,
Pronouns,
Properties,
RelatedTo,
Relation,
SchedulingAddresses,
Service,
SortAs,
SpeakToAs,
TimeZone,
Titles,
Uid,
Units,
Updated,
Uri,
User,
Utc,
VCard,
Value,
Version,
Year,
IdValue(I),
IdReference(String),
Context(Context),
Feature(Feature),
SortAsKind(JSContactKind),
Pointer(JsonPointer<JSContactProperty<I>>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSContactType {
Address,
AddressComponent,
Anniversary,
Author,
Boolean,
Calendar,
Card,
CryptoKey,
Directory,
EmailAddress,
Id,
Int,
JCardProp,
LanguagePref,
Link,
Media,
Name,
NameComponent,
Nickname,
Note,
Number,
OnlineService,
Organization,
OrgUnit,
PartialDate,
PatchObject,
PersonalInfo,
Phone,
Pronouns,
Relation,
Resource,
SchedulingAddress,
SpeakToAs,
String,
Timestamp,
Title,
UnsignedInt,
UTCDateTime,
}
impl FromStr for JSContactType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSContactType,
"Address" => JSContactType::Address,
"AddressComponent" => JSContactType::AddressComponent,
"Anniversary" => JSContactType::Anniversary,
"Author" => JSContactType::Author,
"Boolean" => JSContactType::Boolean,
"Calendar" => JSContactType::Calendar,
"Card" => JSContactType::Card,
"CryptoKey" => JSContactType::CryptoKey,
"Directory" => JSContactType::Directory,
"EmailAddress" => JSContactType::EmailAddress,
"Id" => JSContactType::Id,
"Int" => JSContactType::Int,
"JCardProp" => JSContactType::JCardProp,
"LanguagePref" => JSContactType::LanguagePref,
"Link" => JSContactType::Link,
"Media" => JSContactType::Media,
"Name" => JSContactType::Name,
"NameComponent" => JSContactType::NameComponent,
"Nickname" => JSContactType::Nickname,
"Note" => JSContactType::Note,
"Number" => JSContactType::Number,
"OnlineService" => JSContactType::OnlineService,
"Organization" => JSContactType::Organization,
"OrgUnit" => JSContactType::OrgUnit,
"PartialDate" => JSContactType::PartialDate,
"PatchObject" => JSContactType::PatchObject,
"PersonalInfo" => JSContactType::PersonalInfo,
"Phone" => JSContactType::Phone,
"Pronouns" => JSContactType::Pronouns,
"Relation" => JSContactType::Relation,
"Resource" => JSContactType::Resource,
"SchedulingAddress" => JSContactType::SchedulingAddress,
"SpeakToAs" => JSContactType::SpeakToAs,
"String" => JSContactType::String,
"Timestamp" => JSContactType::Timestamp,
"Title" => JSContactType::Title,
"UnsignedInt" => JSContactType::UnsignedInt,
"UTCDateTime" => JSContactType::UTCDateTime,
)
.copied()
.ok_or(())
}
}
impl JSContactType {
pub fn as_str(&self) -> &'static str {
match self {
JSContactType::Address => "Address",
JSContactType::AddressComponent => "AddressComponent",
JSContactType::Anniversary => "Anniversary",
JSContactType::Author => "Author",
JSContactType::Boolean => "Boolean",
JSContactType::Calendar => "Calendar",
JSContactType::Card => "Card",
JSContactType::CryptoKey => "CryptoKey",
JSContactType::Directory => "Directory",
JSContactType::EmailAddress => "EmailAddress",
JSContactType::Id => "Id",
JSContactType::Int => "Int",
JSContactType::JCardProp => "JCardProp",
JSContactType::LanguagePref => "LanguagePref",
JSContactType::Link => "Link",
JSContactType::Media => "Media",
JSContactType::Name => "Name",
JSContactType::NameComponent => "NameComponent",
JSContactType::Nickname => "Nickname",
JSContactType::Note => "Note",
JSContactType::Number => "Number",
JSContactType::OnlineService => "OnlineService",
JSContactType::Organization => "Organization",
JSContactType::OrgUnit => "OrgUnit",
JSContactType::PartialDate => "PartialDate",
JSContactType::PatchObject => "PatchObject",
JSContactType::PersonalInfo => "PersonalInfo",
JSContactType::Phone => "Phone",
JSContactType::Pronouns => "Pronouns",
JSContactType::Relation => "Relation",
JSContactType::Resource => "Resource",
JSContactType::SchedulingAddress => "SchedulingAddress",
JSContactType::SpeakToAs => "SpeakToAs",
JSContactType::String => "String",
JSContactType::Timestamp => "Timestamp",
JSContactType::Title => "Title",
JSContactType::UnsignedInt => "UnsignedInt",
JSContactType::UTCDateTime => "UTCDateTime",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Context {
Billing,
Delivery,
Private,
Work,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Feature {
Fax,
MainNumber,
Mobile,
Pager,
Text,
TextPhone,
Video,
Voice,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSContactGrammaticalGender {
Animate,
Common,
Feminine,
Inanimate,
Masculine,
Neuter,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSContactKind {
Apartment,
Block,
Building,
Country,
Direction,
District,
Floor,
Landmark,
Locality,
Name,
Number,
Postcode,
PostOfficeBox,
Region,
Room,
Separator,
Subdistrict,
Birth,
Death,
Wedding,
Calendar,
FreeBusy,
Application,
Device,
Group,
Individual,
Location,
Org,
Directory,
Entry,
Contact,
Logo,
Photo,
Sound,
Credential,
Generation,
Given,
Given2,
Surname,
Surname2,
Title,
Expertise,
Hobby,
Interest,
Role,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSContactLevel {
High,
Low,
Medium,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSContactRelation {
Acquaintance,
Agent,
Child,
Colleague,
Contact,
CoResident,
CoWorker,
Crush,
Date,
Emergency,
Friend,
Kin,
Me,
Met,
Muse,
Neighbor,
Parent,
Sibling,
Spouse,
Sweetheart,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum JSContactPhoneticSystem {
Ipa,
Jyut,
Piny,
Script,
}
#[cfg(all(test, inbuxa_tests))]
impl<I, B> std::fmt::Display for JSContact<'_, I, B>
where
I: JSContactId,
B: JSContactId,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = serde_json::to_string_pretty(&self.0).map_err(|_| std::fmt::Error)?;
write!(f, "{}", s)
}
}
impl<T> JSContactId for T where
T: FromStr + Sized + Serialize + Display + Clone + Eq + Hash + Ord + Debug + Default
{
}
impl<I: JSContactId> From<I> for JSContactProperty<I> {
fn from(id: I) -> Self {
JSContactProperty::IdValue(id)
}
}
impl<I: JSContactId, B: JSContactId> From<I> for JSContactValue<I, B> {
fn from(id: I) -> Self {
JSContactValue::Id(id)
}
}
impl<I: JSContactId, B: JSContactId> JSContact<'_, I, B> {
pub fn blob_ids(&self) -> impl Iterator<Item = &B> {
Elements::new(&self.0).filter_map(|element| match element {
JSContactValue::BlobId(blob_id) => Some(blob_id),
_ => None,
})
}
}
impl<'x, I: JSContactId, B: JSContactId> Default for JSContact<'x, I, B> {
fn default() -> Self {
Self(Value::Object(jmap_tools::Map::new()))
}
}
impl<I: JSContactId> Display for JSContactProperty<I> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.to_string().as_ref())
}
}
+399
View File
@@ -0,0 +1,399 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{
CalendarScale, IanaParse, IdReference,
jsprop::text::{DisplayEq, IdReferenceText, PointerText, PointerTextExt},
},
jscalendar::{
JSCalendarDateTime,
parser::{BLOB_ID_SUFFIX, KEY_TEXT, StackText, VALUE_TEXT},
},
jscontact::{
Context, Feature, JSContact, JSContactGrammaticalGender, JSContactId, JSContactKind,
JSContactLevel, JSContactPhoneticSystem, JSContactProperty, JSContactRelation,
JSContactType, JSContactValue,
},
};
use jiff::Timestamp;
use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, PointerDepth, Property as _, Value};
use serde::Serializer;
use std::{
borrow::Cow,
fmt::{self, Write},
mem::discriminant,
str::FromStr,
};
impl<'x, I, B> JSContact<'x, I, B>
where
I: JSContactId,
B: JSContactId,
{
pub fn parse(json: &'x str) -> Result<Self, String> {
Value::parse_json(json).map(JSContact)
}
pub fn to_string_pretty(&self) -> String {
serde_json::to_string_pretty(&self.0).unwrap_or_default()
}
}
impl<I, B> Element for JSContactValue<I, B>
where
I: JSContactId,
B: JSContactId,
{
type Property = JSContactProperty<I>;
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
if let Key::Property(prop) = key {
match prop.patch_or_prop() {
JSContactProperty::Type => JSContactType::from_str(value)
.ok()
.map(JSContactValue::Type),
JSContactProperty::CalendarScale => {
CalendarScale::parse(value.as_bytes()).map(JSContactValue::CalendarScale)
}
JSContactProperty::Created
| JSContactProperty::Updated
| JSContactProperty::Utc => value
.parse::<Timestamp>()
.map(|dt| JSContactValue::Timestamp(dt.as_second()))
.ok(),
JSContactProperty::Kind => JSContactKind::from_str(value)
.ok()
.map(JSContactValue::Kind),
JSContactProperty::GrammaticalGender => JSContactGrammaticalGender::from_str(value)
.ok()
.map(JSContactValue::GrammaticalGender),
JSContactProperty::PhoneticSystem => JSContactPhoneticSystem::from_str(value)
.ok()
.map(JSContactValue::PhoneticSystem),
JSContactProperty::Relation => JSContactRelation::from_str(value)
.ok()
.map(JSContactValue::Relation),
JSContactProperty::Level => JSContactLevel::from_str(value)
.ok()
.map(JSContactValue::Level),
JSContactProperty::BlobId => match IdReference::parse(value) {
IdReference::Value(value) => JSContactValue::BlobId(value).into(),
IdReference::Reference(value) => JSContactValue::IdReference(value).into(),
IdReference::Error => None,
},
JSContactProperty::Id => match IdReference::parse(value) {
IdReference::Value(value) => JSContactValue::Id(value).into(),
IdReference::Reference(value) => JSContactValue::IdReference(value).into(),
IdReference::Error => None,
},
_ => None,
}
} else if key
.as_string_key()
.is_some_and(|name| name.ends_with(BLOB_ID_SUFFIX))
{
match IdReference::parse(value) {
IdReference::Value(value) => JSContactValue::BlobId(value).into(),
IdReference::Reference(value) => JSContactValue::IdReference(value).into(),
IdReference::Error => None,
}
} else {
None
}
}
fn to_cow(&self) -> Cow<'static, str> {
match self {
JSContactValue::Type(v) => v.as_str().into(),
JSContactValue::GrammaticalGender(v) => v.as_str().into(),
JSContactValue::Kind(v) => v.as_str().into(),
JSContactValue::Level(v) => v.as_str().into(),
JSContactValue::Relation(v) => v.as_str().into(),
JSContactValue::PhoneticSystem(v) => v.as_str().into(),
JSContactValue::Timestamp(v) => JSCalendarDateTime::new(*v, false).to_rfc3339().into(),
JSContactValue::CalendarScale(v) => v.as_js_str().into(),
JSContactValue::Id(v) => v.to_string().into(),
JSContactValue::BlobId(v) => v.to_string().into(),
JSContactValue::IdReference(s) => s.to_id_reference().into(),
}
}
#[inline]
fn serialize_text<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
JSContactValue::Timestamp(_)
| JSContactValue::Id(_)
| JSContactValue::BlobId(_)
| JSContactValue::IdReference(_) => self.serialize_data_text(serializer),
_ => serializer.serialize_str(&self.to_cow()),
}
}
}
impl<I: JSContactId, B: JSContactId> JSContactValue<I, B> {
#[inline(never)]
fn serialize_data_text<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
StackText::<VALUE_TEXT>::serialize(
serializer,
|text| match self {
JSContactValue::Timestamp(timestamp) => {
JSCalendarDateTime::new(*timestamp, false).push_rfc3339(text)
}
JSContactValue::Id(id) => write!(text, "{id}"),
JSContactValue::BlobId(id) => write!(text, "{id}"),
JSContactValue::IdReference(id) => text.push_id_reference(id),
_ => Err(fmt::Error),
},
|| self.to_cow(),
)
}
}
impl<I: JSContactId> jmap_tools::Property for JSContactProperty<I> {
fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
Self::try_parse_nested(key, value, PointerDepth::default())
}
fn try_parse_nested(
key: Option<&Key<'_, Self>>,
value: &str,
depth: PointerDepth,
) -> Option<Self> {
match key {
Some(Key::Property(key)) if key.is_metadata() => None,
Some(Key::Property(key)) => match key.patch_or_prop() {
JSContactProperty::Contexts => Context::from_str(value)
.ok()
.map(JSContactProperty::Context),
JSContactProperty::Features => Feature::from_str(value)
.ok()
.map(JSContactProperty::Feature),
JSContactProperty::SortAs => JSContactKind::from_str(value)
.ok()
.map(JSContactProperty::SortAsKind),
JSContactProperty::ConvertedProperties | JSContactProperty::Localizations => {
JsonPointer::parse_nested(value, depth).map(JSContactProperty::Pointer)
}
JSContactProperty::AddressBookIds => match IdReference::parse(value) {
IdReference::Value(value) => JSContactProperty::IdValue(value).into(),
IdReference::Reference(value) => JSContactProperty::IdReference(value).into(),
IdReference::Error => None,
},
JSContactProperty::Pointer(language)
if matches!(
language.as_slice(),
[JsonPointerItem::Key(Key::Borrowed(_) | Key::Owned(_))]
) =>
{
JSContactProperty::parse_name(value)
}
_ => JSContactProperty::parse_nested_name(value),
},
None if value.contains('/') => {
JsonPointer::parse_nested(value, depth).map(JSContactProperty::Pointer)
}
None => JSContactProperty::parse_name(value),
Some(_) => JSContactProperty::parse_nested_name(value),
}
}
fn is_opaque(&self) -> bool {
self.is_metadata()
}
fn to_cow(&self) -> Cow<'static, str> {
match self {
JSContactProperty::Pointer(pointer) => pointer.to_text().into(),
JSContactProperty::IdReference(id) => id.to_id_reference().into(),
_ => self.to_string(),
}
}
#[inline]
fn key_eq(&self, other: &Self) -> bool {
match (self, other) {
(a, b) if !a.has_data() && !b.has_data() => discriminant(a) == discriminant(b),
_ => self.data_key_eq(other),
}
}
#[inline]
fn key_eq_str(&self, other: &str) -> bool {
if !self.has_data() {
self.to_string() == other
} else {
self.data_key_eq_str(other)
}
}
#[inline]
fn serialize_text<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
JSContactProperty::Pointer(_)
| JSContactProperty::IdValue(_)
| JSContactProperty::IdReference(_) => self.serialize_data_text(serializer),
_ => serializer.serialize_str(&self.to_cow()),
}
}
}
impl<I: JSContactId> JSContactProperty<I> {
#[inline(never)]
fn serialize_data_text<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
StackText::<KEY_TEXT>::serialize(
serializer,
|text| match self {
JSContactProperty::Pointer(pointer) => {
text.push_pointer(pointer, |key, text| text.push_pointer_token(&key.to_cow()))
}
JSContactProperty::IdValue(id) => write!(text, "{id}"),
JSContactProperty::IdReference(id) => text.push_id_reference(id),
_ => Err(fmt::Error),
},
|| self.to_cow(),
)
}
#[inline(never)]
fn data_key_eq(&self, other: &Self) -> bool {
match (self, other) {
(JSContactProperty::Pointer(a), JSContactProperty::Pointer(b)) if a == b => true,
_ => match (self.static_name(), other.static_name()) {
(Some(a), Some(b)) => a == b,
(Some(a), None) => other.data_key_eq_str(a),
(None, Some(b)) => self.data_key_eq_str(b),
(None, None) => other.data_key_eq_str(&self.to_string()),
},
}
}
#[inline(never)]
fn data_key_eq_str(&self, other: &str) -> bool {
match self {
JSContactProperty::IdValue(id) => id.display_eq(other),
JSContactProperty::Pointer(pointer) => pointer.text_eq(other),
JSContactProperty::IdReference(reference) => {
other.strip_prefix('#') == Some(reference.as_str())
}
_ => self.to_string() == other,
}
}
#[inline]
fn has_data(&self) -> bool {
match self {
JSContactProperty::IdValue(_)
| JSContactProperty::IdReference(_)
| JSContactProperty::Context(_)
| JSContactProperty::Feature(_)
| JSContactProperty::SortAsKind(_)
| JSContactProperty::Pointer(_) => true,
JSContactProperty::Type
| JSContactProperty::Address
| JSContactProperty::AddressBookIds
| JSContactProperty::Addresses
| JSContactProperty::Anniversaries
| JSContactProperty::Author
| JSContactProperty::BlobId
| JSContactProperty::Calendars
| JSContactProperty::CalendarScale
| JSContactProperty::Components
| JSContactProperty::Contexts
| JSContactProperty::ConvertedProperties
| JSContactProperty::Coordinates
| JSContactProperty::CountryCode
| JSContactProperty::Created
| JSContactProperty::CryptoKeys
| JSContactProperty::Date
| JSContactProperty::Day
| JSContactProperty::DefaultSeparator
| JSContactProperty::Directories
| JSContactProperty::Emails
| JSContactProperty::Extra
| JSContactProperty::Features
| JSContactProperty::Full
| JSContactProperty::GrammaticalGender
| JSContactProperty::Id
| JSContactProperty::IsOrdered
| JSContactProperty::Keywords
| JSContactProperty::Kind
| JSContactProperty::Label
| JSContactProperty::Language
| JSContactProperty::Level
| JSContactProperty::Links
| JSContactProperty::ListAs
| JSContactProperty::Localizations
| JSContactProperty::Media
| JSContactProperty::MediaType
| JSContactProperty::Members
| JSContactProperty::Month
| JSContactProperty::Name
| JSContactProperty::Nicknames
| JSContactProperty::Note
| JSContactProperty::Notes
| JSContactProperty::Number
| JSContactProperty::OnlineServices
| JSContactProperty::OrganizationId
| JSContactProperty::Organizations
| JSContactProperty::Parameters
| JSContactProperty::PersonalInfo
| JSContactProperty::Phones
| JSContactProperty::Phonetic
| JSContactProperty::PhoneticScript
| JSContactProperty::PhoneticSystem
| JSContactProperty::Place
| JSContactProperty::Pref
| JSContactProperty::PreferredLanguages
| JSContactProperty::ProdId
| JSContactProperty::Pronouns
| JSContactProperty::Properties
| JSContactProperty::RelatedTo
| JSContactProperty::Relation
| JSContactProperty::SchedulingAddresses
| JSContactProperty::Service
| JSContactProperty::SortAs
| JSContactProperty::SpeakToAs
| JSContactProperty::TimeZone
| JSContactProperty::Titles
| JSContactProperty::Uid
| JSContactProperty::Units
| JSContactProperty::Updated
| JSContactProperty::Uri
| JSContactProperty::User
| JSContactProperty::Utc
| JSContactProperty::VCard
| JSContactProperty::Value
| JSContactProperty::Version
| JSContactProperty::Year
| JSContactProperty::Metadata
| JSContactProperty::PrivateMetadata => false,
}
}
fn static_name(&self) -> Option<&'static str> {
match self {
JSContactProperty::Pointer(_)
| JSContactProperty::IdValue(_)
| JSContactProperty::IdReference(_) => None,
_ => match self.to_string() {
Cow::Borrowed(name) => Some(name),
Cow::Owned(_) => None,
},
}
}
fn patch_or_prop(&self) -> &JSContactProperty<I> {
if let JSContactProperty::Pointer(ptr) = self
&& let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last()
{
prop
} else {
self
}
}
}
+602
View File
@@ -0,0 +1,602 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::jsprop::{property_names, text::PointerProperty},
jscontact::*,
};
use jmap_tools::JsonPointerItem;
use std::str::FromStr;
impl<I: JSContactId> PointerProperty for JSContactProperty<I> {
fn into_pointer(self) -> Result<JsonPointer<Self>, Self> {
match self {
JSContactProperty::Pointer(pointer) => Ok(pointer),
property => Err(property),
}
}
}
impl<I: JSContactId> FromStr for JSContactProperty<I> {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
JSContactProperty::parse_name(s)
.or_else(|| JSContactProperty::parse_metadata_selector(s))
.ok_or(())
}
}
impl<I: JSContactId> JSContactProperty<I> {
property_names!(
JSContactProperty,
"@type" => Type,
"address" => Address,
"addressBookIds" => AddressBookIds,
"addresses" => Addresses,
"anniversaries" => Anniversaries,
"author" => Author,
"blobId" => BlobId,
"calendars" => Calendars,
"calendarScale" => CalendarScale,
"components" => Components,
"contexts" => Contexts,
"convertedProperties" => ConvertedProperties,
"coordinates" => Coordinates,
"countryCode" => CountryCode,
"created" => Created,
"cryptoKeys" => CryptoKeys,
"date" => Date,
"day" => Day,
"defaultSeparator" => DefaultSeparator,
"directories" => Directories,
"emails" => Emails,
"extra" => Extra,
"features" => Features,
"full" => Full,
"grammaticalGender" => GrammaticalGender,
"id" => Id,
"isOrdered" => IsOrdered,
"keywords" => Keywords,
"kind" => Kind,
"label" => Label,
"language" => Language,
"level" => Level,
"links" => Links,
"listAs" => ListAs,
"localizations" => Localizations,
"media" => Media,
"mediaType" => MediaType,
"members" => Members,
"month" => Month,
"name" => Name,
"nicknames" => Nicknames,
"note" => Note,
"notes" => Notes,
"number" => Number,
"onlineServices" => OnlineServices,
"organizationId" => OrganizationId,
"organizations" => Organizations,
"parameters" => Parameters,
"personalInfo" => PersonalInfo,
"phones" => Phones,
"phonetic" => Phonetic,
"phoneticScript" => PhoneticScript,
"phoneticSystem" => PhoneticSystem,
"place" => Place,
"pref" => Pref,
"preferredLanguages" => PreferredLanguages,
"prodId" => ProdId,
"pronouns" => Pronouns,
"properties" => Properties,
"relatedTo" => RelatedTo,
"relation" => Relation,
"schedulingAddresses" => SchedulingAddresses,
"service" => Service,
"sortAs" => SortAs,
"speakToAs" => SpeakToAs,
"timeZone" => TimeZone,
"titles" => Titles,
"uid" => Uid,
"units" => Units,
"updated" => Updated,
"uri" => Uri,
"user" => User,
"utc" => Utc,
"value" => Value,
"vCard" => VCard,
"version" => Version,
"year" => Year,
);
fn parse_metadata_selector(s: &str) -> Option<Self> {
s.split_once('/')
.and_then(|(root, _)| JSContactProperty::<I>::parse_name(root))
.filter(JSContactProperty::is_metadata)
.map(|_| JSContactProperty::Pointer(JsonPointer::parse(s)))
}
pub(crate) fn is_metadata(&self) -> bool {
match self {
JSContactProperty::Metadata | JSContactProperty::PrivateMetadata => true,
JSContactProperty::Pointer(pointer) => JSContactProperty::is_metadata_pointer(pointer),
_ => false,
}
}
pub(crate) fn is_metadata_pointer(pointer: &JsonPointer<Self>) -> bool {
pointer
.first()
.and_then(JsonPointerItem::as_property_key)
.is_some_and(JSContactProperty::is_metadata)
}
pub fn to_string(&self) -> Cow<'static, str> {
match self {
JSContactProperty::Type => "@type",
JSContactProperty::Address => "address",
JSContactProperty::AddressBookIds => "addressBookIds",
JSContactProperty::Addresses => "addresses",
JSContactProperty::Anniversaries => "anniversaries",
JSContactProperty::Author => "author",
JSContactProperty::BlobId => "blobId",
JSContactProperty::Calendars => "calendars",
JSContactProperty::CalendarScale => "calendarScale",
JSContactProperty::Components => "components",
JSContactProperty::Contexts => "contexts",
JSContactProperty::ConvertedProperties => "convertedProperties",
JSContactProperty::Coordinates => "coordinates",
JSContactProperty::CountryCode => "countryCode",
JSContactProperty::Created => "created",
JSContactProperty::CryptoKeys => "cryptoKeys",
JSContactProperty::Date => "date",
JSContactProperty::Day => "day",
JSContactProperty::DefaultSeparator => "defaultSeparator",
JSContactProperty::Directories => "directories",
JSContactProperty::Emails => "emails",
JSContactProperty::Extra => "extra",
JSContactProperty::Features => "features",
JSContactProperty::Full => "full",
JSContactProperty::GrammaticalGender => "grammaticalGender",
JSContactProperty::Id => "id",
JSContactProperty::IsOrdered => "isOrdered",
JSContactProperty::Keywords => "keywords",
JSContactProperty::Kind => "kind",
JSContactProperty::Label => "label",
JSContactProperty::Language => "language",
JSContactProperty::Level => "level",
JSContactProperty::Links => "links",
JSContactProperty::ListAs => "listAs",
JSContactProperty::Localizations => "localizations",
JSContactProperty::Media => "media",
JSContactProperty::MediaType => "mediaType",
JSContactProperty::Members => "members",
JSContactProperty::Month => "month",
JSContactProperty::Name => "name",
JSContactProperty::Nicknames => "nicknames",
JSContactProperty::Note => "note",
JSContactProperty::Notes => "notes",
JSContactProperty::Number => "number",
JSContactProperty::OnlineServices => "onlineServices",
JSContactProperty::OrganizationId => "organizationId",
JSContactProperty::Organizations => "organizations",
JSContactProperty::Parameters => "parameters",
JSContactProperty::PersonalInfo => "personalInfo",
JSContactProperty::Phones => "phones",
JSContactProperty::Phonetic => "phonetic",
JSContactProperty::PhoneticScript => "phoneticScript",
JSContactProperty::PhoneticSystem => "phoneticSystem",
JSContactProperty::Place => "place",
JSContactProperty::Pref => "pref",
JSContactProperty::PreferredLanguages => "preferredLanguages",
JSContactProperty::ProdId => "prodId",
JSContactProperty::Pronouns => "pronouns",
JSContactProperty::Properties => "properties",
JSContactProperty::RelatedTo => "relatedTo",
JSContactProperty::Relation => "relation",
JSContactProperty::SchedulingAddresses => "schedulingAddresses",
JSContactProperty::Service => "service",
JSContactProperty::SortAs => "sortAs",
JSContactProperty::SpeakToAs => "speakToAs",
JSContactProperty::TimeZone => "timeZone",
JSContactProperty::Titles => "titles",
JSContactProperty::Uid => "uid",
JSContactProperty::Units => "units",
JSContactProperty::Updated => "updated",
JSContactProperty::Uri => "uri",
JSContactProperty::User => "user",
JSContactProperty::Utc => "utc",
JSContactProperty::Value => "value",
JSContactProperty::VCard => "vCard",
JSContactProperty::Version => "version",
JSContactProperty::Year => "year",
JSContactProperty::Metadata => "metadata",
JSContactProperty::PrivateMetadata => "privateMetadata",
JSContactProperty::Context(context) => context.as_str(),
JSContactProperty::Feature(feature) => feature.as_str(),
JSContactProperty::SortAsKind(kind) => kind.as_str(),
JSContactProperty::Pointer(pointer) => return Cow::Owned(pointer.to_string()),
JSContactProperty::IdValue(id) => return Cow::Owned(id.to_string()),
JSContactProperty::IdReference(s) => return Cow::Owned(format!("#{}", s)),
}
.into()
}
}
impl FromStr for Context {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), Context,
"billing" => Context::Billing,
"delivery" => Context::Delivery,
"private" => Context::Private,
"work" => Context::Work,
)
.copied()
.ok_or(())
}
}
impl Context {
pub fn as_str(&self) -> &'static str {
match self {
Context::Billing => "billing",
Context::Delivery => "delivery",
Context::Private => "private",
Context::Work => "work",
}
}
}
impl FromStr for Feature {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), Feature,
"fax" => Feature::Fax,
"main-number" => Feature::MainNumber,
"mobile" => Feature::Mobile,
"pager" => Feature::Pager,
"text" => Feature::Text,
"textphone" => Feature::TextPhone,
"video" => Feature::Video,
"voice" => Feature::Voice,
)
.copied()
.ok_or(())
}
}
impl Feature {
pub fn as_str(&self) -> &'static str {
match self {
Feature::Fax => "fax",
Feature::MainNumber => "main-number",
Feature::Mobile => "mobile",
Feature::Pager => "pager",
Feature::Text => "text",
Feature::TextPhone => "textphone",
Feature::Video => "video",
Feature::Voice => "voice",
}
}
}
impl FromStr for JSContactGrammaticalGender {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSContactGrammaticalGender,
"animate" => JSContactGrammaticalGender::Animate,
"common" => JSContactGrammaticalGender::Common,
"feminine" => JSContactGrammaticalGender::Feminine,
"inanimate" => JSContactGrammaticalGender::Inanimate,
"masculine" => JSContactGrammaticalGender::Masculine,
"neuter" => JSContactGrammaticalGender::Neuter,
)
.copied()
.ok_or(())
}
}
impl JSContactGrammaticalGender {
pub fn as_str(&self) -> &'static str {
match self {
JSContactGrammaticalGender::Animate => "animate",
JSContactGrammaticalGender::Common => "common",
JSContactGrammaticalGender::Feminine => "feminine",
JSContactGrammaticalGender::Inanimate => "inanimate",
JSContactGrammaticalGender::Masculine => "masculine",
JSContactGrammaticalGender::Neuter => "neuter",
}
}
}
impl FromStr for JSContactKind {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSContactKind,
"apartment" => JSContactKind::Apartment,
"block" => JSContactKind::Block,
"building" => JSContactKind::Building,
"country" => JSContactKind::Country,
"direction" => JSContactKind::Direction,
"district" => JSContactKind::District,
"floor" => JSContactKind::Floor,
"landmark" => JSContactKind::Landmark,
"locality" => JSContactKind::Locality,
"name" => JSContactKind::Name,
"number" => JSContactKind::Number,
"postcode" => JSContactKind::Postcode,
"postOfficeBox" => JSContactKind::PostOfficeBox,
"region" => JSContactKind::Region,
"room" => JSContactKind::Room,
"separator" => JSContactKind::Separator,
"subdistrict" => JSContactKind::Subdistrict,
"birth" => JSContactKind::Birth,
"death" => JSContactKind::Death,
"wedding" => JSContactKind::Wedding,
"calendar" => JSContactKind::Calendar,
"freeBusy" => JSContactKind::FreeBusy,
"application" => JSContactKind::Application,
"device" => JSContactKind::Device,
"group" => JSContactKind::Group,
"individual" => JSContactKind::Individual,
"location" => JSContactKind::Location,
"org" => JSContactKind::Org,
"directory" => JSContactKind::Directory,
"entry" => JSContactKind::Entry,
"contact" => JSContactKind::Contact,
"logo" => JSContactKind::Logo,
"photo" => JSContactKind::Photo,
"sound" => JSContactKind::Sound,
"credential" => JSContactKind::Credential,
"generation" => JSContactKind::Generation,
"given" => JSContactKind::Given,
"given2" => JSContactKind::Given2,
"surname" => JSContactKind::Surname,
"surname2" => JSContactKind::Surname2,
"title" => JSContactKind::Title,
"expertise" => JSContactKind::Expertise,
"hobby" => JSContactKind::Hobby,
"interest" => JSContactKind::Interest,
"role" => JSContactKind::Role,
)
.copied()
.ok_or(())
}
}
#[allow(clippy::wrong_self_convention)]
impl JSContactKind {
pub fn as_str(&self) -> &'static str {
match self {
JSContactKind::Apartment => "apartment",
JSContactKind::Block => "block",
JSContactKind::Building => "building",
JSContactKind::Country => "country",
JSContactKind::Direction => "direction",
JSContactKind::District => "district",
JSContactKind::Floor => "floor",
JSContactKind::Landmark => "landmark",
JSContactKind::Locality => "locality",
JSContactKind::Name => "name",
JSContactKind::Number => "number",
JSContactKind::Postcode => "postcode",
JSContactKind::PostOfficeBox => "postOfficeBox",
JSContactKind::Region => "region",
JSContactKind::Room => "room",
JSContactKind::Separator => "separator",
JSContactKind::Subdistrict => "subdistrict",
JSContactKind::Birth => "birth",
JSContactKind::Death => "death",
JSContactKind::Wedding => "wedding",
JSContactKind::Calendar => "calendar",
JSContactKind::FreeBusy => "freeBusy",
JSContactKind::Application => "application",
JSContactKind::Device => "device",
JSContactKind::Group => "group",
JSContactKind::Individual => "individual",
JSContactKind::Location => "location",
JSContactKind::Org => "org",
JSContactKind::Directory => "directory",
JSContactKind::Entry => "entry",
JSContactKind::Contact => "contact",
JSContactKind::Logo => "logo",
JSContactKind::Photo => "photo",
JSContactKind::Sound => "sound",
JSContactKind::Credential => "credential",
JSContactKind::Generation => "generation",
JSContactKind::Given => "given",
JSContactKind::Given2 => "given2",
JSContactKind::Surname => "surname",
JSContactKind::Surname2 => "surname2",
JSContactKind::Title => "title",
JSContactKind::Expertise => "expertise",
JSContactKind::Hobby => "hobby",
JSContactKind::Interest => "interest",
JSContactKind::Role => "role",
}
}
pub(crate) fn from_vcard_adr_pos(pos: usize) -> Option<Self> {
match pos {
0 => JSContactKind::PostOfficeBox,
1 => JSContactKind::Apartment,
2 => JSContactKind::Name,
3 => JSContactKind::Locality,
4 => JSContactKind::Region,
5 => JSContactKind::Postcode,
6 => JSContactKind::Country,
7 => JSContactKind::Room,
8 => JSContactKind::Apartment,
9 => JSContactKind::Floor,
10 => JSContactKind::Number,
11 => JSContactKind::Name,
12 => JSContactKind::Building,
13 => JSContactKind::Block,
14 => JSContactKind::Subdistrict,
15 => JSContactKind::District,
16 => JSContactKind::Landmark,
17 => JSContactKind::Direction,
_ => return None,
}
.into()
}
pub(crate) fn to_vcard_adr_pos(&self) -> Option<usize> {
match self {
JSContactKind::PostOfficeBox => 0,
JSContactKind::Apartment => 8,
JSContactKind::Name => 11,
JSContactKind::Locality => 3,
JSContactKind::Region => 4,
JSContactKind::Postcode => 5,
JSContactKind::Country => 6,
JSContactKind::Room => 7,
JSContactKind::Floor => 9,
JSContactKind::Number => 10,
JSContactKind::Building => 12,
JSContactKind::Block => 13,
JSContactKind::Subdistrict => 14,
JSContactKind::District => 15,
JSContactKind::Landmark => 16,
JSContactKind::Direction => 17,
_ => return None,
}
.into()
}
pub(crate) fn from_vcard_n_pos(pos: usize) -> Option<Self> {
match pos {
0 => JSContactKind::Surname,
1 => JSContactKind::Given,
2 => JSContactKind::Given2,
3 => JSContactKind::Title,
4 => JSContactKind::Credential,
5 => JSContactKind::Surname2,
6 => JSContactKind::Generation,
_ => return None,
}
.into()
}
pub(crate) fn to_vcard_n_pos(&self) -> Option<usize> {
match self {
JSContactKind::Surname => 0,
JSContactKind::Given => 1,
JSContactKind::Given2 => 2,
JSContactKind::Title => 3,
JSContactKind::Credential => 4,
JSContactKind::Surname2 => 5,
JSContactKind::Generation => 6,
_ => return None,
}
.into()
}
}
impl FromStr for JSContactLevel {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSContactLevel,
"high" => JSContactLevel::High,
"low" => JSContactLevel::Low,
"medium" => JSContactLevel::Medium,
)
.copied()
.ok_or(())
}
}
impl JSContactLevel {
pub fn as_str(&self) -> &'static str {
match self {
JSContactLevel::High => "high",
JSContactLevel::Low => "low",
JSContactLevel::Medium => "medium",
}
}
}
impl FromStr for JSContactRelation {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSContactRelation,
"acquaintance" => JSContactRelation::Acquaintance,
"agent" => JSContactRelation::Agent,
"child" => JSContactRelation::Child,
"colleague" => JSContactRelation::Colleague,
"contact" => JSContactRelation::Contact,
"co-resident" => JSContactRelation::CoResident,
"co-worker" => JSContactRelation::CoWorker,
"crush" => JSContactRelation::Crush,
"date" => JSContactRelation::Date,
"emergency" => JSContactRelation::Emergency,
"friend" => JSContactRelation::Friend,
"kin" => JSContactRelation::Kin,
"me" => JSContactRelation::Me,
"met" => JSContactRelation::Met,
"muse" => JSContactRelation::Muse,
"neighbor" => JSContactRelation::Neighbor,
"parent" => JSContactRelation::Parent,
"sibling" => JSContactRelation::Sibling,
"spouse" => JSContactRelation::Spouse,
"sweetheart" => JSContactRelation::Sweetheart,
)
.copied()
.ok_or(())
}
}
impl JSContactRelation {
pub fn as_str(&self) -> &'static str {
match self {
JSContactRelation::Acquaintance => "acquaintance",
JSContactRelation::Agent => "agent",
JSContactRelation::Child => "child",
JSContactRelation::Colleague => "colleague",
JSContactRelation::Contact => "contact",
JSContactRelation::CoResident => "co-resident",
JSContactRelation::CoWorker => "co-worker",
JSContactRelation::Crush => "crush",
JSContactRelation::Date => "date",
JSContactRelation::Emergency => "emergency",
JSContactRelation::Friend => "friend",
JSContactRelation::Kin => "kin",
JSContactRelation::Me => "me",
JSContactRelation::Met => "met",
JSContactRelation::Muse => "muse",
JSContactRelation::Neighbor => "neighbor",
JSContactRelation::Parent => "parent",
JSContactRelation::Sibling => "sibling",
JSContactRelation::Spouse => "spouse",
JSContactRelation::Sweetheart => "sweetheart",
}
}
}
impl FromStr for JSContactPhoneticSystem {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
hashify::map!(s.as_bytes(), JSContactPhoneticSystem,
"ipa" => JSContactPhoneticSystem::Ipa,
"jyut" => JSContactPhoneticSystem::Jyut,
"piny" => JSContactPhoneticSystem::Piny,
"script" => JSContactPhoneticSystem::Script,
)
.copied()
.ok_or(())
}
}
impl JSContactPhoneticSystem {
pub fn as_str(&self) -> &'static str {
match self {
JSContactPhoneticSystem::Ipa => "ipa",
JSContactPhoneticSystem::Jyut => "jyut",
JSContactPhoneticSystem::Piny => "piny",
JSContactPhoneticSystem::Script => "script",
}
}
}
+138
View File
@@ -0,0 +1,138 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
#![deny(rust_2018_idioms)]
#![forbid(unsafe_code)]
use common::tokenizer::{Mode, StopChar, Token};
use icalendar::{ICalendar, ICalendarComponentType};
use std::borrow::Cow;
use vcard::VCard;
pub mod common;
pub mod datecalc;
pub mod icalendar;
#[cfg(feature = "jmap")]
pub mod jscalendar;
#[cfg(feature = "jmap")]
pub mod jscontact;
pub mod vcard;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Entry {
VCard(VCard),
ICalendar(ICalendar),
InvalidLine(String),
UnexpectedComponentEnd {
expected: ICalendarComponentType,
found: ICalendarComponentType,
},
UnterminatedComponent(Cow<'static, str>),
TooManyComponents,
Eof,
}
const PRESIZE_BYTES_PER_SLOT: usize = 8;
pub struct Parser<'x> {
pub(crate) input: &'x [u8],
pub(crate) source: &'x str,
pub(crate) pos: usize,
pub(crate) strict: bool,
pub(crate) mode: Mode,
pub(crate) token_buf: Vec<Token<'x>>,
pub(crate) last_token_end: usize,
presize_budget: usize,
}
impl<'x> Parser<'x> {
pub fn new(input: &'x str) -> Self {
Self {
input: input.as_bytes(),
source: input,
pos: 0,
strict: false,
mode: Mode::INITIAL,
token_buf: Vec::new(),
last_token_end: usize::MAX,
presize_budget: input.len() / PRESIZE_BYTES_PER_SLOT,
}
}
pub fn strict(mut self) -> Self {
self.strict = true;
self
}
pub(crate) fn presize<T>(&mut self, items: &mut Vec<T>, hint: usize) {
let slots = hint.min(self.presize_budget);
self.presize_budget -= slots;
items.reserve_exact(slots);
}
pub fn entry(&mut self) -> Entry {
self.expect_iana_token();
loop {
if let Some(token) = self.token() {
if (token.text.eq_ignore_ascii_case(b"BEGIN")
|| token.text.eq_ignore_ascii_case("\u{feff}BEGIN".as_bytes()))
&& token.stop_char == StopChar::Colon
{
if let Some(token) = self.token() {
if token.stop_char == StopChar::Lf {
hashify::fnc_map_ignore_case!(token.text.as_ref(),
b"VCARD" => { return self.vcard(); },
b"VCALENDAR" => { return self.icalendar(ICalendarComponentType::VCalendar); },
b"VEVENT" => { return self.icalendar(ICalendarComponentType::VEvent); },
b"VTODO" => { return self.icalendar(ICalendarComponentType::VTodo); },
b"VJOURNAL" => { return self.icalendar(ICalendarComponentType::VJournal); },
b"VFREEBUSY" => { return self.icalendar(ICalendarComponentType::VFreebusy); },
b"VTIMEZONE" => { return self.icalendar(ICalendarComponentType::VTimezone); },
b"VALARM" => { return self.icalendar(ICalendarComponentType::VAlarm); },
b"STANDARD" => { return self.icalendar(ICalendarComponentType::Standard); },
b"DAYLIGHT" => { return self.icalendar(ICalendarComponentType::Daylight); },
b"VAVAILABILITY" => { return self.icalendar(ICalendarComponentType::VAvailability); },
b"AVAILABLE" => { return self.icalendar(ICalendarComponentType::Available); },
b"PARTICIPANT" => { return self.icalendar(ICalendarComponentType::Participant); },
b"VLOCATION" => { return self.icalendar(ICalendarComponentType::VLocation); },
b"VRESOURCE" => { return self.icalendar(ICalendarComponentType::VResource); },
_ => {
return self.icalendar(ICalendarComponentType::Other(token.into_string()));
}
)
}
} else {
return Entry::Eof;
}
}
let token_start = token.start;
let mut token_end = token.end;
if token.stop_char != StopChar::Lf {
self.expect_single_value();
while let Some(token) = self.token() {
token_end = token.end;
if token.stop_char == StopChar::Lf {
break;
}
}
} else if token.text.is_empty() {
continue;
}
return Entry::InvalidLine(
self.source
.get(token_start..=token_end)
.unwrap_or_default()
.to_string(),
);
} else {
return Entry::Eof;
}
}
}
}
+348
View File
@@ -0,0 +1,348 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::parser::{Boolean, Timestamp},
vcard::*,
};
use smallvec::SmallVec;
impl VCardEntry {
pub fn new(name: VCardProperty) -> Self {
Self {
group: None,
name,
params: vec![],
values: SmallVec::new(),
}
}
pub fn with_params(mut self, params: Vec<VCardParameter>) -> Self {
self.params = params;
self
}
pub fn with_group(mut self, group: Option<String>) -> Self {
self.group = group;
self
}
pub fn with_value(mut self, value: impl Into<VCardValue>) -> Self {
self.values.push(value.into());
self
}
pub fn with_values(mut self, values: impl IntoIterator<Item = VCardValue>) -> Self {
self.values = values.into_iter().collect();
self
}
pub fn with_param(mut self, param: impl Into<VCardParameter>) -> Self {
self.params.push(param.into());
self
}
pub fn add_param(&mut self, param: impl Into<VCardParameter>) {
self.params.push(param.into());
}
pub fn with_param_opt(mut self, param: Option<impl Into<VCardParameter>>) -> Self {
if let Some(param) = param {
self.params.push(param.into());
}
self
}
pub fn is_type(&self, typ: &VCardValueType) -> bool {
self.parameters(&VCardParameterName::Value)
.any(|p| matches!(p, VCardParameterValue::ValueType(v) if v == typ))
}
}
impl VCardParameter {
pub fn new(name: VCardParameterName, value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name,
value: value.into(),
}
}
pub fn language(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Language,
value: value.into(),
}
}
pub fn value(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Value,
value: value.into(),
}
}
pub fn pref(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Pref,
value: value.into(),
}
}
pub fn altid(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Altid,
value: value.into(),
}
}
pub fn pid(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Pid,
value: value.into(),
}
}
pub fn typ(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Type,
value: value.into(),
}
}
pub fn mediatype(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Mediatype,
value: value.into(),
}
}
pub fn calscale(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Calscale,
value: value.into(),
}
}
pub fn sort_as(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::SortAs,
value: value.into(),
}
}
pub fn geo(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Geo,
value: value.into(),
}
}
pub fn tz(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Tz,
value: value.into(),
}
}
pub fn index(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Index,
value: value.into(),
}
}
pub fn level(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Level,
value: value.into(),
}
}
pub fn group(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Group,
value: value.into(),
}
}
pub fn cc(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Cc,
value: value.into(),
}
}
pub fn author(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Author,
value: value.into(),
}
}
pub fn author_name(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::AuthorName,
value: value.into(),
}
}
pub fn created(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Created,
value: value.into(),
}
}
pub fn derived(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Derived,
value: value.into(),
}
}
pub fn label(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Label,
value: value.into(),
}
}
pub fn phonetic(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Phonetic,
value: value.into(),
}
}
pub fn prop_id(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::PropId,
value: value.into(),
}
}
pub fn script(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Script,
value: value.into(),
}
}
pub fn service_type(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::ServiceType,
value: value.into(),
}
}
pub fn username(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Username,
value: value.into(),
}
}
pub fn jsptr(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Jsptr,
value: value.into(),
}
}
pub fn jscomps(value: impl Into<VCardParameterValue>) -> Self {
VCardParameter {
name: VCardParameterName::Jscomps,
value: value.into(),
}
}
}
impl From<String> for VCardValue {
fn from(text: String) -> Self {
VCardValue::Text(text)
}
}
impl From<VCardKind> for VCardValue {
fn from(kind: VCardKind) -> Self {
VCardValue::Kind(kind)
}
}
impl From<String> for VCardParameterValue {
fn from(value: String) -> Self {
VCardParameterValue::Text(value)
}
}
impl From<u32> for VCardParameterValue {
fn from(value: u32) -> Self {
VCardParameterValue::Integer(value)
}
}
impl From<VCardType> for VCardParameterValue {
fn from(value: VCardType) -> Self {
VCardParameterValue::Type(value)
}
}
impl From<VCardValueType> for VCardParameterValue {
fn from(value: VCardValueType) -> Self {
VCardParameterValue::ValueType(value)
}
}
impl From<CalendarScale> for VCardParameterValue {
fn from(value: CalendarScale) -> Self {
VCardParameterValue::Calscale(value)
}
}
impl From<VCardPhonetic> for VCardParameterValue {
fn from(value: VCardPhonetic) -> Self {
VCardParameterValue::Phonetic(value)
}
}
impl From<VCardLevel> for VCardParameterValue {
fn from(value: VCardLevel) -> Self {
VCardParameterValue::Level(value)
}
}
impl From<bool> for VCardParameterValue {
fn from(value: bool) -> Self {
VCardParameterValue::Bool(value)
}
}
impl From<Vec<Jscomp>> for VCardParameterValue {
fn from(value: Vec<Jscomp>) -> Self {
VCardParameterValue::Jscomps(value)
}
}
impl From<Timestamp> for VCardParameterValue {
fn from(value: Timestamp) -> Self {
VCardParameterValue::Timestamp(value.0)
}
}
impl From<Boolean> for VCardParameterValue {
fn from(value: Boolean) -> Self {
VCardParameterValue::Bool(value.0)
}
}
impl<T: Into<VCardParameterValue>> From<IanaType<T, String>> for VCardParameterValue {
fn from(value: IanaType<T, String>) -> Self {
match value {
IanaType::Iana(v) => v.into(),
IanaType::Other(s) => s.into(),
}
}
}
+327
View File
@@ -0,0 +1,327 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
pub(crate) fn media_type_from_legacy(property: &str, token: &str) -> Option<Cow<'static, str>> {
if token.contains('/') {
Some(Cow::Owned(token.to_ascii_lowercase()))
} else if let Some(media_type) = legacy_alias(property, token.as_bytes()) {
Some(Cow::Borrowed(media_type))
} else {
let subtype = token.to_ascii_lowercase();
match property {
"PHOTO" | "LOGO" if is_image_subtype(subtype.as_bytes()) => {
Some(Cow::Owned(format!("image/{subtype}")))
}
"SOUND" if is_audio_subtype(subtype.as_bytes()) => {
Some(Cow::Owned(format!("audio/{subtype}")))
}
_ => None,
}
}
}
pub(crate) fn legacy_media_type(property: &str, media_type: &str) -> Option<String> {
let token = match media_type {
"application/pgp-keys" => "PGP".to_string(),
"application/x-x509-user-cert" => "X509".to_string(),
_ => media_type.split_once('/')?.1.to_ascii_uppercase(),
};
(media_type_from_legacy(property, &token).as_deref() == Some(media_type)).then_some(token)
}
fn legacy_alias(property: &str, token: &[u8]) -> Option<&'static str> {
match property {
"PHOTO" | "LOGO" => hashify::map_ignore_case!(token, &'static str,
b"JPG" => "image/jpeg",
b"TIF" => "image/tiff",
)
.copied(),
"SOUND" => hashify::map_ignore_case!(token, &'static str,
b"AIF" => "audio/aiff",
b"AIFF" => "audio/aiff",
b"MP3" => "audio/mpeg",
b"PCM" => "audio/l16",
b"WAV" => "audio/wav",
b"WAVE" => "audio/wav",
)
.copied(),
"KEY" => hashify::map_ignore_case!(token, &'static str,
b"GPG" => "application/pgp-keys",
b"PGP" => "application/pgp-keys",
b"X509" => "application/x-x509-user-cert",
)
.copied(),
_ => None,
}
}
fn is_image_subtype(subtype: &[u8]) -> bool {
hashify::set!(
subtype,
"aces",
"apng",
"avci",
"avcs",
"avif",
"bmp",
"cgm",
"dicom-rle",
"dpx",
"emf",
"example",
"fits",
"g3fax",
"gif",
"heic",
"heic-sequence",
"heif",
"heif-sequence",
"hej2k",
"hsj2",
"ief",
"j2c",
"jaii",
"jais",
"jls",
"jp2",
"jpeg",
"jph",
"jphc",
"jpm",
"jpx",
"jxl",
"jxr",
"jxra",
"jxrs",
"jxs",
"jxsc",
"jxsi",
"jxss",
"ktx",
"ktx2",
"naplps",
"png",
"prs.aimg",
"prs.btif",
"prs.pti",
"pwg-raster",
"svg+xml",
"t38",
"tiff",
"tiff-fx",
"vnd.adobe.photoshop",
"vnd.airzip.accelerator.azv",
"vnd.blockfact.facti",
"vnd.clip",
"vnd.cns.inf2",
"vnd.dece.graphic",
"vnd.djvu",
"vnd.dvb.subtitle",
"vnd.dwg",
"vnd.dxf",
"vnd.fastbidsheet",
"vnd.fpx",
"vnd.fst",
"vnd.fujixerox.edmics-mmr",
"vnd.fujixerox.edmics-rlc",
"vnd.globalgraphics.pgb",
"vnd.microsoft.icon",
"vnd.mix",
"vnd.mozilla.apng",
"vnd.ms-modi",
"vnd.net-fpx",
"vnd.pco.b16",
"vnd.radiance",
"vnd.sealed.png",
"vnd.sealedmedia.softseal.gif",
"vnd.sealedmedia.softseal.jpg",
"vnd.sld",
"vnd.svf",
"vnd.tencent.tap",
"vnd.valve.source.texture",
"vnd.wap.wbmp",
"vnd.xiff",
"vnd.zbrush.pcx",
"webp",
"wmf",
"x-emf",
"x-wmf",
)
}
fn is_audio_subtype(subtype: &[u8]) -> bool {
hashify::set!(
subtype,
"1d-interleaved-parityfec",
"32kadpcm",
"3gpp",
"3gpp2",
"amr",
"amr-wb",
"atrac-advanced-lossless",
"atrac-x",
"atrac3",
"bv16",
"bv32",
"cn",
"dat12",
"dv",
"dvi4",
"evrc",
"evrc-qcp",
"evrc0",
"evrc1",
"evrcb",
"evrcb0",
"evrcb1",
"evrcnw",
"evrcnw0",
"evrcnw1",
"evrcwb",
"evrcwb0",
"evrcwb1",
"evs",
"g711-0",
"g719",
"g722",
"g7221",
"g723",
"g726-16",
"g726-24",
"g726-32",
"g726-40",
"g728",
"g729",
"g7291",
"g729d",
"g729e",
"gsm",
"gsm-efr",
"gsm-hr-08",
"l16",
"l20",
"l24",
"l8",
"lpc",
"melp",
"melp1200",
"melp2400",
"melp600",
"mp4a-latm",
"mpa",
"pcma",
"pcma-wb",
"pcmu",
"pcmu-wb",
"qcelp",
"red",
"smv",
"smv-qcp",
"smv0",
"tetra_acelp",
"tetra_acelp_bb",
"tsvcis",
"uemclip",
"vdvi",
"vmr-wb",
"aac",
"ac3",
"amr-wb+",
"aptx",
"asc",
"basic",
"clearmode",
"dls",
"dsr-es201108",
"dsr-es202050",
"dsr-es202211",
"dsr-es202212",
"eac3",
"encaprtp",
"example",
"flac",
"flexfec",
"fwdred",
"ilbc",
"ip-mr_v2.5",
"matroska",
"mhas",
"midi-clip",
"mobile-xmf",
"mp4",
"mpa-robust",
"mpeg",
"mpeg4-generic",
"ogg",
"opus",
"parityfec",
"prs.aaud",
"prs.sid",
"raptorfec",
"rtp-enc-aescm128",
"rtp-midi",
"rtploopback",
"rtx",
"scip",
"sofa",
"soundfont",
"sp-midi",
"speex",
"t140c",
"t38",
"telephone-event",
"tone",
"ulpfec",
"usac",
"vnd.3gpp.iufp",
"vnd.4sb",
"vnd.celp",
"vnd.audiokoz",
"vnd.blockfact.facta",
"vnd.cisco.nse",
"vnd.cmles.radio-events",
"vnd.cns.anp1",
"vnd.cns.inf1",
"vnd.dece.audio",
"vnd.digital-winds",
"vnd.dlna.adts",
"vnd.dolby.heaac.1",
"vnd.dolby.heaac.2",
"vnd.dolby.mlp",
"vnd.dolby.mps",
"vnd.dolby.pl2",
"vnd.dolby.pl2x",
"vnd.dolby.pl2z",
"vnd.dolby.pulse.1",
"vnd.dra",
"vnd.dts",
"vnd.dts.hd",
"vnd.dts.uhd",
"vnd.dvb.file",
"vnd.everad.plj",
"vnd.hns.audio",
"vnd.lucent.voice",
"vnd.ms-playready.media.pya",
"vnd.nokia.mobile-xmf",
"vnd.nortel.vbk",
"vnd.nuera.ecelp4800",
"vnd.nuera.ecelp7470",
"vnd.nuera.ecelp9600",
"vnd.octel.sbc",
"vnd.presonus.multitrack",
"vnd.qcelp",
"vnd.rhetorex.32kadpcm",
"vnd.rip",
"vnd.sealedmedia.softseal.mpeg",
"vnd.vmx.cvsd",
"vorbis",
"vorbis-config",
)
}
+476
View File
@@ -0,0 +1,476 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
#[cfg(feature = "rkyv")]
use crate::common::archive::{InlineVec, Unboxed};
use crate::{
Entry, Parser,
common::{CalendarScale, Data, IanaParse, IanaString, IanaType, PartialDateTime},
};
use smallvec::SmallVec;
pub mod builder;
pub(crate) mod media_type;
pub mod parser;
pub mod types;
pub mod utils;
pub mod writer;
#[cfg(feature = "rkyv")]
pub mod rkyv_types;
#[cfg(feature = "rkyv")]
pub mod rkyv_utils;
#[cfg(feature = "rkyv")]
pub mod rkyv_writer;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))]
pub struct VCard {
pub entries: Vec<VCardEntry>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum VCardVersion {
V2_0 = 20,
V2_1 = 21,
V3_0 = 30,
#[default]
V4_0 = 40,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))]
pub struct VCardEntry {
pub group: Option<String>,
pub name: VCardProperty,
pub params: Vec<VCardParameter>,
#[cfg_attr(feature = "rkyv", rkyv(with = InlineVec))]
pub values: SmallVec<[VCardValue; 1]>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum VCardProperty {
Other(String),
Begin,
End,
Source, // [RFC6350, Section 6.1.3]
Kind, // [RFC6350, Section 6.1.4]
Xml, // [RFC6350, Section 6.1.5]
Fn, // [RFC6350, Section 6.2.1]
N, // [RFC6350, Section 6.2.2][RFC9554, Section 2.2]
Nickname, // [RFC6350, Section 6.2.3]
Photo, // [RFC6350, Section 6.2.4]
Bday, // [RFC6350, Section 6.2.5]
Anniversary, // [RFC6350, Section 6.2.6]
Gender, // [RFC6350, Section 6.2.7]
Adr, // [RFC6350, Section 6.3.1][RFC9554, Section 2.1]
Tel, // [RFC6350, Section 6.4.1]
Email, // [RFC6350, Section 6.4.2]
Impp, // [RFC6350, Section 6.4.3]
Lang, // [RFC6350, Section 6.4.4]
Tz, // [RFC6350, Section 6.5.1]
Geo, // [RFC6350, Section 6.5.2]
Title, // [RFC6350, Section 6.6.1]
Role, // [RFC6350, Section 6.6.2]
Logo, // [RFC6350, Section 6.6.3]
Org, // [RFC6350, Section 6.6.4]
Member, // [RFC6350, Section 6.6.5]
Related, // [RFC6350, Section 6.6.6]
Categories, // [RFC6350, Section 6.7.1]
Note, // [RFC6350, Section 6.7.2]
Prodid, // [RFC6350, Section 6.7.3]
Rev, // [RFC6350, Section 6.7.4]
Sound, // [RFC6350, Section 6.7.5]
Uid, // [RFC6350, Section 6.7.6]
Clientpidmap, // [RFC6350, Section 6.7.7]
Url, // [RFC6350, Section 6.7.8]
Version, // [RFC6350, Section 6.7.9]
Key, // [RFC6350, Section 6.8.1]
Fburl, // [RFC6350, Section 6.9.1]
Caladruri, // [RFC6350, Section 6.9.2]
Caluri, // [RFC6350, Section 6.9.3]
Birthplace, // [RFC6474, Section 2.1]
Deathplace, // [RFC6474, Section 2.2]
Deathdate, // [RFC6474, Section 2.3]
Expertise, // [RFC6715, Section 2.1]
Hobby, // [RFC6715, Section 2.2]
Interest, // [RFC6715, Section 2.3]
OrgDirectory, // [RFC6715, Section 2.4][RFC Errata 3341]
ContactUri, // [RFC8605, Section 2.1]
Created, // [RFC9554, Section 3.1]
Gramgender, // [RFC9554, Section 3.2]
Language, // [RFC9554, Section 3.3]
Pronouns, // [RFC9554, Section 3.4]
Socialprofile, // [RFC9554, Section 3.5]
Jsprop, // [RFC9555, Section 3.2.1]
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))]
pub enum VCardValue {
Text(String),
Integer(i64),
Float(f64),
Boolean(bool),
PartialDateTime(PartialDateTime),
Binary(#[cfg_attr(feature = "rkyv", rkyv(with = Unboxed))] Box<Data>),
Sex(VCardSex),
GramGender(VCardGramGender),
Kind(VCardKind),
Component(Vec<String>),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))]
pub struct VCardParameter {
pub name: VCardParameterName,
pub value: VCardParameterValue,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq)))]
pub enum VCardParameterValue {
Text(String),
Integer(u32),
Timestamp(i64),
Bool(bool),
ValueType(VCardValueType),
Type(VCardType),
Calscale(CalendarScale),
Level(VCardLevel),
Phonetic(VCardPhonetic),
Jscomps(Vec<Jscomp>),
Null,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum Jscomp {
Entry { position: u32, value: u32 },
Separator(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(PartialEq)))]
pub enum VCardParameterName {
Other(String),
Language, // [RFC6350, Section 5.1]
Value, // [RFC6350, Section 5.2]
Pref, // [RFC6350, Section 5.3]
Altid, // [RFC6350, Section 5.4]
Pid, // [RFC6350, Section 5.5]
Type, // [RFC6350, Section 5.6]
Mediatype, // [RFC6350, Section 5.7]
Calscale, // [RFC6350, Section 5.8]
SortAs, // [RFC6350, Section 5.9]
Geo, // [RFC6350, Section 5.10]
Tz, // [RFC6350, Section 5.11]
Index, // [RFC6715, Section 3.1]
Level, // [RFC6715, Section 3.2]
Group, // [RFC7095, Section 8.1]
Cc, // [RFC8605, Section 3.1]
Author, // [RFC9554, Section 4.1]
AuthorName, // [RFC9554, Section 4.2]
Created, // [RFC9554, Section 4.3]
Derived, // [RFC9554, Section 4.4]
Label, // [RFC6350, Section 6.3.1][RFC9554, Section 4.5]
Phonetic, // [RFC9554, Section 4.6]
PropId, // [RFC9554, Section 4.7]
Script, // [RFC9554, Section 4.8]
ServiceType, // [RFC9554, Section 4.9]
Username, // [RFC9554, Section 4.10]
Jsptr, // [RFC9555, Section 3.3.2]
Jscomps, // [RFC9555, Section 3.3.2]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum VCardValueType {
Boolean, // [RFC6350, Section 4.4]
Date, // [RFC6350, Section 4.3.1]
DateAndOrTime, // [RFC6350, Section 4.3.4]
DateTime, // [RFC6350, Section 4.3.3]
Float, // [RFC6350, Section 4.6]
Integer, // [RFC6350, Section 4.5]
LanguageTag, // [RFC6350, Section 4.8]
Text, // [RFC6350, Section 4.1]
Time, // [RFC6350, Section 4.3.2]
Timestamp, // [RFC6350, Section 4.3.5]
Uri, // [RFC6350, Section 4.2]
UtcOffset, // [RFC6350, Section 4.7]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum VCardLevel {
Beginner, // [RFC6715, Section 3.2]
Average, // [RFC6715, Section 3.2]
Expert, // [RFC6715, Section 3.2]
High, // [RFC6715, Section 3.2]
Medium, // [RFC6715, Section 3.2]
Low, // [RFC6715, Section 3.2]
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum VCardPhonetic {
Ipa, // [RFC9554, Section 4.6]
Jyut, // [RFC9554, Section 4.6]
Piny, // [RFC9554, Section 4.6]
Script, // [RFC9554, Section 4.6]
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
serde(tag = "type", content = "data")
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum VCardType {
Work, // [RFC6350, Section 5.6]
Home, // [RFC6350, Section 5.6]
Billing, // [RFC9554, Section 5.1]
Delivery, // [RFC9554, Section 5.2]
Contact, // [RFC6350, Section 6.6.6]
Acquaintance, // [RFC6350, Section 6.6.6]
Friend, // [RFC6350, Section 6.6.6]
Met, // [RFC6350, Section 6.6.6]
CoWorker, // [RFC6350, Section 6.6.6]
Colleague, // [RFC6350, Section 6.6.6]
CoResident, // [RFC6350, Section 6.6.6]
Neighbor, // [RFC6350, Section 6.6.6]
Child, // [RFC6350, Section 6.6.6]
Parent, // [RFC6350, Section 6.6.6]
Sibling, // [RFC6350, Section 6.6.6]
Spouse, // [RFC6350, Section 6.6.6]
Kin, // [RFC6350, Section 6.6.6]
Muse, // [RFC6350, Section 6.6.6]
Crush, // [RFC6350, Section 6.6.6]
Date, // [RFC6350, Section 6.6.6]
Sweetheart, // [RFC6350, Section 6.6.6]
Me, // [RFC6350, Section 6.6.6]
Agent, // [RFC6350, Section 6.6.6]
Emergency, // [RFC6350, Section 6.6.6]
Text, // [RFC6350, Section 6.4.1]
Voice, // [RFC6350, Section 6.4.1]
Fax, // [RFC6350, Section 6.4.1]
Cell, // [RFC6350, Section 6.4.1]
Video, // [RFC6350, Section 6.4.1]
Pager, // [RFC6350, Section 6.4.1]
Textphone, // [RFC6350, Section 6.4.1]
MainNumber, // [RFC7852]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum VCardGramGender {
Animate, // [RFC9554, Section 3.2]
Common, // [RFC9554, Section 3.2]
Feminine, // [RFC9554, Section 3.2]
Inanimate, // [RFC9554, Section 3.2]
Masculine, // [RFC9554, Section 3.2]
Neuter, // [RFC9554, Section 3.2]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum VCardSex {
Male,
Female,
Other,
NoneOrNotApplicable,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
any(all(test, inbuxa_tests), feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
#[cfg_attr(feature = "rkyv", rkyv(compare(PartialEq), derive(Debug)))]
pub enum VCardKind {
Individual, // [RFC6350, Section 6.1.4]
Group, // [RFC6350, Section 6.1.4]
Org, // [RFC6350, Section 6.1.4]
Location, // [RFC6350, Section 6.1.4]
Application, // [RFC6473, Section 3]
Device, // [RFC6869, Section 3]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ValueSeparator {
None,
Comma,
Semicolon,
SemicolonAndComma,
Skip,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ValueType {
Vcard(VCardValueType),
Kind,
Sex,
GramGender,
}
impl ValueType {
pub fn unwrap_vcard(self) -> VCardValueType {
match self {
ValueType::Vcard(v) => v,
_ => VCardValueType::Text,
}
}
}
impl VCard {
pub fn parse(value: impl AsRef<str>) -> Result<Self, Entry> {
let mut parser = Parser::new(value.as_ref());
match parser.entry() {
Entry::VCard(vcard) => Ok(vcard),
other => Err(other),
}
}
}
+901
View File
@@ -0,0 +1,901 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{
PartialDateTime, VCard, VCardEntry, VCardParameter, VCardParameterName, VCardType, VCardValue,
VCardValueType, ValueSeparator, ValueType,
};
use crate::{
Entry, Parser, Token,
common::{
CalendarScale, Data, Encoding, IanaParse, IanaType,
decode::{Decoded, ValueDecoder},
parser::{Boolean, Integer, Timestamp, parse_digits, parse_small_digits},
tokenizer::{Mode, StopChar},
},
vcard::{
Jscomp, VCardGramGender, VCardKind, VCardLevel, VCardParameterValue, VCardPhonetic,
VCardProperty, VCardSex,
},
};
use smallvec::{SmallVec, smallvec};
use std::{iter::Peekable, mem::take, slice::Iter};
struct Params {
params: Vec<VCardParameter>,
stop_char: StopChar,
data_types: Vec<IanaType<VCardValueType, String>>,
charset: Option<String>,
encoding: Option<Encoding>,
group_name: Option<String>,
}
impl VCardProperty {
pub(crate) fn value_decoder<'x>(
&self,
encoding: Encoding,
charset: Option<&'x str>,
) -> ValueDecoder<'x> {
let binary = matches!(encoding, Encoding::Base64)
&& match self {
VCardProperty::Photo
| VCardProperty::Logo
| VCardProperty::Sound
| VCardProperty::Key => true,
VCardProperty::Other(_) => charset.is_none(),
_ => false,
};
ValueDecoder {
encoding,
charset,
binary,
}
}
}
struct ValueText<'x> {
token: Token<'x>,
decoded: Option<String>,
}
impl<'x> ValueText<'x> {
#[inline(always)]
fn bytes(&self) -> &[u8] {
match &self.decoded {
Some(decoded) => decoded.as_bytes(),
None => self.token.text.as_ref(),
}
}
#[inline(always)]
fn into_string(self) -> String {
match self.decoded {
Some(decoded) => decoded,
None => self.token.into_string(),
}
}
#[inline(always)]
fn into_token(self) -> Token<'x> {
let mut token = self.token;
if let Some(decoded) = self.decoded {
token.text = decoded.into();
}
token
}
}
const BYTES_PER_ENTRY: usize = 32;
const MIN_ENTRIES: usize = 4;
const MAX_ENTRIES: usize = 32;
const MULTI_VALUES: usize = 4;
const STRUCTURED_VALUES: usize = 8;
impl Parser<'_> {
pub fn vcard(&mut self) -> Entry {
let mut vcard = VCard::default();
let entries_hint = self.entries_hint();
let mut is_v4 = true;
let mut is_valid = false;
'outer: loop {
self.expect_iana_token();
self.mode.set(Mode::STOP_DOT, true);
let Some(mut token) = self.token() else {
break;
};
self.mode.set(Mode::STOP_DOT, false);
let mut params = Params {
params: Vec::new(),
stop_char: token.stop_char,
data_types: Vec::new(),
group_name: None,
encoding: None,
charset: None,
};
if matches!(token.stop_char, StopChar::Dot) {
params.group_name = token.into_string().into();
token = match self.token() {
Some(token) => token,
None => break,
};
params.stop_char = token.stop_char;
}
match params.stop_char {
StopChar::Semicolon => {
self.vcard_parameters(&mut params);
}
StopChar::Lf => {
if token.text.is_empty() || !self.strict {
continue;
} else {
return Entry::InvalidLine(token.into_string());
}
}
_ => {}
}
if !matches!(params.stop_char, StopChar::Colon | StopChar::Lf) {
params.stop_char = self.seek_value_or_eol();
}
let name = match VCardProperty::parse(token.text.as_ref()) {
Some(name) => name,
None if !token.text.is_empty() => VCardProperty::Other(token.into_string()),
None => {
if params.stop_char != StopChar::Lf {
self.seek_lf();
}
continue;
}
};
let mut entry = VCardEntry {
group: params.group_name,
name,
params: params.params,
values: SmallVec::new(),
};
if params.stop_char != StopChar::Lf {
let (default_type, multi_value) = entry.name.default_types();
let (is_structured, value_capacity) = match multi_value {
ValueSeparator::None => {
self.expect_single_value();
(false, 1)
}
ValueSeparator::Comma => {
self.expect_multi_value_comma();
(false, MULTI_VALUES)
}
ValueSeparator::Semicolon => {
self.expect_multi_value_semicolon();
(false, MULTI_VALUES)
}
ValueSeparator::SemicolonAndComma => {
self.expect_multi_value_semicolon_and_comma();
(true, STRUCTURED_VALUES)
}
ValueSeparator::Skip => {
is_valid = entry.name == VCardProperty::End;
self.expect_single_value();
self.token();
break 'outer;
}
};
entry.values.reserve_exact(value_capacity);
let charset = params.charset.as_deref();
let decoder = match params.encoding {
Some(Encoding::Base64) => {
if multi_value != ValueSeparator::None {
self.expect_single_value();
}
self.mode.set(Mode::UNFOLD_B64, true);
Some(entry.name.value_decoder(Encoding::Base64, charset))
}
Some(Encoding::QuotedPrintable) => {
self.mode.set(Mode::UNFOLD_QP, true);
Some(entry.name.value_decoder(Encoding::QuotedPrintable, charset))
}
None => None,
};
let mut data_types = params.data_types.iter();
let mut token_idx = 0;
let mut last_is_comma = false;
let mut payload = None;
while let Some(token) = self.value_token(params.encoding, &mut payload) {
let (is_eol, is_comma) = match token.stop_char {
StopChar::Lf => (true, false),
StopChar::Comma => (false, true),
_ => (false, false),
};
let mut text = ValueText {
token,
decoded: None,
};
if let Some(decoder) = &decoder {
match decoder.decode(
text.token.text.as_ref(),
text.token.stop_char,
payload.take(),
) {
Decoded::Binary(data) => {
entry.values.push(VCardValue::Binary(Box::new(Data {
data,
content_type: None,
})));
if is_eol {
break;
} else {
continue;
}
}
Decoded::Text(decoded) => text.decoded = Some(decoded),
Decoded::Undecodable => {}
}
}
let default_type = match &default_type {
ValueType::Vcard(default_type) => default_type,
ValueType::Kind if token_idx == 0 => {
if let Some(value) = VCardKind::parse(text.bytes()) {
entry.values.push(VCardValue::Kind(value));
if is_eol {
break;
} else {
continue;
}
}
&VCardValueType::Text
}
ValueType::Sex if token_idx == 0 => {
if let Some(value) = VCardSex::parse(text.bytes()) {
entry.values.push(VCardValue::Sex(value));
if is_eol {
break;
} else {
continue;
}
}
&VCardValueType::Text
}
ValueType::GramGender if token_idx == 0 => {
if let Some(value) = VCardGramGender::parse(text.bytes()) {
entry.values.push(VCardValue::GramGender(value));
if is_eol {
break;
} else {
continue;
}
}
&VCardValueType::Text
}
_ => &VCardValueType::Text,
};
let value = match data_types.next().unwrap_or(&IanaType::Iana(*default_type)) {
IanaType::Iana(VCardValueType::Text) => {
if is_v4
&& entry.name == VCardProperty::Version
&& matches!(text.bytes().first(), Some(b'1'..=b'3'))
{
is_v4 = false;
}
VCardValue::Text(text.into_string())
}
IanaType::Iana(VCardValueType::LanguageTag) | IanaType::Other(_) => {
VCardValue::Text(text.into_string())
}
IanaType::Iana(value_type) => {
text.into_token().into_vcard_value(*value_type, is_v4)
}
};
if is_structured {
match (last_is_comma, entry.values.last_mut(), value) {
(true, Some(last), VCardValue::Text(item)) => {
if let Some(item) = last.extend_component(item) {
entry.values.push(VCardValue::Text(item));
}
}
(_, _, value) => {
entry.values.push(value);
}
}
last_is_comma = is_comma;
} else {
entry.values.push(value);
}
if is_eol {
break;
}
token_idx += 1;
}
} else {
entry.values = smallvec![VCardValue::Text(String::new())];
}
if !params.data_types.is_empty() {
entry
.params
.extend(params.data_types.into_iter().map(|dt| VCardParameter {
name: VCardParameterName::Value,
value: match dt {
IanaType::Iana(v) => VCardParameterValue::ValueType(v),
IanaType::Other(v) => VCardParameterValue::Text(v),
},
}));
}
if !is_v4 {
entry.normalize_legacy_media_type();
}
if vcard.entries.capacity() == 0 {
self.presize(&mut vcard.entries, entries_hint);
}
vcard.entries.push(entry);
}
if is_valid || !self.strict {
Entry::VCard(vcard)
} else {
Entry::UnterminatedComponent("BEGIN".into())
}
}
fn entries_hint(&self) -> usize {
(self.input.len().saturating_sub(self.pos) / BYTES_PER_ENTRY)
.clamp(MIN_ENTRIES, MAX_ENTRIES)
}
fn vcard_parameters(&mut self, params: &mut Params) {
while params.stop_char == StopChar::Semicolon {
self.expect_iana_token();
let Some(name_token) = self.token() else {
params.stop_char = StopChar::Lf;
break;
};
params.stop_char = name_token.stop_char;
if !matches!(
params.stop_char,
StopChar::Lf | StopChar::Colon | StopChar::Semicolon
) {
if params.stop_char != StopChar::Equal {
params.stop_char = self.seek_param_value_or_eol();
}
if params.stop_char == StopChar::Equal {
self.expect_param_value();
while !matches!(
params.stop_char,
StopChar::Lf | StopChar::Colon | StopChar::Semicolon
) {
match self.token() {
Some(token) => {
params.stop_char = token.stop_char;
self.token_buf.push(token);
}
None => {
params.stop_char = StopChar::Lf;
break;
}
}
}
}
}
let param_name = name_token.text.as_ref();
let param_values = &mut params.params;
if let Some(param_name) = VCardParameterName::try_parse(param_name) {
if self.token_buf.is_empty() {
param_values.push(VCardParameter::new(param_name, VCardParameterValue::Null));
continue;
}
match param_name {
VCardParameterName::Value => {
params
.data_types
.extend(self.token_buf.drain(..).map(Into::into));
}
VCardParameterName::Pref | VCardParameterName::Index => {
if let Some(value) = self.buf_parse_one::<IanaType<Integer, String>>() {
param_values.push(VCardParameter {
name: param_name,
value: match value {
IanaType::Iana(value) => {
VCardParameterValue::Integer(value.0 as u32)
}
IanaType::Other(value) => VCardParameterValue::Text(value),
},
});
}
}
VCardParameterName::Calscale => {
if let Some(value) = self.buf_parse_one::<IanaType<CalendarScale, String>>()
{
param_values.push(VCardParameter::new(param_name, value));
}
}
VCardParameterName::Phonetic => {
if let Some(value) = self.buf_parse_one::<IanaType<VCardPhonetic, String>>()
{
param_values.push(VCardParameter::new(param_name, value));
}
}
VCardParameterName::Level => {
if let Some(value) = self.buf_parse_one::<IanaType<VCardLevel, String>>() {
param_values.push(VCardParameter::new(param_name, value));
}
}
VCardParameterName::Created => {
if let Some(value) = self.buf_parse_one::<IanaType<Timestamp, String>>() {
param_values.push(VCardParameter::new(param_name, value));
}
}
VCardParameterName::Derived => {
if let Some(value) = self.buf_parse_one::<IanaType<Boolean, String>>() {
param_values.push(VCardParameter::new(param_name, value));
}
}
VCardParameterName::Type => {
for token in self.token_buf.drain(..) {
token.into_vcard_types(param_values);
}
}
VCardParameterName::Jscomps => {
if let Some(text) = self.raw_token() {
param_values.push(VCardParameter::jscomps(
VCardParameterValue::Jscomps(Jscomp::parse_list(&text)),
));
}
self.token_buf.clear();
}
_ => {
param_values.extend(self.token_buf.drain(..).map(|token| {
VCardParameter::new(
param_name.clone(),
VCardParameterValue::Text(token.into_string()),
)
}));
}
}
} else if !param_name.is_empty() {
match VCardType::parse(param_name) {
Some(typ) if self.token_buf.is_empty() => {
param_values.push(VCardParameter::typ(VCardParameterValue::Type(typ)));
}
_ => match param_name.first() {
Some(b'c' | b'C') if param_name.eq_ignore_ascii_case(b"charset") => {
if let Some(token) = self.token_buf.drain(..).next_back() {
params.charset = token.into_string().into();
}
}
Some(b'e' | b'E') if param_name.eq_ignore_ascii_case(b"encoding") => {
if let Some(token) = self.token_buf.drain(..).next_back() {
params.encoding = Encoding::parse(token.text.as_ref());
}
}
_ => {
if params.encoding.is_none()
&& param_name.eq_ignore_ascii_case(b"base64")
{
params.encoding = Some(Encoding::Base64);
} else {
let name = VCardParameterName::Other(name_token.into_string());
let mut tokens = self.token_buf.drain(..);
match tokens.next_back() {
Some(last) => {
param_values.extend(tokens.map(|token| {
VCardParameter::new(
name.clone(),
VCardParameterValue::Text(token.into_string()),
)
}));
param_values.push(VCardParameter::new(
name,
VCardParameterValue::Text(last.into_string()),
));
}
None => param_values
.push(VCardParameter::new(name, VCardParameterValue::Null)),
}
}
}
},
}
}
}
}
}
impl VCardValue {
fn extend_component(&mut self, item: String) -> Option<String> {
match self {
VCardValue::Component(items) => items.push(item),
VCardValue::Text(text) => *self = VCardValue::Component(vec![take(text), item]),
_ => return Some(item),
}
None
}
}
impl VCardParameter {
fn type_list(text: &str) -> impl Iterator<Item = VCardParameter> + '_ {
text.split(',')
.map(str::trim)
.filter(|text| !text.is_empty())
.map(|text| {
VCardParameter::typ(match VCardType::parse(text.as_bytes()) {
Some(typ) => VCardParameterValue::Type(typ),
None => VCardParameterValue::Text(text.to_string()),
})
})
}
}
impl Jscomp {
fn parse_list(text: &str) -> Vec<Jscomp> {
let mut jscomps = Vec::with_capacity(4);
for (item_pos, item) in text.split(';').enumerate() {
if let Some(item) = item.strip_prefix("s,") {
let mut sep = String::with_capacity(item.len());
let mut last_is_escape = false;
for ch in item.chars() {
if ch == '\\' && !last_is_escape {
last_is_escape = true;
continue;
}
last_is_escape = false;
sep.push(ch);
}
jscomps.push(Jscomp::Separator(sep));
} else if item_pos != 0 {
let mut position = None;
let mut value = None;
for (pos, item) in item.split(',').enumerate() {
if pos == 0 {
position = item.parse::<u32>().ok();
} else if pos == 1 {
value = item.parse::<u32>().ok();
}
}
if let Some(position) = position {
jscomps.push(Jscomp::Entry {
position,
value: value.unwrap_or_default(),
});
}
} else {
jscomps.push(Jscomp::Separator(item.to_string()));
}
}
jscomps
}
}
impl Token<'_> {
fn into_vcard_types(self, param_values: &mut Vec<VCardParameter>) {
if let Some(typ) = VCardType::parse(self.text.as_ref()) {
param_values.push(VCardParameter::typ(VCardParameterValue::Type(typ)));
} else if !self.text.contains(&b',') {
param_values.push(VCardParameter::typ(VCardParameterValue::Text(
self.into_string(),
)));
} else {
param_values.extend(VCardParameter::type_list(self.text.as_str()));
}
}
fn into_vcard_value(self, value_type: VCardValueType, is_v4: bool) -> VCardValue {
match value_type {
VCardValueType::Date if is_v4 => self
.into_vcard_date()
.map(VCardValue::PartialDateTime)
.unwrap_or_else(VCardValue::Text),
VCardValueType::DateAndOrTime if is_v4 => self
.into_vcard_date_and_or_datetime()
.map(VCardValue::PartialDateTime)
.unwrap_or_else(VCardValue::Text),
VCardValueType::DateTime if is_v4 => self
.into_vcard_date_time()
.map(VCardValue::PartialDateTime)
.unwrap_or_else(VCardValue::Text),
VCardValueType::Time if is_v4 => self
.into_vcard_time()
.map(VCardValue::PartialDateTime)
.unwrap_or_else(VCardValue::Text),
VCardValueType::Timestamp if is_v4 => self
.into_timestamp(true)
.map(VCardValue::PartialDateTime)
.unwrap_or_else(VCardValue::Text),
VCardValueType::UtcOffset if is_v4 => self
.into_offset()
.map(VCardValue::PartialDateTime)
.unwrap_or_else(VCardValue::Text),
VCardValueType::Boolean => VCardValue::Boolean(self.into_boolean()),
VCardValueType::Float => self
.into_float()
.map(VCardValue::Float)
.unwrap_or_else(VCardValue::Text),
VCardValueType::Integer => self
.into_integer()
.map(VCardValue::Integer)
.unwrap_or_else(VCardValue::Text),
VCardValueType::LanguageTag | VCardValueType::Text => {
VCardValue::Text(self.into_string())
}
VCardValueType::Uri => self
.into_uri_bytes()
.map(|data| VCardValue::Binary(Box::new(data)))
.unwrap_or_else(VCardValue::Text),
VCardValueType::Date
| VCardValueType::DateAndOrTime
| VCardValueType::DateTime
| VCardValueType::Time => self
.into_vcard_datetime_or_legacy()
.map(VCardValue::PartialDateTime)
.unwrap_or_else(VCardValue::Text),
VCardValueType::Timestamp => self
.into_vcard_timestamp_or_legacy()
.map(VCardValue::PartialDateTime)
.unwrap_or_else(VCardValue::Text),
VCardValueType::UtcOffset => self
.into_vcard_offset_or_legacy()
.map(VCardValue::PartialDateTime)
.unwrap_or_else(VCardValue::Text),
}
}
pub(crate) fn into_vcard_date(self) -> std::result::Result<PartialDateTime, String> {
let mut dt = PartialDateTime::default();
dt.parse_vcard_date(&mut self.text.iter().peekable());
if !dt.is_null() {
Ok(dt)
} else {
Err(self.into_string())
}
}
pub(crate) fn into_vcard_date_and_or_datetime(
self,
) -> std::result::Result<PartialDateTime, String> {
let mut dt = PartialDateTime::default();
dt.parse_vcard_date_and_or_time(&mut self.text.iter().peekable());
if !dt.is_null() {
Ok(dt)
} else {
Err(self.into_string())
}
}
pub(crate) fn into_vcard_date_time(self) -> std::result::Result<PartialDateTime, String> {
let mut dt = PartialDateTime::default();
dt.parse_vcard_date_time(&mut self.text.iter().peekable());
if !dt.is_null() {
Ok(dt)
} else {
Err(self.into_string())
}
}
pub(crate) fn into_vcard_time(self) -> std::result::Result<PartialDateTime, String> {
let mut dt = PartialDateTime::default();
dt.parse_vcard_time(&mut self.text.iter().peekable(), false);
if !dt.is_null() {
Ok(dt)
} else {
Err(self.into_string())
}
}
pub(crate) fn into_vcard_timestamp_or_legacy(
self,
) -> std::result::Result<PartialDateTime, String> {
let mut dt = PartialDateTime::default();
if dt.parse_timestamp(&mut self.text.iter().peekable(), false) {
Ok(dt)
} else {
let mut dt = PartialDateTime::default();
if dt.parse_vcard_date_legacy(&mut self.text.iter().peekable()) {
Ok(dt)
} else {
Err(self.into_string())
}
}
}
pub(crate) fn into_vcard_datetime_or_legacy(
self,
) -> std::result::Result<PartialDateTime, String> {
let mut dt = PartialDateTime::default();
if dt.parse_vcard_date_legacy(&mut self.text.iter().peekable()) {
Ok(dt)
} else {
self.into_vcard_date_and_or_datetime()
}
}
pub(crate) fn into_vcard_offset_or_legacy(
self,
) -> std::result::Result<PartialDateTime, String> {
let mut dt = PartialDateTime::default();
if dt.parse_vcard_zone_legacy(&mut self.text.iter().peekable()) {
Ok(dt)
} else {
self.into_offset()
}
}
}
impl PartialDateTime {
pub fn parse_vcard_date_legacy(&mut self, iter: &mut Peekable<Iter<'_, u8>>) -> bool {
let mut idx = 0;
for ch in iter {
match ch {
b'0'..=b'9' => {
let value = match idx {
0 => {
if let Some(value) = &mut self.year {
*value =
value.saturating_mul(10).saturating_add((ch - b'0') as u16);
} else {
self.year = Some((ch - b'0') as u16);
}
continue;
}
1 => &mut self.month,
2 => &mut self.day,
3 => &mut self.hour,
4 => &mut self.minute,
5 => &mut self.second,
6 => &mut self.tz_hour,
7 => &mut self.tz_minute,
_ => return false,
};
if let Some(value) = value {
*value = value.saturating_mul(10).saturating_add(ch - b'0');
} else {
*value = Some(ch - b'0');
}
}
b'T' | b't' if idx < 3 => {
idx = 3;
}
b'+' if idx <= 5 => {
idx = 6;
}
b'Z' | b'z' if idx == 5 => {
self.tz_hour = Some(0);
self.tz_minute = Some(0);
break;
}
b'-' if idx <= 2 => {
idx += 1;
}
b'-' if idx <= 5 => {
self.tz_minus = true;
idx = 6;
}
b':' if (3..=6).contains(&idx) => {
idx += 1;
}
b' ' | b'\t' | b'\r' | b'\n' => {
continue;
}
_ => return false,
}
}
self.has_date() || self.has_zone()
}
pub fn parse_vcard_zone_legacy(&mut self, iter: &mut Peekable<Iter<'_, u8>>) -> bool {
let mut idx = 0;
for ch in iter {
match ch {
b'0'..=b'9' => {
let value = match idx {
0 => &mut self.tz_hour,
1 => &mut self.tz_minute,
_ => return false,
};
if let Some(value) = value {
*value = value.saturating_mul(10).saturating_add(ch - b'0');
} else {
*value = Some(ch - b'0');
}
}
b'+' if self.tz_hour.is_none() => {}
b'-' if self.tz_hour.is_none() => {
self.tz_minus = true;
}
b'Z' | b'z' if self.tz_hour.is_none() => {
self.tz_hour = Some(0);
self.tz_minute = Some(0);
break;
}
b':' => {
idx += 1;
}
b' ' | b'\t' | b'\r' | b'\n' => {
continue;
}
_ => return false,
}
}
self.tz_hour.is_some() && self.tz_minute.is_some()
}
pub fn parse_vcard_date_time(&mut self, iter: &mut Peekable<Iter<'_, u8>>) {
self.parse_vcard_date_noreduc(iter);
if matches!(iter.peek(), Some(&&b'T' | &&b't')) {
iter.next();
self.parse_vcard_time(iter, true);
}
}
pub fn parse_vcard_date_and_or_time(&mut self, iter: &mut Peekable<Iter<'_, u8>>) {
self.parse_vcard_date(iter);
if matches!(iter.peek(), Some(&&b'T' | &&b't')) {
iter.next();
self.parse_vcard_time(iter, false);
}
}
pub fn parse_vcard_date(&mut self, iter: &mut Peekable<Iter<'_, u8>>) {
parse_digits(iter, &mut self.year, 4, true);
if self.year.is_some() && iter.peek() == Some(&&b'-') {
iter.next();
parse_small_digits(iter, &mut self.month, 2, true);
} else {
parse_small_digits(iter, &mut self.month, 2, true);
parse_small_digits(iter, &mut self.day, 2, false);
}
}
pub fn parse_vcard_date_noreduc(&mut self, iter: &mut Peekable<Iter<'_, u8>>) {
parse_digits(iter, &mut self.year, 4, true);
parse_small_digits(iter, &mut self.month, 2, true);
parse_small_digits(iter, &mut self.day, 2, false);
}
pub fn parse_vcard_time(&mut self, iter: &mut Peekable<Iter<'_, u8>>, mut notrunc: bool) {
for part in [&mut self.hour, &mut self.minute, &mut self.second] {
match iter.peek() {
Some(b'0'..=b'9') => {
notrunc = true;
parse_small_digits(iter, part, 2, false);
}
Some(b'-') if !notrunc => {
iter.next();
}
_ => break,
}
}
self.parse_zone(iter);
}
}
+425
View File
@@ -0,0 +1,425 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{
types::{LEGACY_ADR_COMPONENTS, LEGACY_N_COMPONENTS},
*,
};
impl ArchivedVCardProperty {
pub fn as_str(&self) -> &str {
match self {
ArchivedVCardProperty::Source => "SOURCE",
ArchivedVCardProperty::Kind => "KIND",
ArchivedVCardProperty::Xml => "XML",
ArchivedVCardProperty::Fn => "FN",
ArchivedVCardProperty::N => "N",
ArchivedVCardProperty::Nickname => "NICKNAME",
ArchivedVCardProperty::Photo => "PHOTO",
ArchivedVCardProperty::Bday => "BDAY",
ArchivedVCardProperty::Anniversary => "ANNIVERSARY",
ArchivedVCardProperty::Gender => "GENDER",
ArchivedVCardProperty::Adr => "ADR",
ArchivedVCardProperty::Tel => "TEL",
ArchivedVCardProperty::Email => "EMAIL",
ArchivedVCardProperty::Impp => "IMPP",
ArchivedVCardProperty::Lang => "LANG",
ArchivedVCardProperty::Tz => "TZ",
ArchivedVCardProperty::Geo => "GEO",
ArchivedVCardProperty::Title => "TITLE",
ArchivedVCardProperty::Role => "ROLE",
ArchivedVCardProperty::Logo => "LOGO",
ArchivedVCardProperty::Org => "ORG",
ArchivedVCardProperty::Member => "MEMBER",
ArchivedVCardProperty::Related => "RELATED",
ArchivedVCardProperty::Categories => "CATEGORIES",
ArchivedVCardProperty::Note => "NOTE",
ArchivedVCardProperty::Prodid => "PRODID",
ArchivedVCardProperty::Rev => "REV",
ArchivedVCardProperty::Sound => "SOUND",
ArchivedVCardProperty::Uid => "UID",
ArchivedVCardProperty::Clientpidmap => "CLIENTPIDMAP",
ArchivedVCardProperty::Url => "URL",
ArchivedVCardProperty::Version => "VERSION",
ArchivedVCardProperty::Key => "KEY",
ArchivedVCardProperty::Fburl => "FBURL",
ArchivedVCardProperty::Caladruri => "CALADRURI",
ArchivedVCardProperty::Caluri => "CALURI",
ArchivedVCardProperty::Birthplace => "BIRTHPLACE",
ArchivedVCardProperty::Deathplace => "DEATHPLACE",
ArchivedVCardProperty::Deathdate => "DEATHDATE",
ArchivedVCardProperty::Expertise => "EXPERTISE",
ArchivedVCardProperty::Hobby => "HOBBY",
ArchivedVCardProperty::Interest => "INTEREST",
ArchivedVCardProperty::OrgDirectory => "ORG-DIRECTORY",
ArchivedVCardProperty::ContactUri => "CONTACT-URI",
ArchivedVCardProperty::Created => "CREATED",
ArchivedVCardProperty::Gramgender => "GRAMGENDER",
ArchivedVCardProperty::Language => "LANGUAGE",
ArchivedVCardProperty::Pronouns => "PRONOUNS",
ArchivedVCardProperty::Socialprofile => "SOCIALPROFILE",
ArchivedVCardProperty::Jsprop => "JSPROP",
ArchivedVCardProperty::Begin => "BEGIN",
ArchivedVCardProperty::End => "END",
ArchivedVCardProperty::Other(s) => s.as_str(),
}
}
pub(crate) fn legacy_max_values(&self) -> usize {
match self {
ArchivedVCardProperty::N => LEGACY_N_COMPONENTS,
ArchivedVCardProperty::Adr => LEGACY_ADR_COMPONENTS,
_ => usize::MAX,
}
}
}
impl ArchivedVCardProperty {
// Returns the default value type and whether the property is multi-valued.
pub(crate) fn default_types(&self) -> (ArchivedValueType, ValueSeparator) {
match self {
ArchivedVCardProperty::Source => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Kind => (ArchivedValueType::Kind, ValueSeparator::None),
ArchivedVCardProperty::Xml => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Fn => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::N => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::SemicolonAndComma,
),
ArchivedVCardProperty::Nickname => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::Comma,
),
ArchivedVCardProperty::Photo => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Bday => (
ArchivedValueType::Vcard(ArchivedVCardValueType::DateAndOrTime),
ValueSeparator::None,
),
ArchivedVCardProperty::Anniversary => (
ArchivedValueType::Vcard(ArchivedVCardValueType::DateAndOrTime),
ValueSeparator::None,
),
ArchivedVCardProperty::Gender => (ArchivedValueType::Sex, ValueSeparator::Semicolon),
ArchivedVCardProperty::Adr => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::SemicolonAndComma,
),
ArchivedVCardProperty::Tel => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Email => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Impp => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Lang => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Tz => (
ArchivedValueType::Vcard(ArchivedVCardValueType::UtcOffset),
ValueSeparator::None,
),
ArchivedVCardProperty::Geo => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Title => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Role => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Logo => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Org => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::Semicolon,
),
ArchivedVCardProperty::Member => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Related => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Categories => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::Comma,
),
ArchivedVCardProperty::Note => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Prodid => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Rev => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Timestamp),
ValueSeparator::None,
),
ArchivedVCardProperty::Sound => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Uid => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Clientpidmap => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::Semicolon,
),
ArchivedVCardProperty::Url => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Version => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Key => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Fburl => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Caladruri => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Caluri => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Birthplace => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Deathplace => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Deathdate => (
ArchivedValueType::Vcard(ArchivedVCardValueType::DateAndOrTime),
ValueSeparator::None,
),
ArchivedVCardProperty::Expertise => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Hobby => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Interest => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::OrgDirectory => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::ContactUri => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Created => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Timestamp),
ValueSeparator::None,
),
ArchivedVCardProperty::Gramgender => {
(ArchivedValueType::GramGender, ValueSeparator::None)
}
ArchivedVCardProperty::Language => (
ArchivedValueType::Vcard(ArchivedVCardValueType::LanguageTag),
ValueSeparator::None,
),
ArchivedVCardProperty::Pronouns => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Socialprofile => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Uri),
ValueSeparator::None,
),
ArchivedVCardProperty::Jsprop => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
ArchivedVCardProperty::Begin | ArchivedVCardProperty::End => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::Skip,
),
ArchivedVCardProperty::Other(_) => (
ArchivedValueType::Vcard(ArchivedVCardValueType::Text),
ValueSeparator::None,
),
}
}
}
impl IanaString for ArchivedVCardValueType {
fn as_str(&self) -> &'static str {
match self {
ArchivedVCardValueType::Boolean => "BOOLEAN",
ArchivedVCardValueType::Date => "DATE",
ArchivedVCardValueType::DateAndOrTime => "DATE-AND-OR-TIME",
ArchivedVCardValueType::DateTime => "DATE-TIME",
ArchivedVCardValueType::Float => "FLOAT",
ArchivedVCardValueType::Integer => "INTEGER",
ArchivedVCardValueType::LanguageTag => "LANGUAGE-TAG",
ArchivedVCardValueType::Text => "TEXT",
ArchivedVCardValueType::Time => "TIME",
ArchivedVCardValueType::Timestamp => "TIMESTAMP",
ArchivedVCardValueType::Uri => "URI",
ArchivedVCardValueType::UtcOffset => "UTC-OFFSET",
}
}
}
impl IanaString for ArchivedVCardLevel {
fn as_str(&self) -> &'static str {
match self {
ArchivedVCardLevel::Beginner => "BEGINNER",
ArchivedVCardLevel::Average => "AVERAGE",
ArchivedVCardLevel::Expert => "EXPERT",
ArchivedVCardLevel::High => "HIGH",
ArchivedVCardLevel::Medium => "MEDIUM",
ArchivedVCardLevel::Low => "LOW",
}
}
}
impl IanaString for ArchivedVCardPhonetic {
fn as_str(&self) -> &'static str {
match self {
ArchivedVCardPhonetic::Ipa => "IPA",
ArchivedVCardPhonetic::Jyut => "JYUT",
ArchivedVCardPhonetic::Piny => "PINY",
ArchivedVCardPhonetic::Script => "SCRIPT",
}
}
}
impl IanaString for ArchivedVCardType {
fn as_str(&self) -> &'static str {
match self {
ArchivedVCardType::Work => "WORK",
ArchivedVCardType::Home => "HOME",
ArchivedVCardType::Billing => "BILLING",
ArchivedVCardType::Delivery => "DELIVERY",
ArchivedVCardType::Contact => "CONTACT",
ArchivedVCardType::Acquaintance => "ACQUAINTANCE",
ArchivedVCardType::Friend => "FRIEND",
ArchivedVCardType::Met => "MET",
ArchivedVCardType::CoWorker => "CO-WORKER",
ArchivedVCardType::Colleague => "COLLEAGUE",
ArchivedVCardType::CoResident => "CO-RESIDENT",
ArchivedVCardType::Neighbor => "NEIGHBOR",
ArchivedVCardType::Child => "CHILD",
ArchivedVCardType::Parent => "PARENT",
ArchivedVCardType::Sibling => "SIBLING",
ArchivedVCardType::Spouse => "SPOUSE",
ArchivedVCardType::Kin => "KIN",
ArchivedVCardType::Muse => "MUSE",
ArchivedVCardType::Crush => "CRUSH",
ArchivedVCardType::Date => "DATE",
ArchivedVCardType::Sweetheart => "SWEETHEART",
ArchivedVCardType::Me => "ME",
ArchivedVCardType::Agent => "AGENT",
ArchivedVCardType::Emergency => "EMERGENCY",
ArchivedVCardType::Text => "TEXT",
ArchivedVCardType::Voice => "VOICE",
ArchivedVCardType::Fax => "FAX",
ArchivedVCardType::Cell => "CELL",
ArchivedVCardType::Video => "VIDEO",
ArchivedVCardType::Pager => "PAGER",
ArchivedVCardType::Textphone => "TEXTPHONE",
ArchivedVCardType::MainNumber => "MAIN-NUMBER",
}
}
}
impl IanaString for ArchivedVCardGramGender {
fn as_str(&self) -> &'static str {
match self {
ArchivedVCardGramGender::Animate => "ANIMATE",
ArchivedVCardGramGender::Common => "COMMON",
ArchivedVCardGramGender::Feminine => "FEMININE",
ArchivedVCardGramGender::Inanimate => "INANIMATE",
ArchivedVCardGramGender::Masculine => "MASCULINE",
ArchivedVCardGramGender::Neuter => "NEUTER",
}
}
}
impl IanaString for ArchivedVCardSex {
fn as_str(&self) -> &'static str {
match self {
ArchivedVCardSex::Male => "M",
ArchivedVCardSex::Female => "F",
ArchivedVCardSex::Other => "O",
ArchivedVCardSex::NoneOrNotApplicable => "N",
ArchivedVCardSex::Unknown => "U",
}
}
}
impl IanaString for ArchivedVCardKind {
fn as_str(&self) -> &'static str {
match self {
ArchivedVCardKind::Individual => "INDIVIDUAL",
ArchivedVCardKind::Group => "GROUP",
ArchivedVCardKind::Org => "ORG",
ArchivedVCardKind::Location => "LOCATION",
ArchivedVCardKind::Application => "APPLICATION",
ArchivedVCardKind::Device => "DEVICE",
}
}
}
#[derive(Debug)]
pub(crate) enum ArchivedValueType {
Vcard(ArchivedVCardValueType),
Kind,
Sex,
GramGender,
}
impl ArchivedValueType {
pub fn unwrap_vcard(self) -> ArchivedVCardValueType {
match self {
ArchivedValueType::Vcard(v) => v,
_ => ArchivedVCardValueType::Text,
}
}
}
+225
View File
@@ -0,0 +1,225 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::*;
use crate::common::{ArchivedCalendarScale, ArchivedData, ArchivedPartialDateTime};
impl ArchivedVCard {
pub fn uid(&self) -> Option<&str> {
self.property(&VCardProperty::Uid)
.and_then(|e| e.values.first())
.and_then(|v| v.as_text())
}
pub fn property(&self, prop: &VCardProperty) -> Option<&ArchivedVCardEntry> {
self.entries.iter().find(|entry| &entry.name == prop)
}
pub fn properties<'x, 'y: 'x>(
&'x self,
prop: &'y VCardProperty,
) -> impl Iterator<Item = &'x ArchivedVCardEntry> + 'x {
self.entries.iter().filter(move |entry| &entry.name == prop)
}
pub fn version(&self) -> Option<VCardVersion> {
self.entries
.iter()
.find(|e| e.name == VCardProperty::Version)
.and_then(|e| {
e.values
.first()
.and_then(|v| v.as_text())
.and_then(VCardVersion::try_parse)
})
}
}
impl ArchivedVCardEntry {
#[inline]
pub fn parameters(
&self,
prop: &VCardParameterName,
) -> impl Iterator<Item = &ArchivedVCardParameterValue> {
self.params.iter().filter_map(move |param| {
if &param.name == prop {
Some(&param.value)
} else {
None
}
})
}
}
impl ArchivedVCardValue {
pub fn as_text(&self) -> Option<&str> {
match self {
ArchivedVCardValue::Text(v) => Some(v.as_str()),
ArchivedVCardValue::Sex(v) => Some(v.as_str()),
ArchivedVCardValue::GramGender(v) => Some(v.as_str()),
ArchivedVCardValue::Kind(v) => Some(v.as_str()),
ArchivedVCardValue::Component(v) => v.first().map(|s| s.as_str()),
ArchivedVCardValue::Integer(_)
| ArchivedVCardValue::Float(_)
| ArchivedVCardValue::Boolean(_)
| ArchivedVCardValue::PartialDateTime(_)
| ArchivedVCardValue::Binary(_) => None,
}
}
pub fn as_integer(&self) -> Option<i64> {
match self {
ArchivedVCardValue::Integer(i) => Some(i.to_native()),
_ => None,
}
}
pub fn as_float(&self) -> Option<f64> {
match self {
ArchivedVCardValue::Float(f) => Some(f.to_native()),
_ => None,
}
}
pub fn as_boolean(&self) -> Option<bool> {
match self {
ArchivedVCardValue::Boolean(b) => Some(*b),
_ => None,
}
}
pub fn as_partial_date_time(&self) -> Option<&ArchivedPartialDateTime> {
match self {
ArchivedVCardValue::PartialDateTime(dt) => Some(dt),
_ => None,
}
}
pub fn as_binary(&self) -> Option<&ArchivedData> {
match self {
ArchivedVCardValue::Binary(d) => Some(d),
_ => None,
}
}
}
impl ArchivedVCardParameterName {
pub fn as_str(&self) -> &str {
match self {
ArchivedVCardParameterName::Language => "LANGUAGE",
ArchivedVCardParameterName::Value => "VALUE",
ArchivedVCardParameterName::Pref => "PREF",
ArchivedVCardParameterName::Altid => "ALTID",
ArchivedVCardParameterName::Pid => "PID",
ArchivedVCardParameterName::Type => "TYPE",
ArchivedVCardParameterName::Mediatype => "MEDIATYPE",
ArchivedVCardParameterName::Calscale => "CALSCALE",
ArchivedVCardParameterName::SortAs => "SORT-AS",
ArchivedVCardParameterName::Geo => "GEO",
ArchivedVCardParameterName::Tz => "TZ",
ArchivedVCardParameterName::Index => "INDEX",
ArchivedVCardParameterName::Level => "LEVEL",
ArchivedVCardParameterName::Group => "GROUP",
ArchivedVCardParameterName::Cc => "CC",
ArchivedVCardParameterName::Author => "AUTHOR",
ArchivedVCardParameterName::AuthorName => "AUTHOR-NAME",
ArchivedVCardParameterName::Created => "CREATED",
ArchivedVCardParameterName::Derived => "DERIVED",
ArchivedVCardParameterName::Label => "LABEL",
ArchivedVCardParameterName::Phonetic => "PHONETIC",
ArchivedVCardParameterName::PropId => "PROP-ID",
ArchivedVCardParameterName::Script => "SCRIPT",
ArchivedVCardParameterName::ServiceType => "SERVICE-TYPE",
ArchivedVCardParameterName::Username => "USERNAME",
ArchivedVCardParameterName::Jsptr => "JSPTR",
ArchivedVCardParameterName::Jscomps => "JSCOMPS",
ArchivedVCardParameterName::Other(name) => name.as_str(),
}
}
}
impl ArchivedVCardParameterValue {
pub fn as_text(&self) -> Option<&str> {
match self {
ArchivedVCardParameterValue::Text(v) => Some(v.as_str()),
ArchivedVCardParameterValue::ValueType(v) => Some(v.as_str()),
ArchivedVCardParameterValue::Type(v) => Some(v.as_str()),
ArchivedVCardParameterValue::Calscale(v) => Some(v.as_str()),
ArchivedVCardParameterValue::Level(v) => Some(v.as_str()),
ArchivedVCardParameterValue::Phonetic(v) => Some(v.as_str()),
ArchivedVCardParameterValue::Jscomps(_)
| ArchivedVCardParameterValue::Integer(_)
| ArchivedVCardParameterValue::Timestamp(_)
| ArchivedVCardParameterValue::Bool(_)
| ArchivedVCardParameterValue::Null => None,
}
}
pub fn as_phonetic(&self) -> Option<IanaType<&ArchivedVCardPhonetic, &str>> {
match self {
ArchivedVCardParameterValue::Phonetic(v) => Some(IanaType::Iana(v)),
ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn as_calscale(&self) -> Option<IanaType<&ArchivedCalendarScale, &str>> {
match self {
ArchivedVCardParameterValue::Calscale(v) => Some(IanaType::Iana(v)),
ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn as_level(&self) -> Option<IanaType<&ArchivedVCardLevel, &str>> {
match self {
ArchivedVCardParameterValue::Level(v) => Some(IanaType::Iana(v)),
ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn as_type(&self) -> Option<IanaType<&ArchivedVCardType, &str>> {
match self {
ArchivedVCardParameterValue::Type(v) => Some(IanaType::Iana(v)),
ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn as_value_type(&self) -> Option<IanaType<&ArchivedVCardValueType, &str>> {
match self {
ArchivedVCardParameterValue::ValueType(v) => Some(IanaType::Iana(v)),
ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn as_bool(&self) -> Option<IanaType<bool, &str>> {
match self {
ArchivedVCardParameterValue::Bool(b) => Some(IanaType::Iana(*b)),
ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn as_integer(&self) -> Option<IanaType<u32, &str>> {
match self {
ArchivedVCardParameterValue::Integer(i) => Some(IanaType::Iana(i.to_native())),
ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn as_timestamp(&self) -> Option<IanaType<i64, &str>> {
match self {
ArchivedVCardParameterValue::Timestamp(t) => Some(IanaType::Iana(t.to_native())),
ArchivedVCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
}
+365
View File
@@ -0,0 +1,365 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::{
ArchivedPartialDateTime, PartialDateTime,
format::{AsciiPush, BufferedWrite},
parser::Timestamp,
writer::{
DOCUMENT_BUFFER, ENTRY_BUFFER, FoldingWriter, JscompRef, LineWriter, NeedsQuotes,
write_jscomps, write_param_value, write_text,
},
},
vcard::{media_type::legacy_media_type, *},
};
use std::fmt::{self, Display, Write};
impl ArchivedVCard {
pub fn write_to(&self, out: &mut impl Write, version: VCardVersion) -> fmt::Result {
out.write_buffered::<DOCUMENT_BUFFER>(|buf| {
self.write_entries(&mut FoldingWriter::new(buf), version)
})
}
fn write_entries<W: Write + AsciiPush + ?Sized>(
&self,
out: &mut FoldingWriter<'_, W>,
version: VCardVersion,
) -> fmt::Result {
out.write_raw(version.begin_and_version())?;
for entry in self.entries.iter() {
if !matches!(
entry.name,
ArchivedVCardProperty::Version
| ArchivedVCardProperty::Begin
| ArchivedVCardProperty::End
) {
entry.write_line(out, true, version)?;
}
}
out.write_raw("END:VCARD\r\n")
}
}
impl ArchivedVCardEntry {
#[deprecated(since = "0.4.0", note = "use write_with_version")]
pub fn write_to(&self, out: &mut impl Write, with_value: bool, is_v4: bool) -> fmt::Result {
self.write_with_version(
out,
with_value,
if is_v4 {
VCardVersion::V4_0
} else {
VCardVersion::V3_0
},
)
}
pub fn write_with_version(
&self,
out: &mut impl Write,
with_value: bool,
version: VCardVersion,
) -> fmt::Result {
out.write_buffered::<ENTRY_BUFFER>(|buf| {
self.write_line(&mut FoldingWriter::new(buf), with_value, version)
})
}
pub(crate) fn write_line<W: Write + AsciiPush + ?Sized>(
&self,
out: &mut FoldingWriter<'_, W>,
with_value: bool,
version: VCardVersion,
) -> fmt::Result {
let is_v4 = matches!(version, VCardVersion::V4_0);
if let Some(group_name) = self.group.as_ref() {
out.write_atomic(group_name)?;
out.write_atomic(".")?;
}
out.write_atomic(self.name.as_str())?;
let mut types = None;
let mut last_param: Option<&ArchivedVCardParameterName> = None;
let legacy_binary = if is_v4 {
None
} else {
self.values.iter().find_map(|v| match v {
ArchivedVCardValue::Binary(data) => Some(data),
_ => None,
})
};
for param in self.params.iter() {
if !is_v4
&& match param.name {
ArchivedVCardParameterName::PropId | ArchivedVCardParameterName::Jscomps => {
true
}
ArchivedVCardParameterName::Mediatype => legacy_binary.is_some(),
_ => false,
}
{
continue;
}
if last_param.is_some_and(|last_param| last_param == &param.name) {
out.write_atomic(",")?;
} else {
out.write_atomic(";")?;
out.write_atomic(param.name.as_str())?;
if !matches!(param.value, ArchivedVCardParameterValue::Null) {
out.write_atomic("=")?;
}
last_param = Some(&param.name);
}
match &param.value {
ArchivedVCardParameterValue::Text(v)
if param.name == VCardParameterName::Jsptr && !v.as_str().needs_quotes() =>
{
out.write_atomic("\"")?;
write_param_value(out, v, is_v4)?;
out.write_atomic("\"")?;
}
ArchivedVCardParameterValue::Text(v) => {
write_param_value(out, v, is_v4)?;
}
ArchivedVCardParameterValue::Integer(i) => {
out.push_u64(i.to_native().into())?;
}
ArchivedVCardParameterValue::Timestamp(v) => {
Timestamp(v.to_native()).push_to(out)?;
}
ArchivedVCardParameterValue::Bool(v) => {
out.write_atomic(if *v { "TRUE" } else { "FALSE" })?;
}
ArchivedVCardParameterValue::ValueType(v) => {
if types.is_none() {
types = Some(v);
}
write_param_value(out, v.as_str(), is_v4)?;
}
ArchivedVCardParameterValue::Type(v) => {
write_param_value(out, v.as_str(), is_v4)?;
}
ArchivedVCardParameterValue::Calscale(v) => {
write_param_value(out, v.as_str(), is_v4)?;
}
ArchivedVCardParameterValue::Level(v) => {
write_param_value(out, v.as_str(), is_v4)?;
}
ArchivedVCardParameterValue::Phonetic(v) => {
write_param_value(out, v.as_str(), is_v4)?;
}
ArchivedVCardParameterValue::Jscomps(v) => {
out.write_atomic("\"")?;
write_jscomps(out, v)?;
out.write_atomic("\"")?;
last_param = None;
}
ArchivedVCardParameterValue::Null => {
last_param = None;
}
}
}
let is_v21_base64 = matches!(version, VCardVersion::V2_1) && legacy_binary.is_some();
let max_values = if is_v4 {
usize::MAX
} else {
self.name.legacy_max_values()
};
if !is_v4 {
if let Some(data) = legacy_binary {
out.write_atomic(if is_v21_base64 {
";ENCODING=BASE64"
} else {
";ENCODING=b"
})?;
if let Some(media_type) = data.content_type.as_deref()
&& !self
.params
.iter()
.any(|param| param.name == VCardParameterName::Type)
&& let Some(token) = legacy_media_type(self.name.as_str(), media_type)
{
out.write_atomic(";TYPE=")?;
out.write_atomic(&token)?;
}
}
if self.values.iter().take(max_values).any(|v| match v {
ArchivedVCardValue::Text(s) => !s.is_ascii(),
ArchivedVCardValue::Component(items) => items.iter().any(|s| !s.is_ascii()),
_ => false,
}) {
out.write_atomic(";CHARSET=UTF-8")?;
}
}
out.write_atomic(":")?;
if with_value {
let (default_type, value_separator) = self.name.default_types();
let default_type = default_type.unwrap_vcard();
let mut separator = ";";
let mut escape_semicolon =
matches!(types.unwrap_or(&default_type), ArchivedVCardValueType::Text);
let mut escape_comma = escape_semicolon;
match value_separator {
ValueSeparator::Comma => {
escape_comma = true;
separator = ",";
}
ValueSeparator::Semicolon => escape_semicolon = true,
ValueSeparator::SemicolonAndComma => {
escape_semicolon = true;
escape_comma = true;
}
_ => {}
}
for (pos, value) in self.values.iter().take(max_values).enumerate() {
if pos > 0 {
out.write_atomic(separator)?;
}
match value {
ArchivedVCardValue::Text(v) => {
write_text(out, v, escape_semicolon, escape_comma)?;
}
ArchivedVCardValue::Component(v) => {
for (pos, item) in v.iter().enumerate() {
if pos > 0 {
out.write_atomic(",")?;
}
write_text(out, item, true, true)?;
}
}
ArchivedVCardValue::Integer(v) => {
out.push_i64(v.to_native())?;
}
ArchivedVCardValue::Float(v) => {
write!(out, "{v}")?;
}
ArchivedVCardValue::Boolean(v) => {
out.write_atomic(if *v { "TRUE" } else { "FALSE" })?;
}
ArchivedVCardValue::PartialDateTime(v) => {
let typ = if pos == 0 {
types
} else {
self.parameters(&VCardParameterName::Value)
.nth(pos)
.and_then(|v| v.as_value_type())
.and_then(|v| v.iana().copied())
}
.unwrap_or(&default_type);
let date = PartialDateTime::from(v);
let typ = VCardValueType::from(typ);
if is_v4 {
date.push_vcard(out, &typ)?;
} else {
date.push_legacy_vcard(out, &typ)?;
}
}
ArchivedVCardValue::Binary(v) => {
if is_v4 {
let media_type = v.content_type.as_deref().unwrap_or_default();
out.write_str("data:")?;
out.write_str(media_type)?;
if escape_semicolon {
out.write_atomic("\\;")?;
} else {
out.write_str(";")?;
}
out.write_atomic("base64\\,")?;
}
out.write_base64(&v.data)?;
}
ArchivedVCardValue::Sex(v) => {
out.write_atomic(v.as_str())?;
}
ArchivedVCardValue::GramGender(v) => {
out.write_atomic(v.as_str())?;
}
ArchivedVCardValue::Kind(v) => {
out.write_atomic(v.as_str())?;
}
}
}
}
out.end_line()?;
if is_v21_base64 {
out.end_line()?;
}
Ok(())
}
}
impl ArchivedPartialDateTime {
pub fn format_as_vcard(
&self,
out: &mut impl Write,
fmt: &ArchivedVCardValueType,
) -> fmt::Result {
PartialDateTime::from(self).format_as_vcard(out, &VCardValueType::from(fmt))
}
pub fn format_as_legacy_vcard(
&self,
out: &mut impl Write,
fmt: &ArchivedVCardValueType,
) -> fmt::Result {
PartialDateTime::from(self).format_as_legacy_vcard(out, &VCardValueType::from(fmt))
}
}
impl From<&ArchivedVCardValueType> for VCardValueType {
fn from(value_type: &ArchivedVCardValueType) -> Self {
match value_type {
ArchivedVCardValueType::Boolean => VCardValueType::Boolean,
ArchivedVCardValueType::Date => VCardValueType::Date,
ArchivedVCardValueType::DateAndOrTime => VCardValueType::DateAndOrTime,
ArchivedVCardValueType::DateTime => VCardValueType::DateTime,
ArchivedVCardValueType::Float => VCardValueType::Float,
ArchivedVCardValueType::Integer => VCardValueType::Integer,
ArchivedVCardValueType::LanguageTag => VCardValueType::LanguageTag,
ArchivedVCardValueType::Text => VCardValueType::Text,
ArchivedVCardValueType::Time => VCardValueType::Time,
ArchivedVCardValueType::Timestamp => VCardValueType::Timestamp,
ArchivedVCardValueType::Uri => VCardValueType::Uri,
ArchivedVCardValueType::UtcOffset => VCardValueType::UtcOffset,
}
}
}
impl Display for ArchivedVCard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.write_to(f, self.version().unwrap_or_default())
}
}
impl<'x> From<&'x ArchivedJscomp> for JscompRef<'x> {
fn from(jscomp: &'x ArchivedJscomp) -> Self {
match jscomp {
ArchivedJscomp::Entry { position, value } => JscompRef::Entry {
position: position.to_native(),
value: value.to_native(),
},
ArchivedJscomp::Separator(separator) => JscompRef::Separator(separator),
}
}
}
+629
View File
@@ -0,0 +1,629 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use super::*;
impl IanaParse for VCardProperty {
fn parse(value: &[u8]) -> Option<Self> {
hashify::fnc_map_ignore_case!(value,
"BEGIN" => Some(VCardProperty::Begin),
"END" => Some(VCardProperty::End),
"SOURCE" => Some(VCardProperty::Source),
"KIND" => Some(VCardProperty::Kind),
"XML" => Some(VCardProperty::Xml),
"FN" => Some(VCardProperty::Fn),
"N" => Some(VCardProperty::N),
"NICKNAME" => Some(VCardProperty::Nickname),
"PHOTO" => Some(VCardProperty::Photo),
"BDAY" => Some(VCardProperty::Bday),
"ANNIVERSARY" => Some(VCardProperty::Anniversary),
"GENDER" => Some(VCardProperty::Gender),
"ADR" => Some(VCardProperty::Adr),
"TEL" => Some(VCardProperty::Tel),
"EMAIL" => Some(VCardProperty::Email),
"IMPP" => Some(VCardProperty::Impp),
"LANG" => Some(VCardProperty::Lang),
"TZ" => Some(VCardProperty::Tz),
"GEO" => Some(VCardProperty::Geo),
"TITLE" => Some(VCardProperty::Title),
"ROLE" => Some(VCardProperty::Role),
"LOGO" => Some(VCardProperty::Logo),
"ORG" => Some(VCardProperty::Org),
"MEMBER" => Some(VCardProperty::Member),
"RELATED" => Some(VCardProperty::Related),
"CATEGORIES" => Some(VCardProperty::Categories),
"NOTE" => Some(VCardProperty::Note),
"PRODID" => Some(VCardProperty::Prodid),
"REV" => Some(VCardProperty::Rev),
"SOUND" => Some(VCardProperty::Sound),
"UID" => Some(VCardProperty::Uid),
"CLIENTPIDMAP" => Some(VCardProperty::Clientpidmap),
"URL" => Some(VCardProperty::Url),
"VERSION" => Some(VCardProperty::Version),
"KEY" => Some(VCardProperty::Key),
"FBURL" => Some(VCardProperty::Fburl),
"CALADRURI" => Some(VCardProperty::Caladruri),
"CALURI" => Some(VCardProperty::Caluri),
"BIRTHPLACE" => Some(VCardProperty::Birthplace),
"DEATHPLACE" => Some(VCardProperty::Deathplace),
"DEATHDATE" => Some(VCardProperty::Deathdate),
"EXPERTISE" => Some(VCardProperty::Expertise),
"HOBBY" => Some(VCardProperty::Hobby),
"INTEREST" => Some(VCardProperty::Interest),
"ORG-DIRECTORY" => Some(VCardProperty::OrgDirectory),
"CONTACT-URI" => Some(VCardProperty::ContactUri),
"CREATED" => Some(VCardProperty::Created),
"GRAMGENDER" => Some(VCardProperty::Gramgender),
"LANGUAGE" => Some(VCardProperty::Language),
"PRONOUNS" => Some(VCardProperty::Pronouns),
"SOCIALPROFILE" => Some(VCardProperty::Socialprofile),
"JSPROP" => Some(VCardProperty::Jsprop),
_ => None,
)
}
}
impl VCardProperty {
pub fn as_str(&self) -> &str {
match self {
VCardProperty::Source => "SOURCE",
VCardProperty::Kind => "KIND",
VCardProperty::Xml => "XML",
VCardProperty::Fn => "FN",
VCardProperty::N => "N",
VCardProperty::Nickname => "NICKNAME",
VCardProperty::Photo => "PHOTO",
VCardProperty::Bday => "BDAY",
VCardProperty::Anniversary => "ANNIVERSARY",
VCardProperty::Gender => "GENDER",
VCardProperty::Adr => "ADR",
VCardProperty::Tel => "TEL",
VCardProperty::Email => "EMAIL",
VCardProperty::Impp => "IMPP",
VCardProperty::Lang => "LANG",
VCardProperty::Tz => "TZ",
VCardProperty::Geo => "GEO",
VCardProperty::Title => "TITLE",
VCardProperty::Role => "ROLE",
VCardProperty::Logo => "LOGO",
VCardProperty::Org => "ORG",
VCardProperty::Member => "MEMBER",
VCardProperty::Related => "RELATED",
VCardProperty::Categories => "CATEGORIES",
VCardProperty::Note => "NOTE",
VCardProperty::Prodid => "PRODID",
VCardProperty::Rev => "REV",
VCardProperty::Sound => "SOUND",
VCardProperty::Uid => "UID",
VCardProperty::Clientpidmap => "CLIENTPIDMAP",
VCardProperty::Url => "URL",
VCardProperty::Version => "VERSION",
VCardProperty::Key => "KEY",
VCardProperty::Fburl => "FBURL",
VCardProperty::Caladruri => "CALADRURI",
VCardProperty::Caluri => "CALURI",
VCardProperty::Birthplace => "BIRTHPLACE",
VCardProperty::Deathplace => "DEATHPLACE",
VCardProperty::Deathdate => "DEATHDATE",
VCardProperty::Expertise => "EXPERTISE",
VCardProperty::Hobby => "HOBBY",
VCardProperty::Interest => "INTEREST",
VCardProperty::OrgDirectory => "ORG-DIRECTORY",
VCardProperty::ContactUri => "CONTACT-URI",
VCardProperty::Created => "CREATED",
VCardProperty::Gramgender => "GRAMGENDER",
VCardProperty::Language => "LANGUAGE",
VCardProperty::Pronouns => "PRONOUNS",
VCardProperty::Socialprofile => "SOCIALPROFILE",
VCardProperty::Jsprop => "JSPROP",
VCardProperty::Begin => "BEGIN",
VCardProperty::End => "END",
VCardProperty::Other(v) => v.as_str(),
}
}
pub(crate) fn legacy_max_values(&self) -> usize {
match self {
VCardProperty::N => LEGACY_N_COMPONENTS,
VCardProperty::Adr => LEGACY_ADR_COMPONENTS,
_ => usize::MAX,
}
}
}
pub(crate) const LEGACY_N_COMPONENTS: usize = 5;
pub(crate) const LEGACY_ADR_COMPONENTS: usize = 7;
impl VCardProperty {
// Returns the default value type and whether the property is multi-valued.
pub(crate) fn default_types(&self) -> (ValueType, ValueSeparator) {
match self {
VCardProperty::Source => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None),
VCardProperty::Kind => (ValueType::Kind, ValueSeparator::None),
VCardProperty::Xml => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None),
VCardProperty::Fn => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None),
VCardProperty::N => (
ValueType::Vcard(VCardValueType::Text),
ValueSeparator::SemicolonAndComma,
),
VCardProperty::Nickname => (
ValueType::Vcard(VCardValueType::Text),
ValueSeparator::Comma,
),
VCardProperty::Photo => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None),
VCardProperty::Bday => (
ValueType::Vcard(VCardValueType::DateAndOrTime),
ValueSeparator::None,
),
VCardProperty::Anniversary => (
ValueType::Vcard(VCardValueType::DateAndOrTime),
ValueSeparator::None,
),
VCardProperty::Gender => (ValueType::Sex, ValueSeparator::Semicolon),
VCardProperty::Adr => (
ValueType::Vcard(VCardValueType::Text),
ValueSeparator::SemicolonAndComma,
),
VCardProperty::Tel => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None),
VCardProperty::Email => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None),
VCardProperty::Impp => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None),
VCardProperty::Lang => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None),
VCardProperty::Tz => (
ValueType::Vcard(VCardValueType::UtcOffset),
ValueSeparator::None,
),
VCardProperty::Geo => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None),
VCardProperty::Title => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None),
VCardProperty::Role => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None),
VCardProperty::Logo => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None),
VCardProperty::Org => (
ValueType::Vcard(VCardValueType::Text),
ValueSeparator::Semicolon,
),
VCardProperty::Member => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None),
VCardProperty::Related => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None),
VCardProperty::Categories => (
ValueType::Vcard(VCardValueType::Text),
ValueSeparator::Comma,
),
VCardProperty::Note => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None),
VCardProperty::Prodid => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None),
VCardProperty::Rev => (
ValueType::Vcard(VCardValueType::Timestamp),
ValueSeparator::None,
),
VCardProperty::Sound => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None),
VCardProperty::Uid => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None),
VCardProperty::Clientpidmap => (
ValueType::Vcard(VCardValueType::Text),
ValueSeparator::Semicolon,
),
VCardProperty::Url => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None),
VCardProperty::Version => {
(ValueType::Vcard(VCardValueType::Text), ValueSeparator::None)
}
VCardProperty::Key => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None),
VCardProperty::Fburl => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None),
VCardProperty::Caladruri => {
(ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None)
}
VCardProperty::Caluri => (ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None),
VCardProperty::Birthplace => {
(ValueType::Vcard(VCardValueType::Text), ValueSeparator::None)
}
VCardProperty::Deathplace => {
(ValueType::Vcard(VCardValueType::Text), ValueSeparator::None)
}
VCardProperty::Deathdate => (
ValueType::Vcard(VCardValueType::DateAndOrTime),
ValueSeparator::None,
),
VCardProperty::Expertise => {
(ValueType::Vcard(VCardValueType::Text), ValueSeparator::None)
}
VCardProperty::Hobby => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None),
VCardProperty::Interest => {
(ValueType::Vcard(VCardValueType::Text), ValueSeparator::None)
}
VCardProperty::OrgDirectory => {
(ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None)
}
VCardProperty::ContactUri => {
(ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None)
}
VCardProperty::Created => (
ValueType::Vcard(VCardValueType::Timestamp),
ValueSeparator::None,
),
VCardProperty::Gramgender => (ValueType::GramGender, ValueSeparator::None),
VCardProperty::Language => (
ValueType::Vcard(VCardValueType::LanguageTag),
ValueSeparator::None,
),
VCardProperty::Pronouns => {
(ValueType::Vcard(VCardValueType::Text), ValueSeparator::None)
}
VCardProperty::Socialprofile => {
(ValueType::Vcard(VCardValueType::Uri), ValueSeparator::None)
}
VCardProperty::Jsprop => (ValueType::Vcard(VCardValueType::Text), ValueSeparator::None),
VCardProperty::Other(_) => {
(ValueType::Vcard(VCardValueType::Text), ValueSeparator::None)
}
VCardProperty::Begin | VCardProperty::End => {
(ValueType::Vcard(VCardValueType::Text), ValueSeparator::Skip)
}
}
}
}
impl Eq for VCardValue {}
impl IanaString for VCardValueType {
fn as_str(&self) -> &'static str {
match self {
VCardValueType::Boolean => "BOOLEAN",
VCardValueType::Date => "DATE",
VCardValueType::DateAndOrTime => "DATE-AND-OR-TIME",
VCardValueType::DateTime => "DATE-TIME",
VCardValueType::Float => "FLOAT",
VCardValueType::Integer => "INTEGER",
VCardValueType::LanguageTag => "LANGUAGE-TAG",
VCardValueType::Text => "TEXT",
VCardValueType::Time => "TIME",
VCardValueType::Timestamp => "TIMESTAMP",
VCardValueType::Uri => "URI",
VCardValueType::UtcOffset => "UTC-OFFSET",
}
}
}
impl IanaParse for VCardValueType {
fn parse(value: &[u8]) -> Option<Self> {
hashify::map_ignore_case!(value, VCardValueType,
"BOOLEAN" => VCardValueType::Boolean,
"DATE" => VCardValueType::Date,
"DATE-AND-OR-TIME" => VCardValueType::DateAndOrTime,
"DATE-TIME" => VCardValueType::DateTime,
"FLOAT" => VCardValueType::Float,
"INTEGER" => VCardValueType::Integer,
"LANGUAGE-TAG" => VCardValueType::LanguageTag,
"TEXT" => VCardValueType::Text,
"TIME" => VCardValueType::Time,
"TIMESTAMP" => VCardValueType::Timestamp,
"URI" => VCardValueType::Uri,
"UTC-OFFSET" => VCardValueType::UtcOffset,
)
.copied()
}
}
impl IanaParse for VCardLevel {
fn parse(value: &[u8]) -> Option<Self> {
hashify::map_ignore_case!(value, VCardLevel,
"beginner" => VCardLevel::Beginner,
"average" => VCardLevel::Average,
"expert" => VCardLevel::Expert,
"high" => VCardLevel::High,
"medium" => VCardLevel::Medium,
"low" => VCardLevel::Low,
)
.copied()
}
}
impl IanaString for VCardLevel {
fn as_str(&self) -> &'static str {
match self {
VCardLevel::Beginner => "BEGINNER",
VCardLevel::Average => "AVERAGE",
VCardLevel::Expert => "EXPERT",
VCardLevel::High => "HIGH",
VCardLevel::Medium => "MEDIUM",
VCardLevel::Low => "LOW",
}
}
}
impl IanaParse for VCardPhonetic {
fn parse(value: &[u8]) -> Option<Self> {
hashify::fnc_map_ignore_case!(value,
"ipa" => Some(VCardPhonetic::Ipa),
"jyut" => Some(VCardPhonetic::Jyut),
"piny" => Some(VCardPhonetic::Piny),
"script" => Some(VCardPhonetic::Script),
_ => None,
)
}
}
impl IanaString for VCardPhonetic {
fn as_str(&self) -> &'static str {
match self {
VCardPhonetic::Ipa => "IPA",
VCardPhonetic::Jyut => "JYUT",
VCardPhonetic::Piny => "PINY",
VCardPhonetic::Script => "SCRIPT",
}
}
}
impl IanaParse for VCardType {
fn parse(value: &[u8]) -> Option<Self> {
hashify::fnc_map_ignore_case!(value,
"work" => Some(VCardType::Work),
"home" => Some(VCardType::Home),
"billing" => Some(VCardType::Billing),
"delivery" => Some(VCardType::Delivery),
"contact" => Some(VCardType::Contact),
"acquaintance" => Some(VCardType::Acquaintance),
"friend" => Some(VCardType::Friend),
"met" => Some(VCardType::Met),
"co-worker" => Some(VCardType::CoWorker),
"colleague" => Some(VCardType::Colleague),
"co-resident" => Some(VCardType::CoResident),
"neighbor" => Some(VCardType::Neighbor),
"child" => Some(VCardType::Child),
"parent" => Some(VCardType::Parent),
"sibling" => Some(VCardType::Sibling),
"spouse" => Some(VCardType::Spouse),
"kin" => Some(VCardType::Kin),
"muse" => Some(VCardType::Muse),
"crush" => Some(VCardType::Crush),
"date" => Some(VCardType::Date),
"sweetheart" => Some(VCardType::Sweetheart),
"me" => Some(VCardType::Me),
"agent" => Some(VCardType::Agent),
"emergency" => Some(VCardType::Emergency),
"text" => Some(VCardType::Text),
"voice" => Some(VCardType::Voice),
"fax" => Some(VCardType::Fax),
"cell" => Some(VCardType::Cell),
"video" => Some(VCardType::Video),
"pager" => Some(VCardType::Pager),
"textphone" => Some(VCardType::Textphone),
"main-number" => Some(VCardType::MainNumber),
_ => None,
)
}
}
impl IanaString for VCardType {
fn as_str(&self) -> &'static str {
match self {
VCardType::Work => "WORK",
VCardType::Home => "HOME",
VCardType::Billing => "BILLING",
VCardType::Delivery => "DELIVERY",
VCardType::Contact => "CONTACT",
VCardType::Acquaintance => "ACQUAINTANCE",
VCardType::Friend => "FRIEND",
VCardType::Met => "MET",
VCardType::CoWorker => "CO-WORKER",
VCardType::Colleague => "COLLEAGUE",
VCardType::CoResident => "CO-RESIDENT",
VCardType::Neighbor => "NEIGHBOR",
VCardType::Child => "CHILD",
VCardType::Parent => "PARENT",
VCardType::Sibling => "SIBLING",
VCardType::Spouse => "SPOUSE",
VCardType::Kin => "KIN",
VCardType::Muse => "MUSE",
VCardType::Crush => "CRUSH",
VCardType::Date => "DATE",
VCardType::Sweetheart => "SWEETHEART",
VCardType::Me => "ME",
VCardType::Agent => "AGENT",
VCardType::Emergency => "EMERGENCY",
VCardType::Text => "TEXT",
VCardType::Voice => "VOICE",
VCardType::Fax => "FAX",
VCardType::Cell => "CELL",
VCardType::Video => "VIDEO",
VCardType::Pager => "PAGER",
VCardType::Textphone => "TEXTPHONE",
VCardType::MainNumber => "MAIN-NUMBER",
}
}
}
impl IanaParse for VCardGramGender {
fn parse(value: &[u8]) -> Option<Self> {
hashify::map_ignore_case!(value, VCardGramGender,
"animate" => VCardGramGender::Animate,
"common" => VCardGramGender::Common,
"feminine" => VCardGramGender::Feminine,
"inanimate" => VCardGramGender::Inanimate,
"masculine" => VCardGramGender::Masculine,
"neuter" => VCardGramGender::Neuter,
)
.copied()
}
}
impl IanaString for VCardGramGender {
fn as_str(&self) -> &'static str {
match self {
VCardGramGender::Animate => "ANIMATE",
VCardGramGender::Common => "COMMON",
VCardGramGender::Feminine => "FEMININE",
VCardGramGender::Inanimate => "INANIMATE",
VCardGramGender::Masculine => "MASCULINE",
VCardGramGender::Neuter => "NEUTER",
}
}
}
impl IanaParse for VCardSex {
fn parse(value: &[u8]) -> Option<Self> {
hashify::map_ignore_case!(value, VCardSex,
"M" => VCardSex::Male,
"F" => VCardSex::Female,
"O" => VCardSex::Other,
"N" => VCardSex::NoneOrNotApplicable,
"U" => VCardSex::Unknown,
)
.copied()
}
}
impl IanaString for VCardSex {
fn as_str(&self) -> &'static str {
match self {
VCardSex::Male => "M",
VCardSex::Female => "F",
VCardSex::Other => "O",
VCardSex::NoneOrNotApplicable => "N",
VCardSex::Unknown => "U",
}
}
}
impl IanaParse for VCardKind {
fn parse(value: &[u8]) -> Option<Self> {
hashify::map_ignore_case!(value, VCardKind,
"individual" => VCardKind::Individual,
"group" => VCardKind::Group,
"org" => VCardKind::Org,
"location" => VCardKind::Location,
"application" => VCardKind::Application,
"device" => VCardKind::Device,
)
.copied()
}
}
impl IanaString for VCardKind {
fn as_str(&self) -> &'static str {
match self {
VCardKind::Individual => "INDIVIDUAL",
VCardKind::Group => "GROUP",
VCardKind::Org => "ORG",
VCardKind::Location => "LOCATION",
VCardKind::Application => "APPLICATION",
VCardKind::Device => "DEVICE",
}
}
}
impl VCardParameterName {
pub fn try_parse(input: &[u8]) -> Option<Self> {
hashify::fnc_map_ignore_case!(input,
b"LANGUAGE" => Some(VCardParameterName::Language),
b"VALUE" => Some(VCardParameterName::Value),
b"PREF" => Some(VCardParameterName::Pref),
b"ALTID" => Some(VCardParameterName::Altid),
b"PID" => Some(VCardParameterName::Pid),
b"TYPE" => Some(VCardParameterName::Type),
b"MEDIATYPE" => Some(VCardParameterName::Mediatype),
b"CALSCALE" => Some(VCardParameterName::Calscale),
b"SORT-AS" => Some(VCardParameterName::SortAs),
b"GEO" => Some(VCardParameterName::Geo),
b"TZ" => Some(VCardParameterName::Tz),
b"INDEX" => Some(VCardParameterName::Index),
b"LEVEL" => Some(VCardParameterName::Level),
b"GROUP" => Some(VCardParameterName::Group),
b"CC" => Some(VCardParameterName::Cc),
b"AUTHOR" => Some(VCardParameterName::Author),
b"AUTHOR-NAME" => Some(VCardParameterName::AuthorName),
b"CREATED" => Some(VCardParameterName::Created),
b"DERIVED" => Some(VCardParameterName::Derived),
b"LABEL" => Some(VCardParameterName::Label),
b"PHONETIC" => Some(VCardParameterName::Phonetic),
b"PROP-ID" => Some(VCardParameterName::PropId),
b"SCRIPT" => Some(VCardParameterName::Script),
b"SERVICE-TYPE" => Some(VCardParameterName::ServiceType),
b"USERNAME" => Some(VCardParameterName::Username),
b"JSPTR" => Some(VCardParameterName::Jsptr),
b"JSCOMPS" => Some(VCardParameterName::Jscomps),
_ => None,
)
}
pub fn parse(input: &str) -> Self {
Self::try_parse(input.as_bytes()).unwrap_or_else(|| VCardParameterName::Other(input.into()))
}
pub fn as_str(&self) -> &str {
match self {
VCardParameterName::Language => "LANGUAGE",
VCardParameterName::Value => "VALUE",
VCardParameterName::Pref => "PREF",
VCardParameterName::Altid => "ALTID",
VCardParameterName::Pid => "PID",
VCardParameterName::Type => "TYPE",
VCardParameterName::Mediatype => "MEDIATYPE",
VCardParameterName::Calscale => "CALSCALE",
VCardParameterName::SortAs => "SORT-AS",
VCardParameterName::Geo => "GEO",
VCardParameterName::Tz => "TZ",
VCardParameterName::Index => "INDEX",
VCardParameterName::Level => "LEVEL",
VCardParameterName::Group => "GROUP",
VCardParameterName::Cc => "CC",
VCardParameterName::Author => "AUTHOR",
VCardParameterName::AuthorName => "AUTHOR-NAME",
VCardParameterName::Created => "CREATED",
VCardParameterName::Derived => "DERIVED",
VCardParameterName::Label => "LABEL",
VCardParameterName::Phonetic => "PHONETIC",
VCardParameterName::PropId => "PROP-ID",
VCardParameterName::Script => "SCRIPT",
VCardParameterName::ServiceType => "SERVICE-TYPE",
VCardParameterName::Username => "USERNAME",
VCardParameterName::Jsptr => "JSPTR",
VCardParameterName::Jscomps => "JSCOMPS",
VCardParameterName::Other(name) => name,
}
}
pub fn into_string(self) -> Cow<'static, str> {
match self {
VCardParameterName::Language => "LANGUAGE".into(),
VCardParameterName::Value => "VALUE".into(),
VCardParameterName::Pref => "PREF".into(),
VCardParameterName::Altid => "ALTID".into(),
VCardParameterName::Pid => "PID".into(),
VCardParameterName::Type => "TYPE".into(),
VCardParameterName::Mediatype => "MEDIATYPE".into(),
VCardParameterName::Calscale => "CALSCALE".into(),
VCardParameterName::SortAs => "SORT-AS".into(),
VCardParameterName::Geo => "GEO".into(),
VCardParameterName::Tz => "TZ".into(),
VCardParameterName::Index => "INDEX".into(),
VCardParameterName::Level => "LEVEL".into(),
VCardParameterName::Group => "GROUP".into(),
VCardParameterName::Cc => "CC".into(),
VCardParameterName::Author => "AUTHOR".into(),
VCardParameterName::AuthorName => "AUTHOR-NAME".into(),
VCardParameterName::Created => "CREATED".into(),
VCardParameterName::Derived => "DERIVED".into(),
VCardParameterName::Label => "LABEL".into(),
VCardParameterName::Phonetic => "PHONETIC".into(),
VCardParameterName::PropId => "PROP-ID".into(),
VCardParameterName::Script => "SCRIPT".into(),
VCardParameterName::ServiceType => "SERVICE-TYPE".into(),
VCardParameterName::Username => "USERNAME".into(),
VCardParameterName::Jsptr => "JSPTR".into(),
VCardParameterName::Jscomps => "JSCOMPS".into(),
VCardParameterName::Other(name) => name.into(),
}
}
}
impl VCardVersion {
pub fn try_parse(input: &str) -> Option<Self> {
hashify::map!(input.as_bytes(), VCardVersion,
b"4.0" => VCardVersion::V4_0,
b"3.0" => VCardVersion::V3_0,
b"2.1" => VCardVersion::V2_1,
b"2.0" => VCardVersion::V2_0,
)
.copied()
}
}
+430
View File
@@ -0,0 +1,430 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{VCard, VCardEntry, VCardParameterName, VCardProperty, VCardValue, VCardVersion};
use crate::{
common::{CalendarScale, Data, IanaString, IanaType, PartialDateTime, writer::write_jscomps},
vcard::{
Jscomp, VCardLevel, VCardParameter, VCardParameterValue, VCardPhonetic, VCardType,
VCardValueType, media_type::media_type_from_legacy,
},
};
use encodify::base64;
use std::borrow::Cow;
impl VCard {
pub fn uid(&self) -> Option<&str> {
self.property(&VCardProperty::Uid)
.and_then(|e| e.values.first())
.and_then(|v| v.as_text())
}
pub fn property(&self, prop: &VCardProperty) -> Option<&VCardEntry> {
self.entries.iter().find(|entry| &entry.name == prop)
}
pub fn properties<'x, 'y: 'x>(
&'x self,
prop: &'y VCardProperty,
) -> impl Iterator<Item = &'x VCardEntry> + 'x {
self.entries.iter().filter(move |entry| &entry.name == prop)
}
pub fn version(&self) -> Option<VCardVersion> {
self.entries
.iter()
.find(|e| e.name == VCardProperty::Version)
.and_then(|e| {
e.values
.first()
.and_then(|v| v.as_text())
.and_then(VCardVersion::try_parse)
})
}
pub fn size(&self) -> usize {
self.entries.iter().map(|e| e.size()).sum()
}
}
impl VCardEntry {
#[inline]
pub fn parameters(
&self,
prop: &VCardParameterName,
) -> impl Iterator<Item = &VCardParameterValue> {
self.params.iter().filter_map(move |param| {
if &param.name == prop {
Some(&param.value)
} else {
None
}
})
}
pub fn language(&self) -> Option<&str> {
self.parameters(&VCardParameterName::Language)
.find_map(|v| v.as_text())
}
pub fn alt_id(&self) -> Option<&str> {
self.parameters(&VCardParameterName::Altid)
.find_map(|v| v.as_text())
}
pub fn prop_id(&self) -> Option<&str> {
self.parameters(&VCardParameterName::PropId)
.find_map(|v| v.as_text())
}
pub fn phonetic_system(&self) -> Option<IanaType<&VCardPhonetic, &str>> {
self.parameters(&VCardParameterName::Phonetic)
.find_map(|v| v.as_phonetic())
}
pub fn phonetic_script(&self) -> Option<&str> {
self.parameters(&VCardParameterName::Script)
.find_map(|v| v.as_text())
}
pub fn size(&self) -> usize {
self.group.as_ref().map_or(0, |g| g.len())
+ self.name.as_str().len()
+ self.params.iter().map(|p| p.size()).sum::<usize>()
+ self.values.iter().map(|v| v.size()).sum::<usize>()
}
pub(crate) fn normalize_legacy_media_type(&mut self) {
if self
.values
.iter()
.any(|value| matches!(value, VCardValue::Binary(data) if data.content_type.is_none()))
{
let property = self.name.as_str();
let media_type =
self.params
.iter()
.find_map(|param| match (&param.name, &param.value) {
(VCardParameterName::Mediatype, VCardParameterValue::Text(text)) => {
Some(Cow::Owned(text.to_ascii_lowercase()))
}
(VCardParameterName::Type, VCardParameterValue::Text(text)) => {
media_type_from_legacy(property, text)
}
_ => None,
});
if let Some(media_type) = media_type {
for value in &mut self.values {
if let VCardValue::Binary(data) = value
&& data.content_type.is_none()
{
data.content_type = Some(media_type.to_string());
}
}
}
}
}
}
impl VCardValue {
pub fn as_text(&self) -> Option<&str> {
match self {
VCardValue::Text(v) => v.as_str().into(),
VCardValue::Sex(v) => v.as_str().into(),
VCardValue::GramGender(v) => v.as_str().into(),
VCardValue::Kind(v) => v.as_str().into(),
VCardValue::Component(v) => v.first().map(|s| s.as_str()),
VCardValue::Integer(_)
| VCardValue::Float(_)
| VCardValue::Boolean(_)
| VCardValue::PartialDateTime(_)
| VCardValue::Binary(_) => None,
}
}
pub fn into_text(self) -> Option<Cow<'static, str>> {
match self {
VCardValue::Text(s) => Some(Cow::Owned(s)),
VCardValue::Sex(v) => Some(Cow::Borrowed(v.as_str())),
VCardValue::GramGender(v) => Some(Cow::Borrowed(v.as_str())),
VCardValue::Kind(v) => Some(Cow::Borrowed(v.as_str())),
VCardValue::Component(v) => Some(Cow::Owned(v.join(","))),
VCardValue::Integer(_)
| VCardValue::Float(_)
| VCardValue::Boolean(_)
| VCardValue::PartialDateTime(_)
| VCardValue::Binary(_) => None,
}
}
pub fn into_uri(self) -> Option<String> {
match self {
VCardValue::Binary(v) => Some(v.to_unwrapped_string()),
VCardValue::Text(v) => Some(v),
_ => None,
}
}
pub fn as_integer(&self) -> Option<i64> {
match self {
VCardValue::Integer(i) => Some(*i),
_ => None,
}
}
pub fn as_float(&self) -> Option<f64> {
match self {
VCardValue::Float(f) => Some(*f),
_ => None,
}
}
pub fn as_boolean(&self) -> Option<bool> {
match self {
VCardValue::Boolean(b) => Some(*b),
_ => None,
}
}
pub fn as_partial_date_time(&self) -> Option<&PartialDateTime> {
match self {
VCardValue::PartialDateTime(dt) => Some(dt),
_ => None,
}
}
pub fn as_binary(&self) -> Option<&Data> {
match self {
VCardValue::Binary(d) => Some(d),
_ => None,
}
}
pub fn size(&self) -> usize {
match self {
VCardValue::Text(v) => v.len(),
VCardValue::Sex(v) => v.as_str().len(),
VCardValue::GramGender(v) => v.as_str().len(),
VCardValue::Kind(v) => v.as_str().len(),
VCardValue::Component(v) => v.iter().map(|s| s.len()).sum::<usize>(),
VCardValue::Integer(_) => std::mem::size_of::<i64>(),
VCardValue::Float(_) => std::mem::size_of::<f64>(),
VCardValue::Boolean(_) => std::mem::size_of::<bool>(),
VCardValue::PartialDateTime(_) => std::mem::size_of::<PartialDateTime>(),
VCardValue::Binary(d) => {
d.content_type.as_ref().map_or(0, |ct| ct.len()) + d.data.len()
}
}
}
}
impl Data {
pub fn to_unwrapped_string(&self) -> String {
const SCHEME: &str = "data:";
const BASE64_MARKER: &str = ";base64,";
let media_type = self.content_type.as_deref().unwrap_or_default();
let mut out = String::with_capacity(
SCHEME.len()
+ media_type.len()
+ BASE64_MARKER.len()
+ base64::STANDARD.encoded_len(self.data.len()),
);
out.push_str(SCHEME);
out.push_str(media_type);
out.push_str(BASE64_MARKER);
base64::STANDARD.encode_append(&self.data, &mut out);
out
}
}
impl VCardParameterValue {
pub fn into_text(self) -> Cow<'static, str> {
match self {
VCardParameterValue::Text(s) => Cow::Owned(s),
VCardParameterValue::ValueType(v) => Cow::Borrowed(v.as_str()),
VCardParameterValue::Type(v) => Cow::Borrowed(v.as_str()),
VCardParameterValue::Calscale(v) => Cow::Borrowed(v.as_str()),
VCardParameterValue::Level(v) => Cow::Borrowed(v.as_str()),
VCardParameterValue::Phonetic(v) => Cow::Borrowed(v.as_str()),
VCardParameterValue::Jscomps(v) => {
let mut jscomps = String::new();
let _ = write_jscomps(&mut jscomps, &v);
Cow::Owned(jscomps)
}
VCardParameterValue::Integer(i) => Cow::Owned(i.to_string()),
VCardParameterValue::Timestamp(t) => Cow::Owned(t.to_string()),
VCardParameterValue::Bool(b) => Cow::Borrowed(if b { "true" } else { "false" }),
VCardParameterValue::Null => Cow::Borrowed(""),
}
}
pub fn as_text(&self) -> Option<&str> {
match self {
VCardParameterValue::Text(v) => Some(v.as_str()),
VCardParameterValue::ValueType(v) => Some(v.as_str()),
VCardParameterValue::Type(v) => Some(v.as_str()),
VCardParameterValue::Calscale(v) => Some(v.as_str()),
VCardParameterValue::Level(v) => Some(v.as_str()),
VCardParameterValue::Phonetic(v) => Some(v.as_str()),
VCardParameterValue::Jscomps(_)
| VCardParameterValue::Integer(_)
| VCardParameterValue::Timestamp(_)
| VCardParameterValue::Bool(_)
| VCardParameterValue::Null => None,
}
}
pub fn into_phonetic(self) -> Option<IanaType<VCardPhonetic, String>> {
match self {
VCardParameterValue::Phonetic(v) => Some(IanaType::Iana(v)),
VCardParameterValue::Text(v) => Some(IanaType::Other(v)),
_ => None,
}
}
pub fn as_phonetic(&self) -> Option<IanaType<&VCardPhonetic, &str>> {
match self {
VCardParameterValue::Phonetic(v) => Some(IanaType::Iana(v)),
VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn into_calscale(self) -> Option<IanaType<CalendarScale, String>> {
match self {
VCardParameterValue::Calscale(v) => Some(IanaType::Iana(v)),
VCardParameterValue::Text(v) => Some(IanaType::Other(v)),
_ => None,
}
}
pub fn as_calscale(&self) -> Option<IanaType<&CalendarScale, &str>> {
match self {
VCardParameterValue::Calscale(v) => Some(IanaType::Iana(v)),
VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn into_level(self) -> Option<IanaType<VCardLevel, String>> {
match self {
VCardParameterValue::Level(v) => Some(IanaType::Iana(v)),
VCardParameterValue::Text(v) => Some(IanaType::Other(v)),
_ => None,
}
}
pub fn as_level(&self) -> Option<IanaType<&VCardLevel, &str>> {
match self {
VCardParameterValue::Level(v) => Some(IanaType::Iana(v)),
VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn into_type(self) -> Option<IanaType<VCardType, String>> {
match self {
VCardParameterValue::Type(v) => Some(IanaType::Iana(v)),
VCardParameterValue::Text(v) => Some(IanaType::Other(v)),
_ => None,
}
}
pub fn as_type(&self) -> Option<IanaType<&VCardType, &str>> {
match self {
VCardParameterValue::Type(v) => Some(IanaType::Iana(v)),
VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn into_value_type(self) -> Option<IanaType<VCardValueType, String>> {
match self {
VCardParameterValue::ValueType(v) => Some(IanaType::Iana(v)),
VCardParameterValue::Text(v) => Some(IanaType::Other(v)),
_ => None,
}
}
pub fn as_value_type(&self) -> Option<IanaType<&VCardValueType, &str>> {
match self {
VCardParameterValue::ValueType(v) => Some(IanaType::Iana(v)),
VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn as_bool(&self) -> Option<IanaType<bool, &str>> {
match self {
VCardParameterValue::Bool(b) => Some(IanaType::Iana(*b)),
VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn as_integer(&self) -> Option<IanaType<u32, &str>> {
match self {
VCardParameterValue::Integer(i) => Some(IanaType::Iana(*i)),
VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn into_timestamp(self) -> Option<IanaType<i64, String>> {
match self {
VCardParameterValue::Timestamp(t) => Some(IanaType::Iana(t)),
VCardParameterValue::Text(v) => Some(IanaType::Other(v)),
_ => None,
}
}
pub fn as_timestamp(&self) -> Option<IanaType<i64, &str>> {
match self {
VCardParameterValue::Timestamp(t) => Some(IanaType::Iana(*t)),
VCardParameterValue::Text(v) => Some(IanaType::Other(v.as_str())),
_ => None,
}
}
pub fn into_jscomps(self) -> Option<Vec<Jscomp>> {
match self {
VCardParameterValue::Jscomps(v) => Some(v),
_ => None,
}
}
pub fn size(&self) -> usize {
match self {
VCardParameterValue::Text(v) => v.len(),
VCardParameterValue::ValueType(v) => v.as_str().len(),
VCardParameterValue::Type(v) => v.as_str().len(),
VCardParameterValue::Calscale(v) => v.as_str().len(),
VCardParameterValue::Level(v) => v.as_str().len(),
VCardParameterValue::Phonetic(v) => v.as_str().len(),
VCardParameterValue::Jscomps(v) => v.iter().map(|j| j.size()).sum::<usize>(),
VCardParameterValue::Integer(_) => std::mem::size_of::<u32>(),
VCardParameterValue::Timestamp(_) => std::mem::size_of::<i64>(),
VCardParameterValue::Bool(_) => std::mem::size_of::<bool>(),
VCardParameterValue::Null => 0,
}
}
}
impl VCardParameter {
pub fn size(&self) -> usize {
self.name.as_str().len() + self.value.size()
}
}
impl Jscomp {
pub fn size(&self) -> usize {
match self {
Jscomp::Entry { .. } => std::mem::size_of::<Jscomp>(),
Jscomp::Separator(value) => value.len(),
}
}
}
+573
View File
@@ -0,0 +1,573 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{PartialDateTime, VCard, VCardEntry, VCardValueType, VCardVersion};
use crate::{
common::{
IanaString,
format::{AsciiPush, BufferedWrite},
parser::Timestamp,
writer::{
DOCUMENT_BUFFER, ENTRY_BUFFER, FoldingWriter, LineWriter, NeedsQuotes, write_jscomps,
write_param_value, write_text,
},
},
vcard::{
VCardParameterName, VCardParameterValue, VCardProperty, VCardValue, ValueSeparator,
media_type::legacy_media_type,
},
};
use std::fmt::{self, Display, Write};
pub(crate) const DATE_BUFFER: usize = 48;
impl VCardVersion {
pub(crate) fn begin_and_version(self) -> &'static str {
match self {
VCardVersion::V2_0 => "BEGIN:VCARD\r\nVERSION:2.0\r\n",
VCardVersion::V2_1 => "BEGIN:VCARD\r\nVERSION:2.1\r\n",
VCardVersion::V3_0 => "BEGIN:VCARD\r\nVERSION:3.0\r\n",
VCardVersion::V4_0 => "BEGIN:VCARD\r\nVERSION:4.0\r\n",
}
}
}
impl VCard {
pub fn write_to(&self, out: &mut impl Write, version: VCardVersion) -> fmt::Result {
out.write_buffered::<DOCUMENT_BUFFER>(|buf| {
self.write_entries(&mut FoldingWriter::new(buf), version)
})
}
fn write_entries<W: Write + AsciiPush + ?Sized>(
&self,
out: &mut FoldingWriter<'_, W>,
version: VCardVersion,
) -> fmt::Result {
out.write_raw(version.begin_and_version())?;
for entry in &self.entries {
if !matches!(
entry.name,
VCardProperty::Begin | VCardProperty::End | VCardProperty::Version
) {
entry.write_line(out, version)?;
}
}
out.write_raw("END:VCARD\r\n")
}
}
impl VCardEntry {
#[deprecated(since = "0.4.0", note = "use write_with_version")]
pub fn write_to(&self, out: &mut impl Write, is_v4: bool) -> fmt::Result {
self.write_with_version(
out,
if is_v4 {
VCardVersion::V4_0
} else {
VCardVersion::V3_0
},
)
}
pub fn write_with_version(&self, out: &mut impl Write, version: VCardVersion) -> fmt::Result {
out.write_buffered::<ENTRY_BUFFER>(|buf| {
self.write_line(&mut FoldingWriter::new(buf), version)
})
}
pub(crate) fn write_line<W: Write + AsciiPush + ?Sized>(
&self,
out: &mut FoldingWriter<'_, W>,
version: VCardVersion,
) -> fmt::Result {
let is_v4 = matches!(version, VCardVersion::V4_0);
if let Some(group_name) = &self.group {
out.write_atomic(group_name)?;
out.write_atomic(".")?;
}
out.write_atomic(self.name.as_str())?;
let mut types = None;
let mut last_param: Option<&VCardParameterName> = None;
let legacy_binary = if is_v4 {
None
} else {
self.values.iter().find_map(|v| match v {
VCardValue::Binary(data) => Some(data),
_ => None,
})
};
for param in &self.params {
if !is_v4
&& match param.name {
VCardParameterName::PropId | VCardParameterName::Jscomps => true,
VCardParameterName::Mediatype => legacy_binary.is_some(),
_ => false,
}
{
continue;
}
if last_param.is_some_and(|last_param| last_param == &param.name) {
out.write_atomic(",")?;
} else {
out.write_atomic(";")?;
out.write_atomic(param.name.as_str())?;
if !matches!(param.value, VCardParameterValue::Null) {
out.write_atomic("=")?;
}
last_param = Some(&param.name);
}
match &param.value {
VCardParameterValue::Text(v)
if param.name == VCardParameterName::Jsptr && !v.needs_quotes() =>
{
out.write_atomic("\"")?;
write_param_value(out, v, is_v4)?;
out.write_atomic("\"")?;
}
VCardParameterValue::Text(v) => {
write_param_value(out, v, is_v4)?;
}
VCardParameterValue::Integer(i) => {
out.push_u64((*i).into())?;
}
VCardParameterValue::Timestamp(v) => {
Timestamp(*v).push_to(out)?;
}
VCardParameterValue::Bool(v) => {
out.write_atomic(if *v { "TRUE" } else { "FALSE" })?;
}
VCardParameterValue::ValueType(v) => {
if types.is_none() {
types = Some(v);
}
write_param_value(out, v.as_str(), is_v4)?;
}
VCardParameterValue::Type(v) => {
write_param_value(out, v.as_str(), is_v4)?;
}
VCardParameterValue::Calscale(v) => {
write_param_value(out, v.as_str(), is_v4)?;
}
VCardParameterValue::Level(v) => {
write_param_value(out, v.as_str(), is_v4)?;
}
VCardParameterValue::Phonetic(v) => {
write_param_value(out, v.as_str(), is_v4)?;
}
VCardParameterValue::Jscomps(v) => {
out.write_atomic("\"")?;
write_jscomps(out, v)?;
out.write_atomic("\"")?;
last_param = None;
}
VCardParameterValue::Null => {
last_param = None;
}
}
}
let is_v21_base64 = matches!(version, VCardVersion::V2_1) && legacy_binary.is_some();
let max_values = if is_v4 {
usize::MAX
} else {
self.name.legacy_max_values()
};
if !is_v4 {
if let Some(data) = legacy_binary {
out.write_atomic(if is_v21_base64 {
";ENCODING=BASE64"
} else {
";ENCODING=b"
})?;
if let Some(media_type) = data.content_type.as_deref()
&& !self
.params
.iter()
.any(|param| param.name == VCardParameterName::Type)
&& let Some(token) = legacy_media_type(self.name.as_str(), media_type)
{
out.write_atomic(";TYPE=")?;
out.write_atomic(&token)?;
}
}
if self.values.iter().take(max_values).any(|v| match v {
VCardValue::Text(s) => !s.is_ascii(),
VCardValue::Component(items) => items.iter().any(|s| !s.is_ascii()),
_ => false,
}) {
out.write_atomic(";CHARSET=UTF-8")?;
}
}
out.write_atomic(":")?;
let (default_type, value_separator) = self.name.default_types();
let default_type = default_type.unwrap_vcard();
let mut separator = ";";
let mut escape_semicolon = matches!(types.unwrap_or(&default_type), VCardValueType::Text);
let mut escape_comma = escape_semicolon;
match value_separator {
ValueSeparator::Comma => {
escape_comma = true;
separator = ",";
}
ValueSeparator::Semicolon => escape_semicolon = true,
ValueSeparator::SemicolonAndComma => {
escape_semicolon = true;
escape_comma = true;
}
_ => {}
}
for (pos, value) in self.values.iter().take(max_values).enumerate() {
if pos > 0 {
out.write_atomic(separator)?;
}
match value {
VCardValue::Text(v) => {
write_text(out, v, escape_semicolon, escape_comma)?;
}
VCardValue::Component(v) => {
for (pos, item) in v.iter().enumerate() {
if pos > 0 {
out.write_atomic(",")?;
}
write_text(out, item, true, true)?;
}
}
VCardValue::Integer(v) => {
out.push_i64(*v)?;
}
VCardValue::Float(v) => {
write!(out, "{v}")?;
}
VCardValue::Boolean(v) => {
out.write_atomic(if *v { "TRUE" } else { "FALSE" })?;
}
VCardValue::PartialDateTime(v) => {
let typ = if pos == 0 {
types
} else {
self.parameters(&VCardParameterName::Value)
.nth(pos)
.and_then(|v| v.as_value_type())
.and_then(|v| v.iana().copied())
}
.unwrap_or(&default_type);
if is_v4 {
v.push_vcard(out, typ)?;
} else {
v.push_legacy_vcard(out, typ)?;
}
}
VCardValue::Binary(v) => {
if is_v4 {
let media_type = v.content_type.as_deref().unwrap_or_default();
out.write_str("data:")?;
out.write_str(media_type)?;
if escape_semicolon {
out.write_atomic("\\;")?;
} else {
out.write_str(";")?;
}
out.write_atomic("base64\\,")?;
}
out.write_base64(&v.data)?;
}
VCardValue::Sex(v) => {
out.write_atomic(v.as_str())?;
}
VCardValue::GramGender(v) => {
out.write_atomic(v.as_str())?;
}
VCardValue::Kind(v) => {
out.write_atomic(v.as_str())?;
}
}
}
out.end_line()?;
if is_v21_base64 {
out.end_line()?;
}
Ok(())
}
}
impl PartialDateTime {
pub fn format_as_vcard(&self, out: &mut impl Write, fmt: &VCardValueType) -> fmt::Result {
out.write_buffered::<DATE_BUFFER>(|buf| self.push_vcard(buf, fmt))
}
pub fn format_as_legacy_vcard(
&self,
out: &mut impl Write,
fmt: &VCardValueType,
) -> fmt::Result {
out.write_buffered::<DATE_BUFFER>(|buf| self.push_legacy_vcard(buf, fmt))
}
pub(crate) fn push_vcard(&self, buf: &mut impl AsciiPush, fmt: &VCardValueType) -> fmt::Result {
if matches!(fmt, VCardValueType::Timestamp) {
buf.push_4_digits(self.year.unwrap_or_default())?;
buf.push_2_digits(self.month.unwrap_or_default())?;
buf.push_2_digits(self.day.unwrap_or_default())?;
buf.push_byte(b'T')?;
buf.push_2_digits(self.hour.unwrap_or_default())?;
buf.push_2_digits(self.minute.unwrap_or_default())?;
buf.push_2_digits(self.second.unwrap_or_default())?;
return self.push_timestamp_zone(buf);
}
let missing_time = self.hour.is_none() && self.minute.is_none() && self.second.is_none();
let missing_tz = self.tz_hour.is_none();
if matches!(
fmt,
VCardValueType::Date | VCardValueType::DateAndOrTime | VCardValueType::DateTime
) {
match (self.year, self.month, self.day) {
(Some(year), Some(month), Some(day)) => {
buf.push_4_digits(year)?;
buf.push_2_digits(month)?;
buf.push_2_digits(day)?;
}
(Some(year), Some(month), None) => {
buf.push_4_digits(year)?;
if missing_time && missing_tz {
buf.push_byte(b'-')?;
}
buf.push_2_digits(month)?;
}
(None, Some(month), Some(day)) => {
buf.push_ascii(b"--")?;
buf.push_2_digits(month)?;
buf.push_2_digits(day)?;
}
(None, None, Some(day)) => {
buf.push_ascii(b"---")?;
buf.push_2_digits(day)?;
}
(Some(year), None, None) => {
buf.push_4_digits(year)?;
}
(None, Some(month), None) => {
buf.push_ascii(b"--")?;
buf.push_u64(month.into())?;
}
_ => {}
}
}
if matches!(
fmt,
VCardValueType::DateAndOrTime | VCardValueType::DateTime | VCardValueType::Time
) && !missing_time
{
if matches!(
fmt,
VCardValueType::DateAndOrTime | VCardValueType::DateTime
) {
buf.push_byte(b'T')?;
}
let mut last_is_some = false;
for value in [self.hour, self.minute, self.second] {
if let Some(value) = value {
buf.push_2_digits(value)?;
last_is_some = true;
} else if !last_is_some {
buf.push_byte(b'-')?;
}
}
}
if matches!(
fmt,
VCardValueType::DateAndOrTime
| VCardValueType::DateTime
| VCardValueType::Time
| VCardValueType::UtcOffset
) {
match (self.tz_hour, self.tz_minute) {
(Some(0), Some(0)) | (Some(0), _) => {
buf.push_byte(b'Z')?;
}
(Some(hour), Some(minute)) => {
buf.push_byte(if self.tz_minus { b'-' } else { b'+' })?;
buf.push_2_digits(hour)?;
buf.push_2_digits(minute)?;
}
(Some(hour), None) => {
buf.push_byte(if self.tz_minus { b'-' } else { b'+' })?;
buf.push_2_digits(hour)?;
}
_ => {}
}
}
Ok(())
}
pub(crate) fn push_legacy_vcard(
&self,
buf: &mut impl AsciiPush,
fmt: &VCardValueType,
) -> fmt::Result {
if matches!(fmt, VCardValueType::Timestamp) {
buf.push_4_digits(self.year.unwrap_or_default())?;
buf.push_2_digits(self.month.unwrap_or_default())?;
buf.push_2_digits(self.day.unwrap_or_default())?;
if self.hour.is_some() {
buf.push_byte(b'T')?;
buf.push_2_digits(self.hour.unwrap_or_default())?;
buf.push_2_digits(self.minute.unwrap_or_default())?;
buf.push_2_digits(self.second.unwrap_or_default())?;
}
return self.push_timestamp_zone(buf);
}
let missing_time = self.hour.is_none() && self.minute.is_none() && self.second.is_none();
if matches!(
fmt,
VCardValueType::Date | VCardValueType::DateAndOrTime | VCardValueType::DateTime
) {
match (self.year, self.month, self.day) {
(Some(year), Some(month), Some(day)) => {
buf.push_4_digits(year)?;
buf.push_byte(b'-')?;
buf.push_2_digits(month)?;
buf.push_byte(b'-')?;
buf.push_2_digits(day)?;
}
(Some(year), Some(month), None) => {
buf.push_4_digits(year)?;
buf.push_byte(b'-')?;
buf.push_2_digits(month)?;
}
(None, Some(month), Some(day)) => {
buf.push_ascii(b"--")?;
buf.push_2_digits(month)?;
buf.push_byte(b'-')?;
buf.push_2_digits(day)?;
}
(None, None, Some(day)) => {
buf.push_ascii(b"---")?;
buf.push_2_digits(day)?;
}
(Some(year), None, None) => {
buf.push_4_digits(year)?;
}
(None, Some(month), None) => {
buf.push_ascii(b"--")?;
buf.push_u64(month.into())?;
}
_ => {}
}
}
if matches!(
fmt,
VCardValueType::DateAndOrTime | VCardValueType::DateTime | VCardValueType::Time
) && !missing_time
{
if matches!(
fmt,
VCardValueType::DateAndOrTime | VCardValueType::DateTime
) {
buf.push_byte(b'T')?;
}
let mut last_is_some = false;
for value in [self.hour, self.minute, self.second] {
if let Some(value) = value {
if last_is_some {
buf.push_byte(b':')?;
}
buf.push_2_digits(value)?;
last_is_some = true;
} else if !last_is_some {
buf.push_byte(b'-')?;
}
}
}
if matches!(
fmt,
VCardValueType::DateAndOrTime
| VCardValueType::DateTime
| VCardValueType::Time
| VCardValueType::UtcOffset
) {
match (self.tz_hour, self.tz_minute) {
(Some(0), Some(0)) | (Some(0), _) => {
buf.push_byte(b'Z')?;
}
(Some(hour), Some(minute)) => {
buf.push_byte(if self.tz_minus { b'-' } else { b'+' })?;
buf.push_2_digits(hour)?;
buf.push_byte(b':')?;
buf.push_2_digits(minute)?;
}
(Some(hour), None) => {
buf.push_byte(if self.tz_minus { b'-' } else { b'+' })?;
buf.push_2_digits(hour)?;
}
_ => {}
}
}
Ok(())
}
fn push_timestamp_zone(&self, buf: &mut impl AsciiPush) -> fmt::Result {
if let Some(tz_hour) = self.tz_hour {
let tz_minute = self.tz_minute.unwrap_or_default();
if tz_hour == 0 && tz_minute == 0 {
buf.push_byte(b'Z')?;
} else {
buf.push_byte(if self.tz_minus { b'-' } else { b'+' })?;
buf.push_2_digits(tz_hour)?;
if let Some(tz_minute) = self.tz_minute {
buf.push_2_digits(tz_minute)?;
}
}
}
Ok(())
}
}
impl Display for VCard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.write_to(f, self.version().unwrap_or_default())
}
}
impl Display for VCardVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
VCardVersion::V2_0 => "2.0",
VCardVersion::V2_1 => "2.1",
VCardVersion::V3_0 => "3.0",
VCardVersion::V4_0 => "4.0",
})
}
}
+52 -40
View File
@@ -1,6 +1,6 @@
[package]
name = "common"
version = "0.16.27"
version = "1.0.0"
edition = "2024"
build = "build.rs"
@@ -15,42 +15,46 @@ types = { path = "../types" }
registry = { path = "../registry" }
inbuxa-features = { path = "../features" }
jmap_proto = { path = "../jmap-proto" }
sieve-rs = { version = "0.7", features = ["rkyv", "serde"] }
mail-parser = { version = "0.11", features = ["full_encoding"] }
mail-builder = { version = "1.0" }
mail-auth = { version = "0.13", features = ["generate", "arc"] }
smtp-proto = { version = "0.2.3", features = ["rkyv"] }
sieve-rs = { path = "../sieve", features = ["serde"] }
mail-parser = { version = "1.0", features = ["full_encoding"] }
mail-builder = { version = "2.0" }
mail-auth = { version = "0.14", features = ["generate", "arc"] }
flate2 = "1.1"
smtp-report = { path = "../smtp-report" }
smtp-proto = { path = "../smtp-proto", features = ["rkyv"] }
dns-update = { version = "0.6" }
calcard = { version = "0.3", features = ["rkyv"] }
calcard = { path = "../calcard", features = ["rkyv"] }
ahash = { version = "0.8.12", features = ["serde"] }
parking_lot = "0.12.5"
regex = "1.13.1"
proxy-header = { version = "0.1.2", features = ["tokio"] }
arc-swap = "1.9.2"
rustls = { version = "0.23.43", default-features = false, features = ["std", "aws_lc_rs", "tls12"] }
parking_lot = "0.13.0"
proxy-header = { version = "0.1.3", features = ["tokio"] }
arc-swap = "1.9.3"
rustls = { version = "0.23.46", default-features = false, features = ["std", "aws_lc_rs", "tls12"] }
rustls-pemfile = "2.2"
rustls-pki-types = { version = "1" }
aws-lc-rs = { version = "1" }
tokio = { version = "1.53", features = ["net", "macros"] }
tokio = { version = "1.54", features = ["net", "macros", "sync", "time"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["aws_lc_rs", "tls12"] }
socket2 = { version = "0.6", features = ["all"] }
futures = "0.3"
rcgen = { version = "0.14", default-features = false, features = ["crypto", "pem", "aws_lc_rs"] }
reqwest = { version = "0.13", default-features = false, features = ["rustls", "http2", "stream"]}
serde = { version = "1.0", features = ["derive"]}
serde_json = "1.0"
base64 = "0.23"
serde_json = { version = "1.0", features = ["raw_value"] }
encodify = "1.0"
x509-parser = "0.18"
pem = "4.0"
chrono = { version = "0.4", features = ["serde"] }
hyper = { version = "1.11.1", features = ["server", "http1", "http2"] }
opentelemetry = { git = "https://github.com/stalwartlabs/opentelemetry-rust" }
opentelemetry_sdk = { git = "https://github.com/stalwartlabs/opentelemetry-rust" }
opentelemetry-otlp = { git = "https://github.com/stalwartlabs/opentelemetry-rust", default-features = false, features = ["reqwest-client", "http-proto", "trace", "metrics", "logs", "internal-logs", "grpc-tonic", "tls-aws-lc", "tls-roots", "reqwest-rustls"] }
opentelemetry-semantic-conventions = { git = "https://github.com/stalwartlabs/opentelemetry-rust" }
jiff = "0.2.38"
hyper = { version = "1.12.0", features = ["server", "http1", "http2"] }
# inbuxa: the opentelemetry crates come from vendor/ (root [patch.crates-io])
opentelemetry = { version = "0.33" }
opentelemetry_sdk = { version = "0.33" }
opentelemetry-otlp = { version = "0.33", default-features = false, features = ["reqwest-client", "http-proto", "trace", "metrics", "logs", "internal-logs", "grpc-tonic", "tls-aws-lc", "tls-roots", "reqwest-rustls"] }
opentelemetry-semantic-conventions = { version = "0.33" }
prometheus = { version = "0.14", default-features = false }
imagesize = "0.15"
sha1 = "0.11"
sha2 = "0.11"
siphasher = "1"
md5 = "0.8.1"
whatlang = "0.18"
idna = "1.1"
@@ -59,47 +63,55 @@ unicode-security = "0.1.2"
infer = "0.22"
bincode = { version = "2.0.1", features = ["serde"] }
hostname = "0.4.2"
zip = "8.6"
xxhash-rust = { version = "0.8.18", features = ["xxh3"] }
zip = "9.1"
xxhash-rust = { version = "0.8.19", features = ["xxh3"] }
psl = "2"
aes-gcm-siv = "0.12.1"
jsonwebtoken = { version = "11.0.0", features = ["aws_lc_rs"] }
jsonwebtoken = { version = "11.1.0", features = ["aws_lc_rs"] }
rsa = "0.9.10"
p256 = { version = "0.13", features = ["ecdh", "ecdsa", "pkcs8", "pem"] }
p384 = { version = "0.13", features = ["ecdh"] }
hashify = "0.2"
rkyv = { version = "0.8.18", features = ["little_endian"] }
tinyvec = { version = "1.12.0", features = ["alloc"] }
p256 = { version = "0.14", features = ["ecdh", "ecdsa", "pkcs8", "pem"] }
p384 = { version = "0.14", features = ["ecdh"] }
hashify = "0.3"
rkyv.workspace = true
tinyvec = { version = "1.13.3", features = ["alloc"] }
compact_str = { version = "0.10.0", features = ["rkyv", "serde"] }
bytes = "1"
lz4_flex = { version = "0.14", features = ["frame"], default-features = false }
hickory-proto = "0.26.3"
nohash-hasher = "0.2.0"
quick_cache = "0.7"
rasn = "0.28"
rasn-pkix = "0.28"
rasn = "0.29"
rasn-pkix = "0.29"
sequoia-openpgp = { version = "2.4", default-features = false, features = ["crypto-rust", "allow-experimental-crypto", "allow-variable-time-crypto"] }
zxcvbn = "3.1.1"
pkcs8 = { version = "0.10.2", features = ["alloc", "std"] }
quick-xml = "0.41"
pkcs8 = { version = "0.11.0", features = ["alloc", "std"] }
quick-xml = "0.42"
percent-encoding = "2.3.2"
bumpalo = { version = "3.20.3", features = ["collections"] }
regex-automata = { version = "0.4.18", default-features = false, features = ["std", "syntax", "meta", "nfa-pikevm", "nfa-backtrack", "hybrid", "dfa-onepass", "perf-inline", "perf-literal", "unicode"] }
memchr = "2.8.3"
itoa = "1.0.18"
zmij = "1.0.23"
[target.'cfg(unix)'.dependencies]
privdrop = "0.5.6"
libc = "0.2.189"
privdrop = "0.5.7"
libc = "0.2.191"
[target.'cfg(target_os = "linux")'.dependencies]
sd-notify = "0.5"
[target.'cfg(windows)'.dependencies]
socket2 = "0.6"
[target.'cfg(inbuxa_tests)'.dependencies]
dns-update = { version = "0.6", features = ["test_provider"] }
[features]
test_mode = []
dev_mode = []
enterprise = []
enterprise = ["store/enterprise", "utils/enterprise", "trc/enterprise", "directory/enterprise", "coordinator/enterprise", "registry/enterprise", "jmap_proto/enterprise"]
foundation = []
redis = ["store/redis"]
[dev-dependencies]
tokio = { version = "1.53", features = ["full"] }
tokio = { version = "1.54", features = ["full"] }
regex = "1.13.1"
[lints]
workspace = true
+12 -14
View File
@@ -1,4 +1,4 @@
use std::collections::{HashMap, HashSet};
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::env;
use std::fs;
use std::path::Path;
@@ -23,8 +23,8 @@ fn main() {
println!("cargo:rerun-if-changed={}", yaml_path.display());
}
fn parse_yaml(content: &str) -> HashMap<String, HashMap<String, String>> {
let mut result: HashMap<String, HashMap<String, String>> = HashMap::new();
fn parse_yaml(content: &str) -> BTreeMap<String, BTreeMap<String, String>> {
let mut result: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
let mut current_key = None;
for line in content.lines() {
@@ -78,7 +78,7 @@ fn split_plural_forms(value: &str) -> Option<Vec<(&str, &str)>> {
.collect()
}
fn plural_keys(locales: &HashMap<String, HashMap<String, String>>) -> HashSet<String> {
fn plural_keys(locales: &BTreeMap<String, BTreeMap<String, String>>) -> HashSet<String> {
let mut keys = HashSet::new();
for (key, translations) in locales {
@@ -132,7 +132,7 @@ fn plural_forms_literal(value: &str) -> String {
literal
}
fn generate_locale_code(locales: &HashMap<String, HashMap<String, String>>) -> String {
fn generate_locale_code(locales: &BTreeMap<String, BTreeMap<String, String>>) -> String {
let mut code = String::new();
let plural = plural_keys(locales);
@@ -159,7 +159,7 @@ fn generate_locale_code(locales: &HashMap<String, HashMap<String, String>>) -> S
code.push_str("}\n\n");
let mut languages = std::collections::HashSet::new();
let mut languages = BTreeSet::new();
for translations in locales.values() {
for lang in translations.keys() {
languages.insert(lang.clone());
@@ -188,19 +188,17 @@ fn generate_locale_code(locales: &HashMap<String, HashMap<String, String>>) -> S
code.push_str("};\n\n");
}
let mut sorted: Vec<&String> = languages.iter().collect();
sorted.sort_unstable();
code.push_str(&format!(
"pub static ALL_LOCALES: [&Locale; {}] = [\n",
sorted.len()
languages.len()
));
for lang in &sorted {
for lang in &languages {
code.push_str(&format!(" &{}_LOCALES,\n", const_name(lang)));
}
code.push_str("];\n\n");
code.push_str("pub fn locale(name: &str) -> Option<&'static Locale> {\n");
code.push_str(" hashify::tiny_map_ignore_case!(name.as_bytes(),\n");
code.push_str(" hashify::map_ignore_case!(name.as_bytes(), &'static Locale,\n");
for lang in &languages {
code.push_str(&format!(
" \"{}\" => &{}_LOCALES,\n",
@@ -208,7 +206,7 @@ fn generate_locale_code(locales: &HashMap<String, HashMap<String, String>>) -> S
const_name(lang)
));
}
code.push_str(" )\n");
code.push_str(" )\n .copied()\n");
code.push_str("}\n\n");
// Maps a bare language tag onto the regional locale shipped for it
@@ -220,7 +218,7 @@ fn generate_locale_code(locales: &HashMap<String, HashMap<String, String>>) -> S
by_language.dedup_by_key(|(language, _)| *language);
code.push_str("pub fn locale_by_language(language: &str) -> Option<&'static Locale> {\n");
code.push_str(" hashify::tiny_map_ignore_case!(language.as_bytes(),\n");
code.push_str(" hashify::map_ignore_case!(language.as_bytes(), &'static Locale,\n");
for (language, lang) in by_language {
code.push_str(&format!(
" \"{}\" => &{}_LOCALES,\n",
@@ -228,7 +226,7 @@ fn generate_locale_code(locales: &HashMap<String, HashMap<String, String>>) -> S
const_name(lang)
));
}
code.push_str(" )\n");
code.push_str(" )\n .copied()\n");
code.push_str("}\n");
code
}
File diff suppressed because it is too large. Load diff
+69
View File
@@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::sync::atomic::{AtomicU64, Ordering};
const VALUE_MASK: u64 = u32::MAX as u64;
const GENERATION: u64 = 1 << u32::BITS;
const UNKNOWN: u32 = u32::MAX;
const INACTIVE: u32 = u32::MAX - 1;
#[derive(Debug)]
pub struct ActiveScriptSlot(AtomicU64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CachedActiveScript {
Known(Option<u32>),
Unknown(ActiveScriptTicket),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ActiveScriptTicket(u64);
impl ActiveScriptSlot {
pub fn get(&self) -> CachedActiveScript {
let state = self.0.load(Ordering::Relaxed);
match (state & VALUE_MASK) as u32 {
UNKNOWN => CachedActiveScript::Unknown(ActiveScriptTicket(state)),
INACTIVE => CachedActiveScript::Known(None),
document_id => CachedActiveScript::Known(Some(document_id)),
}
}
pub fn fill(&self, ticket: ActiveScriptTicket, document_id: Option<u32>) {
let value = match document_id {
None => INACTIVE,
Some(document_id) if document_id < INACTIVE => document_id,
Some(_) => return,
};
let _ = self.0.compare_exchange(
ticket.0,
(ticket.0 & !VALUE_MASK) | u64::from(value),
Ordering::Relaxed,
Ordering::Relaxed,
);
}
pub fn invalidate(&self) {
self.0
.update(Ordering::Relaxed, Ordering::Relaxed, |state| {
(state & !VALUE_MASK).wrapping_add(GENERATION) | u64::from(UNKNOWN)
});
}
}
impl Default for ActiveScriptSlot {
fn default() -> Self {
ActiveScriptSlot(AtomicU64::new(u64::from(UNKNOWN)))
}
}
impl Clone for ActiveScriptSlot {
fn clone(&self) -> Self {
ActiveScriptSlot::default()
}
}
File diff suppressed because it is too large. Load diff
+19 -12
View File
@@ -4,13 +4,11 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use std::io::Write;
use encodify::{base32::STALWART, base64::URL_SAFE_NO_PAD};
use store::{
U32_LEN,
rand::{self},
};
use utils::codec::base32_custom::{Base32Reader, Base32Writer};
pub struct ApiKey {
pub account_id: u32,
@@ -18,6 +16,8 @@ pub struct ApiKey {
pub secret: [u8; 20],
}
const API_KEY_LEN: usize = U32_LEN * 2 + 20;
pub struct AppPassword {
pub credential_id: u32,
pub secret: [u8; 18],
@@ -33,7 +33,11 @@ impl ApiKey {
}
pub fn parse(token: &str) -> Option<Self> {
let decoded = URL_SAFE_NO_PAD.decode(token.strip_prefix("API_")?).ok()?;
let mut decoded = [0u8; API_KEY_LEN];
URL_SAFE_NO_PAD
.decode_slice(token.strip_prefix("API_")?, &mut decoded)
.ok()
.filter(|&len| len == API_KEY_LEN)?;
Some(ApiKey {
account_id: u32::from_be_bytes(decoded.get(0..U32_LEN)?.try_into().ok()?),
@@ -43,11 +47,13 @@ impl ApiKey {
}
pub fn build(&self) -> String {
let mut bytes = Vec::with_capacity(U32_LEN * 2 + self.secret.len());
let mut bytes = Vec::with_capacity(API_KEY_LEN);
bytes.extend_from_slice(&self.account_id.to_be_bytes());
bytes.extend_from_slice(&self.credential_id.to_be_bytes());
bytes.extend_from_slice(&self.secret);
format!("API_{}", URL_SAFE_NO_PAD.encode(bytes))
let mut token = String::from("API_");
URL_SAFE_NO_PAD.encode_append(bytes, &mut token);
token
}
}
@@ -61,7 +67,7 @@ impl AppPassword {
pub fn parse(token: &str) -> Option<Self> {
let token = token.strip_prefix("app")?;
let mut reader = Base32Reader::new(token.as_bytes().get(1..)?);
let mut reader = STALWART.decoder(token.as_bytes().get(1..)?);
let mut credential_id = [0u8; 4];
let mut secret = [0u8; 18];
@@ -84,10 +90,11 @@ impl AppPassword {
}
pub fn build(&self) -> String {
let mut writer = Base32Writer::with_capacity(std::mem::size_of::<Self>().div_ceil(5) * 8);
writer.push_string("app_");
let _ = writer.write(&self.credential_id.to_be_bytes());
let _ = writer.write_all(&self.secret);
writer.finalize()
let mut token = String::from("app_");
let mut encoder = STALWART.encoder(&mut token);
encoder.push(&self.credential_id.to_be_bytes());
encoder.push(&self.secret);
encoder.finish();
token
}
}
+127 -36
View File
@@ -7,10 +7,12 @@
*/
use crate::{
auth::{active_script::ActiveScriptSlot, oauth::scope::OAuthScopes},
expr::if_block::IfBlock,
network::limiter::ConcurrencyLimiter,
network::limiter::AccountLimits,
storage::{ObjectQuota, TenantQuota},
};
use compact_str::CompactString;
use directory::Credentials;
use quick_cache::Equivalent;
use registry::{
@@ -28,19 +30,21 @@ use types::collection::Collection;
use utils::{cache::CacheItemWeight, map::bitmap::Bitmap};
pub mod access_token;
pub mod active_script;
pub mod authentication;
pub mod credential;
pub mod oauth;
pub mod permissions;
pub mod rate_limit;
pub mod sasl;
pub const RECOVERY_ADMIN_ID: u32 = u32::MAX;
const PERMISSIONS_BITSET_SIZE: usize = Permission::COUNT.div_ceil(std::mem::size_of::<usize>());
const PERMISSIONS_BITSET_SIZE: usize = Permission::COUNT.div_ceil(std::mem::size_of::<usize>() * 8);
pub type Permissions = Bitset<PERMISSIONS_BITSET_SIZE>;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct EmailAddress {
pub local_part: Box<str>,
pub local_part: CompactString,
pub domain_id: u32,
}
@@ -62,6 +66,7 @@ pub enum EmailCache {
pub struct DomainCache {
pub names: Box<[Box<str>]>,
pub id: u32,
// inbuxa: MT, DIR-1
pub id_directory: Option<u32>,
pub id_tenant: Option<u32>,
pub catch_all: Option<Box<str>>,
@@ -74,19 +79,43 @@ pub const DOMAIN_FLAG_SUB_ADDRESSING: u8 = 1 << 1;
// inbuxa: SCIM-15, SCIM-58
pub const DOMAIN_FLAG_SCIM: u8 = 1 << 2;
#[derive(Debug, Clone, Default)]
pub struct AccountCache {
pub name: Box<str>,
pub id: u32,
pub addresses: Box<[EmailAddress]>,
// inbuxa: MT
pub id_tenant: Option<u32>,
pub id_member_of: TinyVec<[u32; 3]>,
pub id_member_of: Box<[u32]>,
pub quota_disk: u64,
pub quota_objects: Option<Box<ObjectQuota>>,
pub description: Option<Box<str>>,
pub encryption_key: Option<EncryptionKeys>,
pub storage: Option<Box<AccountStorage>>,
pub locale: Locale,
pub flags: u64,
pub active_script: ActiveScriptSlot,
}
#[derive(Debug, Clone, Default)]
pub struct AccountStorage {
pub quota_objects: Option<ObjectQuota>,
pub encryption_key: Option<EncryptionKeys>,
}
const _: () = assert!(size_of::<AccountCache>() <= 112);
impl AccountStorage {
pub fn boxed(
quota_objects: Option<ObjectQuota>,
encryption_key: Option<EncryptionKeys>,
) -> Option<Box<Self>> {
(quota_objects.is_some() || encryption_key.is_some()).then(|| {
Box::new(AccountStorage {
quota_objects,
encryption_key,
})
})
}
}
pub type EncryptionKeys = Box<[Box<[u8]>]>;
@@ -113,13 +142,6 @@ pub struct MailingListCache {
pub recipients: Arc<[Box<str>]>,
}
#[derive(Debug, Clone)]
pub struct TenantCache {
pub id_roles: TinyVec<[u32; 3]>,
pub quota_disk: u64,
pub quota_objects: Option<Box<TenantQuota>>,
pub permissions: Option<Box<PermissionsGroup>>,
}
#[derive(Debug, Clone, Default)]
pub struct PermissionsGroup {
@@ -132,24 +154,54 @@ pub struct PermissionsGroup {
pub struct AccessToken {
scope_idx: usize,
inner: Arc<AccessTokenInner>,
token_scope: Option<TokenScope>,
// inbuxa: how this session signed in, for the audit log (AU-5)
origin: Option<Arc<inbuxa_features::audit::Via>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenScope {
Grant {
grant_id: u64,
scopes: OAuthScopes,
client: GrantClient,
},
External {
scopes: OAuthScopes,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GrantClient {
FirstParty,
ThirdParty,
}
const _: () = assert!(size_of::<TokenScope>() <= 16);
const _: () = assert!(size_of::<Option<TokenScope>>() <= 16);
// inbuxa: MT
#[derive(Debug, Clone)]
pub struct TenantCache {
pub id_roles: Box<[u32]>,
pub quota_disk: u64,
pub quota_objects: Option<Box<TenantQuota>>,
pub permissions: Option<Box<PermissionsGroup>>,
}
#[derive(Debug, Default, Clone)]
pub struct AccessTokenInner {
pub(crate) account_id: u32,
// inbuxa: MT
pub(crate) tenant_id: Option<u32>,
pub(crate) member_of: TinyVec<[u32; 3]>,
pub(crate) obj_size: u32,
pub(crate) member_of: Box<[u32]>,
pub(crate) access_to: Box<[AccessTo]>,
pub(crate) scopes: Box<[AccessScope]>,
pub(crate) concurrent_http_requests: Option<ConcurrencyLimiter>,
pub(crate) concurrent_imap_requests: Option<ConcurrencyLimiter>,
pub(crate) concurrent_uploads: Option<ConcurrencyLimiter>,
pub(crate) limits: Option<Arc<AccountLimits>>,
pub(crate) revision_account: u64,
pub(crate) revision: u64,
pub(crate) credential_version: u64,
pub(crate) obj_size: u64,
// inbuxa: AL-2: the account is locked; it may not authenticate
pub(crate) locked: bool,
// inbuxa: MA-S: the lock is a shared mailbox
@@ -158,6 +210,9 @@ pub struct AccessTokenInner {
pub(crate) delegations: Box<[Delegation]>,
}
// inbuxa: 96 plus the lock flags, delegations and tenant
const _: () = assert!(size_of::<AccessTokenInner>() <= 120);
/// inbuxa: a locked account this one may open, and how (AL-5, AL-6).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Delegation {
@@ -183,6 +238,7 @@ pub struct AccessScope {
pub(crate) struct AccessTo {
pub account_id: u32,
pub collections: Bitmap<Collection>,
pub is_writable: bool,
}
#[derive(Clone)]
@@ -193,8 +249,9 @@ pub struct AccountInfo {
}
#[derive(Clone, Copy)]
pub struct AccountTenantIds {
pub struct ChangedBy {
pub account_id: u32,
// inbuxa: MT: tenant quotas follow the change
pub tenant_id: Option<u32>,
}
@@ -206,13 +263,13 @@ pub struct AuthRequest {
impl CacheItemWeight for AccessTokenInner {
fn weight(&self) -> u64 {
self.obj_size
u64::from(self.obj_size)
}
}
impl CacheItemWeight for EmailAddress {
fn weight(&self) -> u64 {
std::mem::size_of::<EmailAddress>() as u64 + self.local_part.len() as u64
std::mem::size_of::<EmailAddress>() as u64 + self.heap_size()
}
}
@@ -240,13 +297,13 @@ impl CacheItemWeight for DomainCache {
impl Equivalent<EmailAddress> for EmailAddressRef<'_> {
fn equivalent(&self, key: &EmailAddress) -> bool {
self.local_part == &*key.local_part && self.domain_id == key.domain_id
self.local_part == key.local_part.as_str() && self.domain_id == key.domain_id
}
}
impl Hash for EmailAddress {
fn hash<H: Hasher>(&self, state: &mut H) {
self.local_part.as_ref().hash(state);
self.local_part.as_str().hash(state);
self.domain_id.hash(state);
}
}
@@ -260,14 +317,22 @@ impl Hash for EmailAddressRef<'_> {
impl CacheItemWeight for AccountCache {
fn weight(&self) -> u64 {
std::mem::size_of::<AccountCache>() as u64
+ self.name.len() as u64
(std::mem::size_of::<AccountCache>()
+ self.name.len()
+ std::mem::size_of_val::<[u32]>(&self.id_member_of)
+ self.description.as_ref().map_or(0, |s| s.len())) as u64
+ self
.addresses
.iter()
.map(|s| s.local_part.len() as u64 + std::mem::size_of::<EmailAddress>() as u64)
.map(|address| address.weight())
.sum::<u64>()
+ self.description.as_ref().map_or(0, |s| s.len() as u64)
+ self.storage.as_ref().map_or(0, |storage| storage.weight())
}
}
impl CacheItemWeight for AccountStorage {
fn weight(&self) -> u64 {
std::mem::size_of::<AccountStorage>() as u64
+ self.encryption_key.as_ref().map_or(0, |keys| {
keys.iter()
.map(|k| k.len() as u64 + std::mem::size_of::<Box<[u8]>>() as u64)
@@ -282,13 +347,21 @@ impl CacheItemWeight for RoleCache {
}
}
// inbuxa: MT
impl CacheItemWeight for TenantCache {
fn weight(&self) -> u64 {
std::mem::size_of::<TenantCache>() as u64
+ self.permissions.as_ref().map_or(0, |p| p.weight())
}
}
impl CacheItemWeight for MailingListCache {
fn weight(&self) -> u64 {
std::mem::size_of::<MailingListCache>() as u64
+ self
.addresses
.iter()
.map(|s| s.local_part.len() as u64 + std::mem::size_of::<EmailAddress>() as u64)
.map(|address| address.weight())
.sum::<u64>()
+ self
.recipients
@@ -298,12 +371,6 @@ impl CacheItemWeight for MailingListCache {
}
}
impl CacheItemWeight for TenantCache {
fn weight(&self) -> u64 {
std::mem::size_of::<TenantCache>() as u64
+ self.permissions.as_ref().map_or(0, |p| p.weight())
}
}
impl CacheItemWeight for PermissionsGroup {
fn weight(&self) -> u64 {
@@ -319,19 +386,29 @@ impl BuildAccessToken for Arc<AccessTokenInner> {
fn build(self) -> AccessToken {
AccessToken {
scope_idx: 0,
origin: None,
inner: self,
token_scope: None,
origin: None,
}
}
}
impl EmailAddress {
pub fn new(local_part: impl Into<Box<str>>, domain_id: u32) -> Self {
pub fn new(local_part: impl Into<CompactString>, domain_id: u32) -> Self {
Self {
local_part: local_part.into(),
domain_id,
}
}
#[inline]
pub fn heap_size(&self) -> u64 {
if self.local_part.is_heap_allocated() {
self.local_part.capacity() as u64
} else {
0
}
}
}
impl<'x> EmailAddressRef<'x> {
@@ -347,6 +424,14 @@ impl AccountCache {
pub fn domain_id(&self) -> Option<u32> {
self.addresses.first().map(|address| address.domain_id)
}
/// inbuxa: MT: a tenant's token sees only its own tenant's accounts.
#[inline]
pub fn is_same_tenant(&self, access_token: &AccessToken) -> bool {
access_token
.tenant_id()
.is_none_or(|tenant_id| self.id_tenant == Some(tenant_id))
}
}
impl DomainCache {
@@ -358,4 +443,10 @@ impl DomainCache {
pub fn allows_scim(&self) -> bool {
self.flags & DOMAIN_FLAG_SCIM != 0
}
/// inbuxa: MT
#[inline]
pub fn is_same_tenant(&self, other: &DomainCache) -> bool {
self.id_tenant == other.id_tenant
}
}
+414
View File
@@ -0,0 +1,414 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use super::{
registration::{check_client_name, is_https_url, loopback_redirect_parts},
scope::OAuthScopes,
};
use crate::{KV_OAUTH_CIMD, KV_RATE_LIMIT_CIMD, Server, USER_AGENT};
use registry::schema::{enums::OAuthClientMetadataPolicy, prelude::Duration, structs::Rate};
use reqwest::{
Method, StatusCode, Url,
header::{ACCEPT, CACHE_CONTROL},
};
use serde::{Deserialize, de::IgnoredAny};
use std::time::Duration as StdDuration;
use store::{
Serialize,
dispatch::lookup::KeyValue,
write::{Archive, ArchiveBytes, Archiver},
};
use trc::{AddContext, AuthEvent};
use utils::{
HttpLimitResponse,
http::http_client_builder,
outbound::{OutboundClient, OutboundError, OutboundPolicy, Redirects},
};
pub const CIMD_MAX_DOCUMENT_SIZE: usize = 5 * 1024;
const CIMD_FETCH_TIMEOUT: StdDuration = StdDuration::from_secs(5);
const CIMD_MIN_TTL: u64 = 5 * 60;
const CIMD_MAX_TTL: u64 = 24 * 60 * 60;
const CIMD_MAX_REDIRECT_URIS: usize = 16;
const CIMD_MAX_REDIRECT_URI_LEN: usize = 2048;
const CIMD_DOMAIN_RATE: Rate = Rate {
count: 30,
period: Duration(StdDuration::from_secs(60)),
};
#[derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, Debug, Clone, PartialEq, Eq)]
pub struct CimdClient {
pub client_host: String,
pub client_name: Option<String>,
pub redirect_uris: Vec<String>,
pub post_logout_redirect_uris: Vec<String>,
pub scopes: OAuthScopes,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CimdError {
Disabled,
InvalidUrl(&'static str),
DomainNotAllowed,
Outbound(OutboundError),
Fetch,
Status(u16),
TooLarge,
InvalidDocument(&'static str),
}
#[derive(Deserialize)]
struct CimdDocument {
#[serde(default)]
client_id: Option<String>,
#[serde(default)]
client_name: Option<String>,
#[serde(default)]
redirect_uris: Option<Vec<String>>,
#[serde(default)]
post_logout_redirect_uris: Option<Vec<String>>,
#[serde(default)]
scope: Option<String>,
#[serde(default)]
token_endpoint_auth_method: Option<String>,
#[serde(default)]
client_secret: Option<IgnoredAny>,
#[serde(default)]
client_secret_expires_at: Option<IgnoredAny>,
#[serde(default)]
jwks: Option<serde_json::Value>,
}
impl Server {
pub fn check_cimd_policy(&self, client_id: &str) -> Result<Url, CimdError> {
cimd_policy_allows(
self.core.oauth.client_metadata_documents,
client_id,
self.core.network.resources.registrable_domain.as_deref(),
)
}
pub async fn cimd_client(
&self,
client_id: &str,
url: Url,
) -> trc::Result<Result<CimdClient, CimdError>> {
if let Some(stored) = self
.in_memory_store()
.key_get::<Archive<ArchiveBytes>>(KeyValue::<()>::build_key(
KV_OAUTH_CIMD,
client_id.as_bytes(),
))
.await
.caused_by(trc::location!())?
&& let Ok(client) = stored.deserialize::<CimdClient>()
{
return Ok(Ok(client));
}
let host = url.host_str().unwrap_or_default();
let domain = psl::domain_str(host).unwrap_or(host);
if let Some(reset) = self
.core
.storage
.memory
.is_rate_allowed(KV_RATE_LIMIT_CIMD, domain.as_bytes(), &CIMD_DOMAIN_RATE)
.await
.caused_by(trc::location!())?
{
return Err(trc::LimitEvent::TooManyRequests
.into_err()
.ctx(trc::Key::Expires, reset)
.ctx(trc::Key::Limit, CIMD_DOMAIN_RATE.count)
.details("Too many client metadata document fetches for this domain"));
}
let client_host = host.to_string();
let (body, ttl) = match fetch_cimd_document(url).await {
Ok(result) => result,
Err(err) => return Ok(Err(err)),
};
let client = match validate_cimd_document(client_id, client_host, &body) {
Ok(client) => client,
Err(err) => return Ok(Err(err)),
};
let value = Archiver::new(client.clone())
.untrusted()
.serialize()
.caused_by(trc::location!())?;
self.in_memory_store()
.key_set(KeyValue::with_prefix(KV_OAUTH_CIMD, client_id.as_bytes(), value).expires(ttl))
.await
.caused_by(trc::location!())?;
Ok(Ok(client))
}
}
pub fn cimd_policy_allows(
policy: OAuthClientMetadataPolicy,
client_id: &str,
registrable_domain: Option<&str>,
) -> Result<Url, CimdError> {
match policy {
OAuthClientMetadataPolicy::Disabled => Err(CimdError::Disabled),
OAuthClientMetadataPolicy::SameDomain => {
let url = check_cimd_url(client_id)?;
if registrable_domain.is_some_and(|expected| {
url.host_str()
.and_then(psl::domain_str)
.is_some_and(|domain| domain.eq_ignore_ascii_case(expected))
}) {
Ok(url)
} else {
Err(CimdError::DomainNotAllowed)
}
}
OAuthClientMetadataPolicy::Any => check_cimd_url(client_id),
}
}
pub fn check_cimd_url(client_id: &str) -> Result<Url, CimdError> {
let rest = client_id
.strip_prefix("https://")
.ok_or(CimdError::InvalidUrl("Client id must use the https scheme"))?;
if client_id.contains(['#', '\\']) {
return Err(CimdError::InvalidUrl(
"Client id must not contain a fragment",
));
}
if client_id.contains('?') {
return Err(CimdError::InvalidUrl(
"Client id must not contain a query component",
));
}
let (authority, path) = rest
.split_once('/')
.ok_or(CimdError::InvalidUrl("Client id must contain a path"))?;
if authority.is_empty() || authority.contains('@') {
return Err(CimdError::InvalidUrl(
"Client id must not contain a userinfo component",
));
}
if path.split('/').any(is_dot_segment) {
return Err(CimdError::InvalidUrl(
"Client id must not contain dot path segments",
));
}
let url = Url::parse(client_id).map_err(|_| CimdError::InvalidUrl("Invalid client id URL"))?;
if url.domain().is_some_and(|domain| !domain.is_empty()) {
Ok(url)
} else {
Err(CimdError::InvalidUrl(
"Client id must contain a domain name, not an IP address literal",
))
}
}
pub(crate) fn is_dot_segment(segment: &str) -> bool {
hashify::set_ignore_case!(
segment.as_bytes(),
".",
"..",
"%2e",
"%2e%2e",
".%2e",
"%2e.",
)
}
pub fn validate_cimd_document(
client_id: &str,
client_host: String,
body: &[u8],
) -> Result<CimdClient, CimdError> {
if body.len() > CIMD_MAX_DOCUMENT_SIZE {
return Err(CimdError::TooLarge);
}
let document = serde_json::from_slice::<CimdDocument>(body)
.map_err(|_| CimdError::InvalidDocument("Document is not a valid JSON object"))?;
if document.client_id.as_deref() != Some(client_id) {
return Err(CimdError::InvalidDocument(
"client_id does not match the document URL",
));
}
if document.client_secret.is_some() || document.client_secret_expires_at.is_some() {
return Err(CimdError::InvalidDocument(
"Document must not contain a client secret",
));
}
if document.token_endpoint_auth_method.as_deref() != Some("none") {
return Err(CimdError::InvalidDocument(
"token_endpoint_auth_method must be \"none\"",
));
}
if document.jwks.as_ref().is_some_and(has_private_key) {
return Err(CimdError::InvalidDocument(
"Document must not contain private key material",
));
}
let redirect_uris = document
.redirect_uris
.filter(|uris| !uris.is_empty() && uris.len() <= CIMD_MAX_REDIRECT_URIS)
.ok_or(CimdError::InvalidDocument(
"redirect_uris must list between 1 and 16 URIs",
))?;
if !redirect_uris
.iter()
.all(|uri| is_valid_cimd_redirect_uri(uri))
{
return Err(CimdError::InvalidDocument(
"redirect_uris must be https or loopback URIs without a fragment, of at most 2048 bytes",
));
}
let post_logout_redirect_uris = document.post_logout_redirect_uris.unwrap_or_default();
if post_logout_redirect_uris.len() > CIMD_MAX_REDIRECT_URIS {
return Err(CimdError::InvalidDocument(
"post_logout_redirect_uris must not list more than 16 URIs",
));
}
if !post_logout_redirect_uris
.iter()
.all(|uri| is_valid_cimd_redirect_uri(uri))
{
return Err(CimdError::InvalidDocument(
"post_logout_redirect_uris must be https or loopback URIs without a fragment, of at most 2048 bytes",
));
}
let client_name = match document.client_name.as_deref().map(str::trim) {
Some(name) if !name.is_empty() => {
check_client_name(name).map_err(CimdError::InvalidDocument)?;
Some(name.to_string())
}
_ => None,
};
Ok(CimdClient {
client_host,
client_name,
redirect_uris,
post_logout_redirect_uris,
scopes: document
.scope
.as_deref()
.map_or(OAuthScopes::INTEROPERABLE, |scope| {
OAuthScopes::parse(scope).interoperable()
}),
})
}
fn is_valid_cimd_redirect_uri(uri: &str) -> bool {
uri.len() <= CIMD_MAX_REDIRECT_URI_LEN
&& !uri.contains(['#', '\\'])
&& (is_https_url(uri) || loopback_redirect_parts(uri).is_some())
}
fn has_private_key(jwks: &serde_json::Value) -> bool {
jwks.get("keys")
.and_then(|keys| keys.as_array())
.is_some_and(|keys| {
keys.iter().any(|key| {
key.as_object().is_some_and(|key| {
["d", "p", "q", "dp", "dq", "qi", "oth", "k"]
.iter()
.any(|member| key.contains_key(*member))
})
})
})
}
async fn fetch_cimd_document(url: Url) -> Result<(Vec<u8>, u64), CimdError> {
let display_url = url.to_string();
let response = OutboundClient::new(
http_client_builder(cfg!(inbuxa_tests))
.user_agent(USER_AGENT)
.timeout(CIMD_FETCH_TIMEOUT),
OutboundPolicy::PublicOnly,
Redirects::None,
)
.request_url(Method::GET, url)
.map_err(CimdError::Outbound)?
.header(ACCEPT, "application/json")
.send()
.await
.map_err(|err| {
trc::event!(
Auth(AuthEvent::Error),
Url = display_url.clone(),
Reason = err.to_string(),
Details = "Failed to fetch client metadata document",
);
CimdError::Fetch
})?;
if response.status() != StatusCode::OK {
return Err(CimdError::Status(response.status().as_u16()));
}
let ttl = cache_ttl(
response
.headers()
.get(CACHE_CONTROL)
.and_then(|value| value.to_str().ok()),
);
let body = response
.bytes_with_limit(CIMD_MAX_DOCUMENT_SIZE)
.await
.map_err(|err| {
trc::event!(
Auth(AuthEvent::Error),
Url = display_url,
Reason = err.to_string(),
Details = "Failed to read client metadata document",
);
CimdError::Fetch
})?
.ok_or(CimdError::TooLarge)?;
Ok((body, ttl))
}
fn cache_ttl(cache_control: Option<&str>) -> u64 {
cache_control
.and_then(|value| {
value.split(',').find_map(|directive| {
let (name, value) = directive.split_once('=')?;
if name.trim().eq_ignore_ascii_case("max-age") {
value.trim().trim_matches('"').parse::<u64>().ok()
} else {
None
}
})
})
.unwrap_or(CIMD_MIN_TTL)
.clamp(CIMD_MIN_TTL, CIMD_MAX_TTL)
}
impl CimdError {
pub fn as_str(&self) -> &'static str {
match self {
CimdError::Disabled => "Client metadata documents are disabled",
CimdError::InvalidUrl(reason) | CimdError::InvalidDocument(reason) => reason,
CimdError::DomainNotAllowed => "Client metadata document domain is not allowed",
CimdError::Outbound(err) => err.as_str(),
CimdError::Fetch => "Failed to fetch the client metadata document",
CimdError::Status(_) => "Client metadata document request did not return 200 OK",
CimdError::TooLarge => "Client metadata document is too large",
}
}
}
impl store::write::ArchiveCompression for CimdClient {
const COMPRESSION: store::write::Compression = store::write::Compression::None;
}
Loaded 100 of 2142 files, more files were not shown because too many files have changed in this diff. Show more