pax_global_header00006660000000000000000000000064144711502530014514gustar00rootroot0000000000000052 comment=c174826ee7484fcbc2decc7da3082508fd02727d imap-codec-1.0.0/000077500000000000000000000000001447115025300135135ustar00rootroot00000000000000imap-codec-1.0.0/.github/000077500000000000000000000000001447115025300150535ustar00rootroot00000000000000imap-codec-1.0.0/.github/FUNDING.yml000066400000000000000000000000211447115025300166610ustar00rootroot00000000000000github: [duesee] imap-codec-1.0.0/.github/workflows/000077500000000000000000000000001447115025300171105ustar00rootroot00000000000000imap-codec-1.0.0/.github/workflows/api.yml000066400000000000000000000015421447115025300204060ustar00rootroot00000000000000name: Check API on: push: branches: [ main ] paths: - '**.rs' - '**.toml' - '.github/workflows/**' pull_request: branches: [ main ] paths: - '**.rs' - '**.toml' - '.github/workflows/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: semver: runs-on: ubuntu-latest steps: - name: Setup | Install cargo-semver-checks run: cargo install cargo-semver-checks --locked - name: Setup | Checkout code uses: actions/checkout@v3 - name: Check for SemVer violations | imap-types run: | cd imap-types cargo semver-checks check-release - name: Check for SemVer violations | imap-codec run: | cd imap-codec cargo semver-checks check-release imap-codec-1.0.0/.github/workflows/audit.yml000066400000000000000000000020051447115025300207360ustar00rootroot00000000000000name: Audit on: schedule: # 21:43 on Wednesday and Sunday. (Thanks, crontab.guru) - cron: '43 21 * * 3,0' workflow_dispatch: jobs: # test_extended: # runs-on: ubuntu-latest # # steps: # - name: Checkout code # uses: actions/checkout@v3 # # - uses: taiki-e/install-action@v2 # with: # tool: cargo-hack # # - name: Test (extended) # # TODO: We exclude the tokio demos for now to bypass a "none of the selected packages contains these features" error. # run: | # cargo hack test \ # --workspace \ # --exclude tokio-client --exclude tokio-server \ # --feature-powerset \ # --group-features starttls,ext_condstore_qresync,ext_login_referrals,ext_mailbox_referrals \ # --exclude-features ext,split audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: EmbarkStudios/cargo-deny-action@e0a440755b184aa50374330fa75cca0f84fcb59a imap-codec-1.0.0/.github/workflows/build_and_test.yml000066400000000000000000000123031447115025300226120ustar00rootroot00000000000000name: Build & Test on: push: branches: [ main ] paths: - "**.rs" - "**.toml" - ".github/workflows/**" pull_request: branches: [ main ] paths: - "**.rs" - "**.toml" - ".github/workflows/**" workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: check: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - uses: taiki-e/install-action@v2 with: tool: cargo-hack - name: Check # TODO: We exclude the tokio demos for now to bypass a "none of the selected packages contains these features" error. run: | cargo hack check \ --workspace \ --exclude tokio-client --exclude tokio-server --exclude tokio-support \ --feature-powerset \ --group-features starttls,ext_condstore_qresync,ext_login_referrals,ext_mailbox_referrals \ --exclude-features ext,split test: strategy: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v3 - name: Setup | Install toolchain run: | rustup toolchain install stable --profile minimal rustup toolchain install nightly --profile minimal - name: Setup | Install cargo-fuzz run: | cargo install cargo-fuzz - name: Setup | Cache dependencies uses: Swatinem/rust-cache@v2.5.1 id: cache with: cache-all-crates: true - name: Test | Everything w/o fuzzing (macOS, Ubuntu) if: matrix.os != 'windows-latest' run: | for build_mode in "" "--release"; do for feature_mode in "" "--all-features"; do echo "# Testing" ${build_mode} ${feature_mode} cargo test --workspace ${build_mode} ${feature_mode} --doc cargo test --workspace ${build_mode} ${feature_mode} --all-targets --exclude imap-codec-fuzz --exclude imap-types-fuzz done done - name: Test | Everything w/o fuzzing (Windows) if: matrix.os == 'windows-latest' run: | $build_modes = @('','--release') $feature_modes = @('','--all-features') foreach ($build_mode in $build_modes) { foreach ($feature_mode in $feature_modes) { echo "# Testing" ${build_mode} ${feature_mode} cargo test --workspace ${build_mode} ${feature_mode} --doc cargo test --workspace ${build_mode} ${feature_mode} --all-targets --exclude imap-codec-fuzz --exclude imap-types-fuzz } } - name: Test | Limited fuzzing (Ubuntu) if: matrix.os == 'ubuntu-latest' run: | cd imap-codec for fuzz_target in $(cargo +nightly fuzz list); do echo "# Fuzzing ${fuzz_target}"; cargo +nightly fuzz run --features=ext ${fuzz_target} -- -dict=fuzz/terminals.dict -max_len=256 -only_ascii=1 -runs=25000 done minimal-versions: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Setup | Install toolchain run: | # 1.65 is the Minimum Supported Rust Version (MSRV) for imap-codec. rustup toolchain install 1.65 --profile minimal rustup toolchain install nightly --profile minimal - name: Setup | Cache dependencies uses: Swatinem/rust-cache@v2.5.1 id: cache with: cache-all-crates: true - name: Check run: | cargo +nightly update -Z minimal-versions cargo +1.65 check --workspace --all-targets --all-features --exclude tokio-server cargo +1.65 test --workspace --all-targets --all-features --exclude tokio-server --exclude imap-codec-fuzz --exclude imap-types-fuzz env: RUSTFLAGS: -Dwarnings audit: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Audit dependencies uses: EmbarkStudios/cargo-deny-action@7257a18a9c2fe3f92b85d41ae473520dff953c97 clippy: runs-on: ubuntu-latest steps: - name: Install toolchain uses: actions-rs/toolchain@88dc2356392166efad76775c878094f4e83ff746 with: profile: minimal toolchain: stable override: true components: clippy - name: Checkout code uses: actions/checkout@v3 - name: Check for common mistakes and missed improvements uses: actions-rs/clippy-check@b5b5f21f4797c02da247df37026fcd0a5024aa4d with: token: ${{ secrets.GITHUB_TOKEN }} args: --all-features formatting: runs-on: ubuntu-latest steps: - name: Install nightly toolchain uses: actions-rs/toolchain@88dc2356392166efad76775c878094f4e83ff746 with: profile: minimal toolchain: nightly override: true components: rustfmt - name: Checkout code uses: actions/checkout@v3 - name: Check code formatting run: cargo +nightly fmt --check imap-codec-1.0.0/.github/workflows/coverage.yml000066400000000000000000000023151447115025300214270ustar00rootroot00000000000000name: Measure test coverage on: push: branches: [ main ] paths: - '**.rs' - '**.toml' - '.github/workflows/**' pull_request: branches: [ main ] paths: - '**.rs' - '**.toml' - '.github/workflows/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: coverage: runs-on: ubuntu-latest steps: - name: Setup run: | rustup component add llvm-tools-preview cargo install grcov - name: Checkout code uses: actions/checkout@v3 - name: Measure test coverage env: RUSTFLAGS: "-Cinstrument-coverage" LLVM_PROFILE_FILE: "coverage-%m-%p.profraw" run: | cargo test -p imap-codec -p imap-types --all-features grcov . \ --source-dir . \ --binary-path target/debug \ --branch \ --keep-only '{imap-codec/src/**,imap-types/src/**}' \ --output-types "lcov" \ --llvm > coveralls.lcov - name: Upload to Coveralls uses: coverallsapp/github-action@v2 with: format: lcov file: coveralls.lcov imap-codec-1.0.0/.github/workflows/issue_opened.yml000066400000000000000000000005461447115025300223220ustar00rootroot00000000000000name: Process opened issues on: issues: types: - opened jobs: add-to-project: name: Add opened issue to project board runs-on: ubuntu-latest steps: - uses: actions/add-to-project@v0.3.0 with: project-url: https://github.com/users/duesee/projects/1 github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} imap-codec-1.0.0/.gitignore000066400000000000000000000001001447115025300154720ustar00rootroot00000000000000Cargo.lock target .idea # Coverage **/*.profraw coveralls.lcov imap-codec-1.0.0/CHANGELOG.md000066400000000000000000000230501447115025300153240ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Version 1.0.0] - 2032-08-22 ### Changed * Use `'static` lifetime for `Decoder::Error` in `decode_static`. ## [Version 1.0.0-beta] - 2023-08-17 ### Added * Introduced `FlagNameAttributeExtension`. * Implemented `Display` for some `T` where `T`s `Display` implementation equals `Encode`. ### Changed * Inlined stable `ext_*` features to improve SemVer compatibility. * Re-exported `imap_codec::imap_types`. * Simplified module hierarchy. * Increased MSRV to 1.65. * Moved tokio implementation to demos. * Replaced `Decode` trait with `Decoder`. * Replaced `Encode` trait with `Encoder`. * Made `*Other` types merely technicalities. * Improved `Debug` print. * Updated `Swatinem/rust-cache`. * Simplified `Error`s. * Aligned type names with IMAP RFC. * Improved documentation. * Replaced `Capability::Literal(LiteralCapability)` with `Capability::LiteralPlus`, and `Capability::LiteralMinus`. ## [Version 0.10.0] - 2023-07-05 ### Added * Added `AuthMechanism::XOAUTH`. * Added more constructors. * Added (and improved) feature documentation. (Thanks, @jakoschiko!) * Added multiple `quirk_*` features to improve interoperability. * Added `DecodeStatic`. * Checking with `cargo-hack` and `--feature-powerset`. * Fuzz-testing with incomplete messages. ### Changed * Simplified module hierarchy. * Renamed types for better understandability (and to align them with the IMAP4rev1 standard). * Renamed constructors so they cannot be confused with `unsafe`. (Thanks, @jakoschiko!) * Resolved multiple SemVer hazards. * Use custom nom error. * Deduplicated (and added a new) fuzz-target(s). * Don't export nom parsers anymore. * Removed constant-time comparison support. * Simplified `Debug`ing of `NonEmptyVec`. ### Fixed * Fixed warnings and broken links in documentation. * Fixed `is_text_char`. * Fixed `condstore` identity. * Fixed usage of `complete` (instead of `streaming`). ### Removed * Removed `ansi_term` dev dependency. ## [Version 0.9.0] - 2023-05-30 ### Added - Implemented `MOVE` (RFC 6851). - Implemented `UNSELECT` (RFC 3691). - Implemented (some of) `CONDSTORE` / `QRESYNC` (RFC 7162). - Reworked (and enabled) coverage job in CI. - Added (spot-)fuzzing to CI. - Added `minimal-versions` job to CI. - Test MSRV. - Test lowest versions of dependencies. ### Changed - Migrated to Rust 2021. - Redesigned `Encode` trait. - Moved `Encode` trait from imap-types to imap-codec. ### Fixed - Made known-answer tests stronger. - Made it so that `Decode` is always tested during `Encode` and vice versa. - Made it so that random tests are reproducable through a seed. - Resolved remaining `TODO`s in `command_to_bytes_and_back` fuzz-target. - Resolved remaining `TODO`s in `{Single,Multi}PartExtensionData` - Fixed misuse of `{Single,Multi}PartExtensionData.` - Introduced `BodyExtension`. - Introduced `ContinueBasic` to prevent ambiguities. - Fixed `Eq` side effect of `Secret`. - Fixed `mbx_list_flags`. - Fixed `NaiveDate`. - Made `MyNaiveDate::arbitrary` really arbitrary. - Narrowed allowed values for `DateTime` and `NaiveDate`. - Fixed poor constant-time sanity check. - Fixed possible `panic!` in `response`. - Reactivated ignored tests. ## [Version 0.8.0] - 2023-04-16 ### Added * Community * Introduced a project board and a GitHub action that adds all opened issues to the project board. * Added a `CONTRIBUTING.md`. * Features * Implemented RFC 2088/RFC 7888 (LITERAL+). * Implemented RFC 2087/RFC 9208 (QUOTA). * Thanks, @MinisculeGirraffe! * Introduced usable error reporting. * Introduced `Encode::encode_detached`. * Implemented missing `From`, `TryFrom`, `AsRef`, ... conversions for various types. * Testing/Fuzzing * Improved debug workflow. * Introduced `ext` and `debug` features. * Security * Forbid `unsafe` and introduced `unchecked` feature. * Ensured that secret values are not `Debug`-printed and comparisons are made in constant time. * Wrapped `AuthenticateData` in `Secret`. * Wrapped `CommandBody::Login.password` in `Secret`. ### Changed * Refactoring * Feature-gated all existing extensions. * Simplified module/feature names for `tokio` support. * Changed naming schema to phase out `mod.rs`. * Renamed `MyDateTime` to `DateTime`, `SeqNo` to `SeqOrUid`, `SeqNo::Largest` to `SeqNo::Asterisk`. * CI * Added a job that checks for SemVer violations. * Improved CI runtime. * Made it so that superseded jobs are eagerly canceled. * Made it so that the `Coverage` job is started only after a successful `Build & Test`. * Inlined `--all-features` to reduce compilation time. * Chore * Allowed `Unicode-DFS-2016` and `BSD-3-Clause` dependencies. ### Fixed * Testing/Fuzzing * Made fuzz-targets tighter by not skipping (known) misuses. * Reactivated commented-out test code. * Restored trace generation for `README.md`. * Misuses * Fixed (known) misuses for `Capability{,Other}`, `Code{,Other}`, `Continue`, `Flag`, and `Body`. * Worked around ambiguities in IMAP. * Fixed various parsers that need to greedily consume tokens such as `Atom`s. * Fixed `text` parser by excluding `[` and `]`. ## [Version 0.7.0] - 2022-08-05 ### Added * Add tokio demos (client + server). * Introduce `ImapClientCodec` and implement `tokio_util::codec::{Encoder, Decoder}`. * Add tests to `tokio_compat`. * Add `greeting_to_bytes_and_back` fuzz target. * Introduce `Decode` trait and implement it for `Command` and `Response`. * Introduce `Greeting`, and `GreetingKind`. * Introduce `State::Greeting` variant. * Introduce `IdleDone`. * Introduce `CapabilityOther` and implement `Capability::other()`. * Implement `Decode` for `Greeting` and use it in the `tokio_compat` module. * Implement `AuthMechanism::other`. * Implement `Data::expunge`. * Implement `Code::{uidnext, uidvalidity, unseen}`. ### Changed * Improve CI. * Improve documentation. * Switch to new module layout in imap-codec. * Refactor creation of `Command`s and `CommandBody`s. * Use `Decode` trait in examples. * Use `Command::decode` instead of `command`. * Allow "Unicode-DFS-2016" license in "deny.toml". * Use `Tag` in `State::{IdleAuthenticated,IdleSelected}` instead of `String`. * Derive `Debug`, `Eq`, and `PartialEq` for `State`. * Feature-gate `Capability::LoginDisabled` with "starttls" feature. * Feature-gate `State::{Idle*}` variants with "ext_idle" feature. ### Removed * Remove `nom` feature. * Don't export `arbitrary`, and `rfc3501`. * Make `imap_types::{codec, state}` part of public API. Don't export `imap_types::Encode` directly. * Delete `greeting` constructor of `Status`. * Delete `PreAuth` variant (and constructor) of `Status`. ### Fixed * Fix missing doc test in CI. * Fix (and improve) examples. ## [Version 0.6.0] - 2022-06-14 ### Added - Introduce "starttls" feature. - Cleanup and document existing features. - Measure code coverage in CI. - Upload coverage report to Coveralls.io. - Add/Update code coverage badge in README.md. - Compile fuzzers in CI. - Implement benchmarks (Criterion.rs). - Compile benchmarks in CI. - Implement `Command::into_static()` and `Response::into_static()`. - Use `bounded-static` (thanks, @jakoschiko) - Add types to fix misuses - Introduce `AtomExt` (1*ASTRING-CHAR) to fix misuse. - Introduce `CapabilityEnable` to increase misuse-resistance. - Split imap-codec into imap-codec and imap-types. - Implement non-nom parsing in imap-types. - Add README.md to imap-types. ### Changed - Split crate into imap-codec and imap-types. - Make imap-codec the primary workspace member. - Re-export `imap-types`. - Make fuzz targets members of workspace to simplify workflow. - Rename "serdex"/"nomx" features to "serde"/"nom". - Reduce allocations during parsing. - Use `Cow` to abstract over owned and borrowed slices. - Do not check slices twice. - Introduce `new_unchecked()` functions. - Check `new_unchecked()` during debug builds. - Cleanup API for `AuthMechanism`. - Update to nom 7 and abnf-core 0.5. ### Removed - Remove `impl Display` for types in imap-types. - Remove `nom` feature in imap-types. - Remove/cleanup (unused) dependencies in imap-codec. - Remove/cleanup (unused) dependencies in imap-types. ### Fixed - Fix fuzz targets. - Fix benchmarks (thanks, @franziskuskiefer). - Fix misuses, e.g., `AtomExt` (1*ASTRING-CHAR). [Version 0.6.0]: https://github.com/duesee/imap-codec/compare/fcb400e508f74a8d88bbcbfd777bdca7cb75bdeb...63b6a2e4a94f2734d67a18039b3f6dae68994902 [Version 0.7.0]: https://github.com/duesee/imap-codec/compare/63b6a2e4a94f2734d67a18039b3f6dae68994902...16e34bce239840bc3a39c811f1ce3d36c6ea20b0 [Version 0.8.0]: https://github.com/duesee/imap-codec/compare/16e34bce239840bc3a39c811f1ce3d36c6ea20b0...f5138ac09b6e160256c8e6dc80db1597aee92394 [Version 0.9.0]: https://github.com/duesee/imap-codec/compare/f5138ac09b6e160256c8e6dc80db1597aee92394...3bb1b380a6f163a16732f9dd9c8382f2af73868c [Version 0.10.0]: https://github.com/duesee/imap-codec/compare/3bb1b380a6f163a16732f9dd9c8382f2af73868c...ca3ef319681d4e8ea2daf28b9a3650d2d74813c7 [Version 1.0.0-beta]: https://github.com/duesee/imap-codec/compare/ca3ef319681d4e8ea2daf28b9a3650d2d74813c7...1b8924dce7c943cd003a8316f384af97649feadf [Version 1.0.0]: https://github.com/duesee/imap-codec/compare/1b8924dce7c943cd003a8316f384af97649feadf...a5d8dff9e8047bda2c477a3a9d56e53274113b26 [Unreleased]: https://github.com/duesee/imap-codec/compare/a5d8dff9e8047bda2c477a3a9d56e53274113b26...HEAD imap-codec-1.0.0/CONTRIBUTING.md000066400000000000000000000023471447115025300157520ustar00rootroot00000000000000# Welcome to imap-codec's (and imap-types') contributing guide Thanks for investing your time to help with this project! Keep in mind that this project is driven by volunteers. Be patient and polite, and empower others to improve. Always use your best judgment and be excellent to each other. ## Technicalities ### Formatting of code We use [`rustfmt`] through `cargo +nightly fmt` configured by `rustfmt.toml` to format our code. This helps to minimize diffs and eases onboarding. Code formatting is automatically checked by GitHub Actions through our CI. ### Misuse resistance We make use of strong-typing to [eliminate invalid state]. Ask yourself: Can I instantiate a type that has an invalid setting of variables? If yes, consider how to eliminate it. If you're unsure, let's figure it out together. ### Usage of features IMAP is extensible. Thus, we use [Cargo features] to enable/disable extensions to the core IMAP protocol. Feature-gating helps to reduce the amount of exposed code and serves as documentation for the supported extensions. [`rustfmt`]: https://github.com/rust-lang/rustfmt [eliminate invalid state]: https://duesee.dev/p/type-driven-development/ [Cargo features]: https://doc.rust-lang.org/cargo/reference/features.html imap-codec-1.0.0/Cargo.toml000066400000000000000000000004301447115025300154400ustar00rootroot00000000000000[workspace] resolver = "2" members = [ "imap-codec", "imap-codec/fuzz", "imap-types", "imap-types/fuzz", "assets/demos/tokio-support", "assets/demos/tokio-client", "assets/demos/tokio-server", ] [patch.crates-io] imap-types = { path = "imap-types" } imap-codec-1.0.0/LICENSE-APACHE000066400000000000000000000261351447115025300154460ustar00rootroot00000000000000 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. imap-codec-1.0.0/LICENSE-MIT000066400000000000000000000020621447115025300151470ustar00rootroot00000000000000MIT License Copyright (c) 2020 Damian Poddebniak 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. imap-codec-1.0.0/README.md000066400000000000000000000373541447115025300150060ustar00rootroot00000000000000[![Build & Test](https://github.com/duesee/imap-codec/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/duesee/imap-codec/actions/workflows/build_and_test.yml) [![Audit](https://github.com/duesee/imap-codec/actions/workflows/audit.yml/badge.svg)](https://github.com/duesee/imap-codec/actions/workflows/audit.yml) [![Coverage](https://coveralls.io/repos/github/duesee/imap-codec/badge.svg?branch=main)](https://coveralls.io/github/duesee/imap-codec?branch=main) [![Documentation](https://docs.rs/imap-codec/badge.svg)](https://docs.rs/imap-codec) # imap-{codec,types} This workspace contains [`imap-codec`] and [`imap-types`], two [rock-solid] and [well-documented] crates to build [IMAP4rev1] clients and servers. `imap-codec` provides parsing and serialization, and is based on `imap-types`. `imap-types` provides misuse-resistant types, constructors, and general support for IMAP implementations. The crates live here together, but `imap-types` is a perfectly fine standalone crate. Let's talk on [Matrix]! ## Features * Complete [formal syntax] of IMAP4rev1 is implemented. Furthermore, several IMAP [extensions] are supported. * Correctness and misuse-resistance are enforced on the type level. It's not possible to construct a message that violates the IMAP specification. * Messages automatically use the most efficient representation. For example, atoms are preferred over quoted strings, and quoted strings are preferred over literals. It's equally easy to manually choose a representation. * Parsing works in streaming mode. `Incomplete` is returned when there is insufficient data to make a final decision. No message will be truncated. * Parsing is zero-copy by default. Allocation is avoided during parsing, but all messages can explicitly be converted into more flexible owned variants. * Fuzzing and property-based tests exercise the library. The library is fuzz-tested never to produce a message it can't parse itself. ## Usage ```rust use imap_codec::{ codec::{Decode, Encode}, imap_types::command::Command, }; fn main() { let input = b"ABCD UID FETCH 1,2:* (BODY.PEEK[1.2.3.4.MIME]<42.1337>)\r\n"; let (remainder, parsed) = Command::decode(input).unwrap(); println!("# Parsed\n\n{:#?}\n\n", parsed); let buffer = parsed.encode().dump(); // Note: IMAP4rev1 may produce messages that are not valid UTF-8. println!("# Serialized\n\n{:?}", std::str::from_utf8(&buffer)); } ``` ## Examples ### Simple parsing Try one of the `parse_*` examples, e.g., ... ```sh $ cargo run --example=parse_command ``` ... to parse some IMAP messages. ### Tokio demo You can also start the [demo server] with ... ```sh $ cargo run -p tokio-server -- : ``` ... and connect to it with ... ```sh $ netcat -C ``` There is also a [demo client] available. **Note:** All demos are a work-in-progress. Feel free to propose API changes to `imap-codec` (or `imap-types`) to simplify them. ### Parsed and serialized IMAP4rev1 connection The following output was generated by reading the trace from [RFC 3501 section 8](https://tools.ietf.org/html/rfc3501#section-8), printing the input (first line), `Debug`-printing the parsed object (second line), and printing the serialized output (third line). ```rust // * OK IMAP4rev1 Service Ready Status(Ok { tag: None, code: None, text: Text("IMAP4rev1 Service Ready") }) // * OK IMAP4rev1 Service Ready // a001 login mrc secret Command { tag: Tag("a001"), body: Login { username: Atom(AtomExt("mrc")), password: /* REDACTED */ } } // a001 LOGIN mrc secret // a001 OK LOGIN completed Status(Ok { tag: Some(Tag("a001")), code: None, text: Text("LOGIN completed") }) // a001 OK LOGIN completed // a002 select inbox Command { tag: Tag("a002"), body: Select { mailbox: Inbox } } // a002 SELECT INBOX // * 18 EXISTS Data(Exists(18)) // * 18 EXISTS // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) Data(Flags([Answered, Flagged, Deleted, Seen, Draft])) // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) // * 2 RECENT Data(Recent(2)) // * 2 RECENT // * OK [UNSEEN 17] Message 17 is the first unseen message Status(Ok { tag: None, code: Some(Unseen(17)), text: Text("Message 17 is the first unseen message") }) // * OK [UNSEEN 17] Message 17 is the first unseen message // * OK [UIDVALIDITY 3857529045] UIDs valid Status(Ok { tag: None, code: Some(UidValidity(3857529045)), text: Text("UIDs valid") }) // * OK [UIDVALIDITY 3857529045] UIDs valid // a002 OK [READ-WRITE] SELECT completed Status(Ok { tag: Some(Tag("a002")), code: Some(ReadWrite), text: Text("SELECT completed") }) // a002 OK [READ-WRITE] SELECT completed // a003 fetch 12 full Command { tag: Tag("a003"), body: Fetch { sequence_set: SequenceSet([Single(Value(12))]+), macro_or_item_names: Macro(Full), uid: false } } // a003 FETCH 12 FULL // * 12 FETCH (FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" RFC822.SIZE 4286 ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" "IMAP4rev1 WG mtg summary and minutes" (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) ((NIL NIL "imap" "cac.washington.edu")) ((NIL NIL "minutes" "CNRI.Reston.VA.US")("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL "") BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 3028 92)) Data(Fetch { seq: 12, items: [Flags([Flag(Seen)]), InternalDate(1996-07-17T02:44:25-07:00), Rfc822Size(4286), Envelope(Envelope { date: NString(Some(Quoted(Quoted("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)")))), subject: NString(Some(Quoted(Quoted("IMAP4rev1 WG mtg summary and minutes")))), from: [Address { name: NString(Some(Quoted(Quoted("Terry Gray")))), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("gray")))), host: NString(Some(Quoted(Quoted("cac.washington.edu")))) }], sender: [Address { name: NString(Some(Quoted(Quoted("Terry Gray")))), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("gray")))), host: NString(Some(Quoted(Quoted("cac.washington.edu")))) }], reply_to: [Address { name: NString(Some(Quoted(Quoted("Terry Gray")))), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("gray")))), host: NString(Some(Quoted(Quoted("cac.washington.edu")))) }], to: [Address { name: NString(None), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("imap")))), host: NString(Some(Quoted(Quoted("cac.washington.edu")))) }], cc: [Address { name: NString(None), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("minutes")))), host: NString(Some(Quoted(Quoted("CNRI.Reston.VA.US")))) }, Address { name: NString(Some(Quoted(Quoted("John Klensin")))), adl: NString(None), mailbox: NString(Some(Quoted(Quoted("KLENSIN")))), host: NString(Some(Quoted(Quoted("MIT.EDU")))) }], bcc: [], in_reply_to: NString(None), message_id: NString(Some(Quoted(Quoted("")))) }), Body(Single { body: Body { basic: BasicFields { parameter_list: [(Quoted(Quoted("CHARSET")), Quoted(Quoted("US-ASCII")))], id: NString(None), description: NString(None), content_transfer_encoding: Quoted(Quoted("7BIT")), size: 3028 }, specific: Text { subtype: Quoted(Quoted("PLAIN")), number_of_lines: 92 } }, extension_data: None })]+ }) // * 12 FETCH (FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" RFC822.SIZE 4286 ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" "IMAP4rev1 WG mtg summary and minutes" (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) ((NIL NIL "imap" "cac.washington.edu")) ((NIL NIL "minutes" "CNRI.Reston.VA.US")("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL "") BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 3028 92)) // a003 OK FETCH completed Status(Ok { tag: Some(Tag("a003")), code: None, text: Text("FETCH completed") }) // a003 OK FETCH completed // a004 fetch 12 body[header] Command { tag: Tag("a004"), body: Fetch { sequence_set: SequenceSet([Single(Value(12))]+), macro_or_item_names: MessageDataItemNames([BodyExt { section: Some(Header(None)), partial: None, peek: false }]), uid: false } } // a004 FETCH 12 BODY[HEADER] // * 12 FETCH (BODY[HEADER] {342} // Date: Wed, 17 Jul 1996 02:23:25 -0700 (PDT) // From: Terry Gray // Subject: IMAP4rev1 WG mtg summary and minutes // To: imap@cac.washington.edu // cc: minutes@CNRI.Reston.VA.US, John Klensin // Message-Id: // MIME-Version: 1.0 // Content-Type: TEXT/PLAIN; CHARSET=US-ASCII // // ) Data(Fetch { seq: 12, items: [BodyExt { section: Some(Header(None)), origin: None, data: NString(Some(Literal(Literal { data: b"Date: Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\r\nFrom: Terry Gray \r\nSubject: IMAP4rev1 WG mtg summary and minutes\r\nTo: imap@cac.washington.edu\r\ncc: minutes@CNRI.Reston.VA.US, John Klensin \r\nMessage-Id: \r\nMIME-Version: 1.0\r\nContent-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n\r\n" }))) }]+ }) // * 12 FETCH (BODY[HEADER] {342} // Date: Wed, 17 Jul 1996 02:23:25 -0700 (PDT) // From: Terry Gray // Subject: IMAP4rev1 WG mtg summary and minutes // To: imap@cac.washington.edu // cc: minutes@CNRI.Reston.VA.US, John Klensin // Message-Id: // MIME-Version: 1.0 // Content-Type: TEXT/PLAIN; CHARSET=US-ASCII // // ) // a004 OK FETCH completed Status(Ok { tag: Some(Tag("a004")), code: None, text: Text("FETCH completed") }) // a004 OK FETCH completed // a005 store 12 +flags \deleted Command { tag: Tag("a005"), body: Store { sequence_set: SequenceSet([Single(Value(12))]+), kind: Add, response: Answer, flags: [Deleted], uid: false } } // a005 STORE 12 +FLAGS (\Deleted) // * 12 FETCH (FLAGS (\Seen \Deleted)) Data(Fetch { seq: 12, items: [Flags([Flag(Seen), Flag(Deleted)])]+ }) // * 12 FETCH (FLAGS (\Seen \Deleted)) // a005 OK +FLAGS completed Status(Ok { tag: Some(Tag("a005")), code: None, text: Text("+FLAGS completed") }) // a005 OK +FLAGS completed // a006 logout Command { tag: Tag("a006"), body: Logout } // a006 LOGOUT // * BYE IMAP4rev1 server terminating connection Status(Bye { code: None, text: Text("IMAP4rev1 server terminating connection") }) // * BYE IMAP4rev1 server terminating connection // a006 OK LOGOUT completed Status(Ok { tag: Some(Tag("a006")), code: None, text: Text("LOGOUT completed") }) // a006 OK LOGOUT completed ``` # FAQ
How does imap-codec compare to imap-proto? `imap-codec` provides low-level parsing and serialization support for IMAP4rev1, similar to [`imap-proto`]. The most significant differences are server support, the split into `imap-codec` and `imap-types`, misuse resistance (affecting API design), and (real-world) test coverage. No matter if implementing a client- or a server, you need the full set of IMAP type definitions. When you send a command with a specific [`Tag`], you expect a command completion response with the same [`Tag`]. Thus, commands and responses must work well together (and are best provided by a single crate). As far as I know, `imap-proto` doesn't provide types that would be reusable in a generic server implementation. `imap-types` provides type definitions for client- and server implementations. As a client developer, you will never parse commands or serialize responses. As a server developer, you will never serialize commands or parse responses. Thus, you only need "half of" the set of parsers and serializers. As far as I know, `imap-proto` provides the "client half" only. `imap-codec` provides both the "client half" and the "server half". Separating types and codecs increases cohesion and (hopefully) paves the way for IMAP crates that operate at higher levels. However, the maintenance cost of two crates, `imap-types` and `imap-codec`, could be higher than for `imap-proto`. Generally, `imap-codec` has a more extensive API surface than `imap-proto` and could be [more challenging to use](construction). In return, it guarantees that you always construct valid messages and aims to make IMAP usable even for people with less IMAP experience. For example, `imap-codec` has [build-in support for IMAP literals] and ensures to always use [a correct representation for strings](construction). `imap-codec` has a high test coverage and is fuzz-tested to ensure properties such as invertibility, misuse-resistance, etc. You should be unable to crash the library or generate messages that can't be parsed. However, "interoperability can not be tested in a vacuum" [^1]. `imap-proto` already succeeded in production as it is (transitively) used in [`imap`], [`async-imap`], and [Delta Chat]. It could solve more real-world quirks, provide more IMAP extensions that matter in practice, or generally have a more mature interoperability story.
Have you considered contributing to imap-proto? I created `imap-codec` because I needed [server-side support](https://github.com/Email-Analysis-Toolkit/fake-mail-server). The intention was to eventually merge `imap-codec` into `imap-proto` as soon as it's "ready". I even did a bit of [preparation work](https://github.com/djc/tokio-imap/graphs/contributors). However, the different types (and philosophy, maybe), made merging non-trivial. Both projects can learn from each other and align on their goals. Still, joining forces would require a fair amount of work from everyone, and I wonder if we are willing (and have the resources) to start such an endeavor.
# License This crate is dual-licensed under Apache 2.0 and MIT terms. # Thanks Thanks to the [NLnet Foundation](https://nlnet.nl/) for supporting imap-codec through their [NGI Assure](https://nlnet.nl/assure/) program!
[rock-solid]: https://github.com/duesee/imap-codec/tree/main/imap-codec/fuzz [well-documented]: https://docs.rs/imap-codec/latest/imap_codec/ [Matrix]: https://matrix.to/#/#imap-codec:matrix.org [IMAP4rev1]: https://tools.ietf.org/html/rfc3501 [formal syntax]: https://tools.ietf.org/html/rfc3501#section-9 [extensions]: https://docs.rs/imap-codec/latest/imap_codec/#features [cargo fuzz]: https://github.com/rust-fuzz/cargo-fuzz [demo client]: https://github.com/duesee/imap-codec/tree/main/assets/demos/tokio-client [demo server]: https://github.com/duesee/imap-codec/tree/main/assets/demos/tokio-server [parse_command]: https://github.com/duesee/imap-codec/blob/main/examples/parse_command.rs [`imap-codec`]: imap-codec [`imap-types`]: imap-types [`imap`]: https://github.com/jonhoo/rust-imap [`imap-proto`]: https://crates.io/crates/imap-proto [`async-imap`]: https://github.com/async-email/async-imap [Delta Chat]: https://delta.chat [core types]: https://docs.rs/imap-types/latest/imap_types/core/index.html [`Command`]: https://docs.rs/imap-types/latest/imap_types/command/struct.Command.html [`Response`]: https://docs.rs/imap-types/latest/imap_types/response/enum.Response.html [`Tag`]: https://docs.rs/imap-types/latest/imap_types/core/struct.Tag.html [`BodyStructure`]: https://docs.rs/imap-types/latest/imap_types/body/enum.BodyStructure.html [construction]: https://github.com/duesee/imap-codec/tree/main/imap-types#examples [build-in support for IMAP literals]: https://docs.rs/imap-codec/latest/imap_codec/codec/struct.Encoded.html [IMAP servers with imap-codec]: https://github.com/Email-Analysis-Toolkit/fake-mail-server [^1]: https://datatracker.ietf.org/doc/html/rfc2683 imap-codec-1.0.0/SECURITY.md000066400000000000000000000003521447115025300153040ustar00rootroot00000000000000# Security Policy ## Reporting a Vulnerability Feel free to open a public issue here if you found a security vulnerability. If, for whatever reason, you prefer to not make it public, please write an email to poddebniak@mailbox.org imap-codec-1.0.0/assets/000077500000000000000000000000001447115025300150155ustar00rootroot00000000000000imap-codec-1.0.0/assets/demos/000077500000000000000000000000001447115025300161245ustar00rootroot00000000000000imap-codec-1.0.0/assets/demos/tokio-client/000077500000000000000000000000001447115025300205255ustar00rootroot00000000000000imap-codec-1.0.0/assets/demos/tokio-client/Cargo.toml000066400000000000000000000006701447115025300224600ustar00rootroot00000000000000[package] name = "tokio-client" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1.0.71" futures = "0.3" tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.7", features = ["codec"] } imap-codec = { path = "../../../imap-codec" } tokio-support = { path = "../tokio-support" } imap-codec-1.0.0/assets/demos/tokio-client/src/000077500000000000000000000000001447115025300213145ustar00rootroot00000000000000imap-codec-1.0.0/assets/demos/tokio-client/src/main.rs000066400000000000000000000102611447115025300226060ustar00rootroot00000000000000use anyhow::{Context, Error}; use futures::{SinkExt, StreamExt}; use imap_codec::imap_types::{ command::{Command, CommandBody}, core::Tag, response::{Response, Status}, }; use tokio::{self, net::TcpStream}; use tokio_support::client::{Event, ImapClientCodec}; use tokio_util::codec::Decoder; // Poor human's terminal color support. const BLUE: &str = "\x1b[34m"; const RED: &str = "\x1b[31m"; const RESET: &str = "\x1b[0m"; #[tokio::main] async fn main() -> Result<(), Error> { let addr = std::env::args() .nth(1) .context("USAGE: tokio-client :")?; let mut framed = { let stream = TcpStream::connect(&addr) .await .context(format!("Could not connect to `{addr}`"))?; // This is for demonstration purposes only, and we probably want a bigger number. let max_literal_size = 1024; ImapClientCodec::new(max_literal_size).framed(stream) }; // First, we read the server greeting. match framed .next() .await // We get an `Option>` here that denotes ... // 1) if we got something from the server, and // 2) if it was valid. .context("Connection closed unexpectedly")? .context("Failed to obtain next message")? { Event::Greeting(greeting) => { println!("S: {BLUE}{greeting:#?}{RESET}"); } Event::Response(response) => { return Err(Error::msg(format!("Expected greeting, got `{response:?}`"))); } }; // Then, we send a login command to the server ... let tag_login = Tag::unvalidated("A1"); let cmd = Command { tag: tag_login.clone(), body: CommandBody::login("alice", "password").context("Could not create command")?, }; framed.send(&cmd).await.context("Could not send command")?; println!("C: {RED}{cmd:#?}{RESET}"); // ... and process the response(s). We must read zero or many data responses before we can // finally examine the status response that tells us whether the login succeeded. loop { let frame = framed .next() .await .context("Connection closed unexpectedly")? .context("Failed to obtain next message")?; println!("S: {BLUE}{frame:#?}{RESET}"); match frame { Event::Greeting(greeting) => { return Err(Error::msg(format!("Expected response, got `{greeting:?}`"))); } Event::Response(response) => match response { Response::Status(ref status) if status.tag() == Some(&tag_login) => { if matches!(status, Status::Ok { .. }) { println!("[!] got login done (successful)"); } else { println!("[!] got login done (failed)"); } break; } _ => { println!("[!] unexpected response"); } }, } } let tag_logout = Tag::unvalidated("A2"); let cmd = Command { tag: tag_logout.clone(), body: CommandBody::Logout, }; framed.send(&cmd).await.context("Could not send command")?; println!("C: {RED}{cmd:#?}{RESET}"); loop { let frame = framed .next() .await .context("Connection closed unexpectedly")? .context("Failed to obtain next message")?; println!("S: {BLUE}{frame:#?}{RESET}"); match frame { Event::Greeting(greeting) => { return Err(Error::msg(format!("Expected response, got `{greeting:?}`"))); } Event::Response(response) => match response { Response::Status(Status::Bye { .. }) => { println!("[!] got bye"); } Response::Status(Status::Ok { tag: Some(ref tag), .. }) if *tag == tag_logout => { println!("[!] got logout done"); break; } _ => { println!("[!] unexpected response"); } }, } } Ok(()) } imap-codec-1.0.0/assets/demos/tokio-server/000077500000000000000000000000001447115025300205555ustar00rootroot00000000000000imap-codec-1.0.0/assets/demos/tokio-server/Cargo.toml000066400000000000000000000007111447115025300225040ustar00rootroot00000000000000[package] name = "tokio-server" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1.0.71" argon2 = "0.5.0" futures = "0.3" tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.7", features = ["codec"] } imap-codec = { path = "../../../imap-codec" } tokio-support = { path = "../tokio-support" } imap-codec-1.0.0/assets/demos/tokio-server/src/000077500000000000000000000000001447115025300213445ustar00rootroot00000000000000imap-codec-1.0.0/assets/demos/tokio-server/src/main.rs000066400000000000000000000156671447115025300226550ustar00rootroot00000000000000use anyhow::{Context, Error}; use argon2::Argon2; use futures::{SinkExt, StreamExt}; use imap_codec::imap_types::{ command::CommandBody, core::{NonEmptyVec, Text}, response::{Capability, CommandContinuationRequest, Data, Greeting, Response, Status}, }; use tokio::{self, net::TcpListener}; use tokio_support::server::{Action, Event, ImapServerCodec}; use tokio_util::codec::Decoder; // Poor human's terminal color support. const BLUE: &str = "\x1b[34m"; const RED: &str = "\x1b[31m"; const RESET: &str = "\x1b[0m"; #[tokio::main] async fn main() -> Result<(), Error> { let addr = std::env::args() .nth(1) .context("USAGE: tokio-server :")?; let mut framed = { let stream = { // Bind listener ... let listener = TcpListener::bind(&addr) .await .context(format!("Could not bind to `{addr}`"))?; // ... and accept a single connection. let (stream, _) = listener .accept() .await .context("Could not accept connection")?; stream }; // Accept 2 MiB literals. let mib2 = 2 * 1024 * 1024; ImapServerCodec::new(mib2).framed(stream) }; // Send a positive greeting ... let greeting = Greeting::ok(None, "Hello, World!").context("Could not create greeting")?; framed .send(&greeting) .await .context("Could not send greeting")?; println!("S: {BLUE}{greeting:#?}{RESET}"); // ... and process the following commands in a loop. loop { match framed .next() .await .context("Connection closed unexpectedly")? .context("Failed to obtain next message")? { Event::Command(cmd) => { println!("C: {RED}{cmd:#?}{RESET}"); match (cmd.tag, cmd.body) { (tag, CommandBody::Capability) => { let rsp = Response::Data(Data::Capability(NonEmptyVec::from( Capability::Imap4Rev1, ))); framed.send(&rsp).await.context("Could not send response")?; println!("S: {BLUE}{rsp:#?}{RESET}"); let rsp = Response::Status( Status::ok(Some(tag), None, "CAPABILITY done") .context("Could not create `Status`")?, ); framed.send(&rsp).await.context("Could not send response")?; println!("S: {BLUE}{rsp:#?}{RESET}"); } (tag, CommandBody::Login { username, password }) => { let login_okay = { let username_okay = username.as_ref() == b"alice"; let password_okay = { // Salt should be unique per password. let salt = b"hf63l9nx43gf95ks"; let password = password.declassify().as_ref(); let mut output = [0u8; 32]; Argon2::default() .hash_password_into(password, salt, &mut output) .map_err(|error| Error::msg(error.to_string())) .context("Failed to hash password.")?; output == [ 227, 130, 151, 49, 100, 203, 239, 68, 119, 207, 247, 237, 214, 42, 85, 208, 198, 107, 116, 35, 64, 122, 143, 68, 236, 228, 130, 250, 31, 221, 217, 77, ] }; username_okay && password_okay }; let rsp = if login_okay { Response::Status(Status::Ok { tag: Some(tag), code: None, text: Text::unvalidated("LOGIN succeeded"), }) } else { Response::Status(Status::Ok { tag: Some(tag), code: None, text: Text::unvalidated("LOGIN failed"), }) }; framed.send(&rsp).await.context("Could not send response")?; println!("S: {BLUE}{rsp:#?}{RESET}"); } (tag, CommandBody::Logout) => { let rsp = Response::Status( Status::bye(None, "...").expect("Could not create `Status`"), ); framed.send(&rsp).await.context("Could not send response")?; println!("S: {BLUE}{rsp:#?}{RESET}"); let rsp = Response::Status( Status::ok(Some(tag), None, "LOGOUT done") .expect("Could not create `Status`"), ); framed.send(&rsp).await.context("Could not send response")?; println!("S: {BLUE}{rsp:#?}{RESET}"); return Ok(()); } (tag, body) => { let text = format!("{} not supported", body.name()); let rsp = Response::Status( Status::no(Some(tag), None, text) .context("Could not create `Status`")?, ); framed.send(&rsp).await.context("Could not send response")?; println!("S: {BLUE}{rsp:#?}{RESET}"); } } } Event::ActionRequired(Action::SendLiteralAck(_)) => { println!("[!] Send continuation request."); let rsp = Response::CommandContinuationRequest( CommandContinuationRequest::basic(None, "...") .context("Could not create `Continue`")?, ); framed.send(&rsp).await.context("Could not send response")?; println!("S: {BLUE}{rsp:#?}{RESET}"); } Event::ActionRequired(Action::SendLiteralReject(_)) => { println!("[!] Send literal reject."); let rsp = Response::Status( Status::bad(None, None, "literal too large.") .context("Could not create `Status`")?, ); framed.send(&rsp).await.context("Could not send response")?; println!("S: {BLUE}{rsp:#?}{RESET}"); } } } } imap-codec-1.0.0/assets/demos/tokio-support/000077500000000000000000000000001447115025300207635ustar00rootroot00000000000000imap-codec-1.0.0/assets/demos/tokio-support/Cargo.toml000066400000000000000000000006311447115025300227130ustar00rootroot00000000000000[package] name = "tokio-support" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] bytes = "1.4.0" bounded-static = "0.5.0" thiserror = "1.0.29" tokio-util = { version = "0.7.8", features = ["codec"] } imap-codec = { path = "../../../imap-codec", features = ["bounded-static"] } imap-codec-1.0.0/assets/demos/tokio-support/src/000077500000000000000000000000001447115025300215525ustar00rootroot00000000000000imap-codec-1.0.0/assets/demos/tokio-support/src/client.rs000066400000000000000000000321451447115025300234030ustar00rootroot00000000000000use std::io::{Error as IoError, Write}; use bounded_static::IntoBoundedStatic; use bytes::{Buf, BufMut, BytesMut}; use imap_codec::{ decode::{Decoder, GreetingDecodeError, ResponseDecodeError}, encode::Encoder, imap_types::{ command::Command, response::{Greeting, Response}, state::{State as ImapState, State}, }, CommandCodec, GreetingCodec, ResponseCodec, }; use thiserror::Error; use tokio_util::codec::{Decoder as TokioDecoder, Encoder as TokioEncoder}; use super::{find_crlf_inclusive, FramingError, FramingState}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ImapClientCodec { state: FramingState, imap_state: ImapState<'static>, max_literal_length: u32, } impl ImapClientCodec { pub fn new(max_literal_length: u32) -> Self { Self { state: FramingState::ReadLine { to_consume_acc: 0 }, imap_state: ImapState::Greeting, max_literal_length, } } } #[derive(Debug, Error)] pub enum ImapClientCodecError { #[error(transparent)] Io(#[from] IoError), #[error(transparent)] Framing(#[from] FramingError), #[error("Parsing failed")] ParsingFailed(BytesMut), } impl PartialEq for ImapClientCodecError { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Io(error1), Self::Io(error2)) => error1.kind() == error2.kind(), (Self::Framing(kind2), Self::Framing(kind1)) => kind1 == kind2, (Self::ParsingFailed(x), Self::ParsingFailed(y)) => x == y, _ => false, } } } #[derive(Debug, PartialEq, Eq)] pub enum Event { Greeting(Greeting<'static>), Response(Response<'static>), } impl TokioDecoder for ImapClientCodec { type Item = Event; type Error = ImapClientCodecError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { loop { if self.imap_state == State::Greeting { match GreetingCodec::default().decode(src) { Ok((remaining, grt)) => { let grt = grt.into_static(); let to_consume_acc = src.len() - remaining.len(); src.advance(to_consume_acc); self.imap_state = ImapState::NotAuthenticated; return Ok(Some(Event::Greeting(grt))); } Err(GreetingDecodeError::Incomplete) => { return Ok(None); } Err(GreetingDecodeError::Failed) => { let discarded = src.split_to(src.len()); src.clear(); return Err(ImapClientCodecError::ParsingFailed(discarded)); } } } match self.state { FramingState::ReadLine { ref mut to_consume_acc, } => { match find_crlf_inclusive(*to_consume_acc, src) { Some(line) => match line { // After skipping `to_consume_acc` bytes, we need `to_consume` more // bytes to form a full line (including the `\r\n`). Ok(to_consume) => { *to_consume_acc += to_consume; let line = &src[..*to_consume_acc]; // TODO: Choose the required parser. let parser = |input| { ResponseCodec::default() .decode(input) .map(|(rem, rsp)| (rem, Event::Response(rsp.into_static()))) }; match parser(line) { // We got a complete message. Ok((rem, outcome)) => { assert!(rem.is_empty()); src.advance(*to_consume_acc); self.state = FramingState::ReadLine { to_consume_acc: 0 }; if self.imap_state == ImapState::Greeting { self.imap_state = ImapState::NotAuthenticated; } return Ok(Some(outcome)); } Err(error) => match error { // We supposedly need more data ... // // This should not happen because a line that doesn't end // with a literal is always "complete" in IMAP. ResponseDecodeError::Incomplete => { unreachable!(); } // We found a literal. ResponseDecodeError::LiteralFound { length } => { if length <= self.max_literal_length { src.reserve(length as usize); self.state = FramingState::ReadLiteral { to_consume_acc: *to_consume_acc, length, }; return Ok(None); } else { src.advance(*to_consume_acc); self.state = FramingState::ReadLine { to_consume_acc: 0 }; return Err(ImapClientCodecError::Framing( FramingError::LiteralTooLarge { max_literal_length: self.max_literal_length, length, }, )); } } ResponseDecodeError::Failed => { let consumed = src.split_to(*to_consume_acc); self.state = FramingState::ReadLine { to_consume_acc: 0 }; return Err(ImapClientCodecError::ParsingFailed( consumed, )); } }, } } // After skipping `to_consume_acc` bytes, we need `to_consume` more // bytes to form a full line (including the `\n`). // // Note: This line is missing the `\r\n` and should be discarded. Err(to_discard) => { *to_consume_acc += to_discard; src.advance(*to_consume_acc); self.state = FramingState::ReadLine { to_consume_acc: 0 }; return Err(ImapClientCodecError::Framing(FramingError::NotCrLf)); } }, // More data needed. None => { return Ok(None); } } } FramingState::ReadLiteral { to_consume_acc, length, } => { if to_consume_acc + length as usize <= src.len() { self.state = FramingState::ReadLine { to_consume_acc: to_consume_acc + length as usize, } } else { return Ok(None); } } } } } } impl<'a> TokioEncoder<&Command<'a>> for ImapClientCodec { type Error = IoError; fn encode(&mut self, item: &Command, dst: &mut BytesMut) -> Result<(), Self::Error> { //dst.reserve(item.len()); let mut writer = dst.writer(); // TODO(225): Don't use `dump` here. let data = CommandCodec::default().encode(item).dump(); writer.write_all(&data)?; Ok(()) } } #[cfg(test)] mod tests { #[cfg(feature = "quirk_crlf_relaxed")] use std::num::NonZeroU32; use bytes::BytesMut; use imap_codec::imap_types::{ core::{Literal, NString}, fetch::{MessageDataItem, Section}, response::{Data, GreetingKind}, }; use tokio_util::codec::Decoder; use super::*; #[test] fn test_decoder_line() { let tests = [ (b"".as_ref(), Ok(None)), (b"* ", Ok(None)), (b"OK ...\r", Ok(None)), ( b"\n", Ok(Some(Event::Greeting( Greeting::new(GreetingKind::Ok, None, "...").unwrap(), ))), ), (b"", Ok(None)), (b"xxxx", Ok(None)), ( b"\r\n", Err(ImapClientCodecError::ParsingFailed(BytesMut::from( b"xxxx\r\n".as_ref(), ))), ), ]; let mut src = BytesMut::new(); let mut codec = ImapClientCodec::new(1024); for (test, expected) in tests { src.extend_from_slice(test); let got = codec.decode(&mut src); assert_eq!(expected, got); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); } } #[test] fn test_decoder_literal() { let tests = [ ( b"* OK ...\r\n".as_ref(), Ok(Some(Event::Greeting( Greeting::new(GreetingKind::Ok, None, "...").unwrap(), ))), ), (b"* 12 FETCH (BODY[HEADER] {3}", Ok(None)), (b"\r", Ok(None)), (b"\n", Ok(None)), (b"a", Ok(None)), (b"bc)", Ok(None)), (b"\r", Ok(None)), ( b"\n", Ok(Some(Event::Response(Response::Data( Data::fetch( 12, vec![MessageDataItem::BodyExt { section: Some(Section::Header(None)), origin: None, data: NString(Some(Literal::try_from("abc").unwrap().into())), }], ) .unwrap(), )))), ), ]; let mut src = BytesMut::new(); let mut codec = ImapClientCodec::new(1024); for (test, expected) in tests { src.extend_from_slice(test); let got = codec.decode(&mut src); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); assert_eq!(expected, got); } } #[test] fn test_decoder_error() { let tests = [ // We need to process the greeting first. ( b"* OK ...\r\n".as_ref(), Ok(Some(Event::Greeting( Greeting::new(GreetingKind::Ok, None, "...").unwrap(), ))), ), ( b"xxx\r\n".as_ref(), Err(ImapClientCodecError::ParsingFailed(BytesMut::from( b"xxx\r\n".as_ref(), ))), ), ( b"* search 1\n", #[cfg(not(feature = "quirk_crlf_relaxed"))] Err(ImapClientCodecError::Framing(FramingError::NotCrLf)), #[cfg(feature = "quirk_crlf_relaxed")] Ok(Some(Event::Response(Response::Data(Data::Search(vec![ NonZeroU32::try_from(1).unwrap(), ]))))), ), ( b"* 1 fetch (BODY[] {17}\r\naaaaaaaaaaaaaaaa)\r\n", Err(ImapClientCodecError::Framing( FramingError::LiteralTooLarge { max_literal_length: 16, length: 17, }, )), ), ]; let mut src = BytesMut::new(); let mut codec = ImapClientCodec::new(16); for (test, expected) in tests { src.extend_from_slice(test); let got = codec.decode(&mut src); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); assert_eq!(expected, got); } } } imap-codec-1.0.0/assets/demos/tokio-support/src/lib.rs000066400000000000000000000065241447115025300226750ustar00rootroot00000000000000//! Support for tokio and (tokio_util::codec). use thiserror::Error; pub mod client; pub mod server; /// All interactions transmitted by client and server are in the form of /// lines, that is, strings that end with a CRLF. /// /// The protocol receiver of an IMAP4rev1 client or server is either ... #[derive(Debug, Clone, PartialEq, Eq)] enum FramingState { /// ... reading a line, or ... ReadLine { to_consume_acc: usize }, /// ... is reading a sequence of octets /// with a known count followed by a line. ReadLiteral { to_consume_acc: usize, length: u32 }, } #[derive(Debug, Error, PartialEq, Eq)] pub enum FramingError { #[error("Expected `\\r\\n`, got `\\n`")] NotCrLf, #[error("Could not find a line searching a maximum of {max_line_length} bytes")] LineTooLarge { max_line_length: u32 }, #[error("Could not find a message while searching a maximum of {max_message_length} bytes")] MessageTooLarge { max_message_length: u32 }, #[error("Expected a maximum literal length of {max_literal_length} bytes, got {length} bytes")] LiteralTooLarge { max_literal_length: u32, length: u32, }, } /// Skip the first `skip` bytes of `buf` and count how many more bytes are needed to cover the next `\r\n`. /// /// This function returns `Ok(None)` when no line was found, `Ok(Some(length))` with /// `buf[..skip + length]` being the first line (including `\r\n`), or `Err(length)` with /// `buf[..skip + length]` being the first line (including `\n`) with a missing `\r`. fn find_crlf_inclusive(skip: usize, buf: &[u8]) -> Option> { #[allow(clippy::manual_map)] match buf.iter().skip(skip).position(|item| *item == b'\n') { Some(position) => { #[cfg(not(feature = "quirk_crlf_relaxed"))] if buf[skip + position.saturating_sub(1)] == b'\r' { Some(Ok(position + 1)) } else { Some(Err(position + 1)) } #[cfg(feature = "quirk_crlf_relaxed")] Some(Ok(position + 1)) } None => None, } } #[cfg(test)] mod tests { use super::*; #[test] fn test_find_crlf_inclusive() { let tests = [ (b"A\r".as_ref(), 0, None), (b"A\r\n", 0, Some(Ok(3))), #[cfg(not(feature = "quirk_crlf_relaxed"))] (b"A\n", 0, Some(Err(2))), #[cfg(feature = "quirk_crlf_relaxed")] (b"A\n", 0, Some(Ok(2))), #[cfg(not(feature = "quirk_crlf_relaxed"))] (b"\n", 0, Some(Err(1))), #[cfg(feature = "quirk_crlf_relaxed")] (b"\n", 0, Some(Ok(1))), (b"aaa\r\nA\r".as_ref(), 5, None), (b"aaa\r\nA\r\n", 5, Some(Ok(3))), #[cfg(not(feature = "quirk_crlf_relaxed"))] (b"aaa\r\nA\n", 5, Some(Err(2))), #[cfg(feature = "quirk_crlf_relaxed")] (b"aaa\r\nA\n", 5, Some(Ok(2))), #[cfg(not(feature = "quirk_crlf_relaxed"))] (b"aaa\r\n\n", 5, Some(Err(1))), #[cfg(feature = "quirk_crlf_relaxed")] (b"aaa\r\n\n", 5, Some(Ok(1))), ]; for (test, skip, expected) in tests { let got = find_crlf_inclusive(skip, test); dbg!((std::str::from_utf8(test).unwrap(), skip, &expected, &got)); assert_eq!(expected, got); } } } imap-codec-1.0.0/assets/demos/tokio-support/src/server.rs000066400000000000000000000306011447115025300234260ustar00rootroot00000000000000use std::io::{Error as IoError, Write}; use bounded_static::IntoBoundedStatic; use bytes::{Buf, BufMut, BytesMut}; use imap_codec::{ decode::{CommandDecodeError, Decoder}, encode::Encoder, imap_types::{ command::Command, response::{Greeting, Response}, }, CommandCodec, GreetingCodec, ResponseCodec, }; use thiserror::Error; use tokio_util::codec::{Decoder as TokioDecoder, Encoder as TokioEncoder}; use super::{find_crlf_inclusive, FramingError, FramingState}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ImapServerCodec { state: FramingState, max_literal_size: usize, } impl ImapServerCodec { pub fn new(max_literal_size: usize) -> Self { Self { state: FramingState::ReadLine { to_consume_acc: 0 }, max_literal_size, } } } #[derive(Debug, Error)] pub enum ImapServerCodecError { #[error(transparent)] Io(#[from] IoError), #[error(transparent)] Framing(#[from] FramingError), #[error("Parsing failed")] ParsingFailed(BytesMut), } impl PartialEq for ImapServerCodecError { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Io(error1), Self::Io(error2)) => error1.kind() == error2.kind(), (Self::Framing(kind1), Self::Framing(kind2)) => kind1 == kind2, (Self::ParsingFailed(x), Self::ParsingFailed(y)) => x == y, _ => false, } } } #[derive(Debug, PartialEq, Eq)] pub enum Event { Command(Command<'static>), ActionRequired(Action), // More might be require. } #[derive(Debug, PartialEq, Eq)] pub enum Action { SendLiteralAck(u32), SendLiteralReject(u32), } impl TokioDecoder for ImapServerCodec { type Item = Event; type Error = ImapServerCodecError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { loop { match self.state { FramingState::ReadLine { ref mut to_consume_acc, } => match find_crlf_inclusive(*to_consume_acc, src) { Some(line) => match line { // After skipping `to_consume_acc` bytes, we need `to_consume` more // bytes to form a full line (including the `\r\n`). Ok(to_consume) => { *to_consume_acc += to_consume; let line = &src[..*to_consume_acc]; // TODO: Choose the required parser. match CommandCodec::default().decode(line) { // We got a complete message. Ok((rem, cmd)) => { assert!(rem.is_empty()); let cmd = cmd.into_static(); src.advance(*to_consume_acc); self.state = FramingState::ReadLine { to_consume_acc: 0 }; return Ok(Some(Event::Command(cmd))); } Err(error) => match error { // We supposedly need more data ... // // This should not happen because a line that doesn't end // with a literal is always "complete" in IMAP. CommandDecodeError::Incomplete => { unreachable!(); } // We found a literal. CommandDecodeError::LiteralFound { length, .. } => { if length as usize <= self.max_literal_size { src.reserve(length as usize); self.state = FramingState::ReadLiteral { to_consume_acc: *to_consume_acc, length, }; return Ok(Some(Event::ActionRequired( Action::SendLiteralAck(length), ))); } else { src.advance(*to_consume_acc); self.state = FramingState::ReadLine { to_consume_acc: 0 }; return Ok(Some(Event::ActionRequired( Action::SendLiteralReject(length), ))); } } CommandDecodeError::Failed => { let consumed = src.split_to(*to_consume_acc); self.state = FramingState::ReadLine { to_consume_acc: 0 }; return Err(ImapServerCodecError::ParsingFailed(consumed)); } }, } } // After skipping `to_consume_acc` bytes, we need `to_consume` more // bytes to form a full line (including the `\n`). // // Note: This line is missing the `\r\n` and should be discarded. Err(to_discard) => { src.advance(*to_consume_acc + to_discard); self.state = FramingState::ReadLine { to_consume_acc: 0 }; return Err(ImapServerCodecError::Framing(FramingError::NotCrLf)); } }, // More data needed. None => { return Ok(None); } }, FramingState::ReadLiteral { to_consume_acc, length, } => { if to_consume_acc + length as usize <= src.len() { self.state = FramingState::ReadLine { to_consume_acc: to_consume_acc + length as usize, } } else { return Ok(None); } } } } } } impl TokioEncoder<&Greeting<'_>> for ImapServerCodec { type Error = IoError; fn encode(&mut self, item: &Greeting, dst: &mut BytesMut) -> Result<(), Self::Error> { //dst.reserve(item.len()); let mut writer = dst.writer(); // TODO(225): Don't use `dump` here. let data = GreetingCodec::default().encode(item).dump(); writer.write_all(&data)?; Ok(()) } } impl TokioEncoder<&Response<'_>> for ImapServerCodec { type Error = IoError; fn encode(&mut self, item: &Response, dst: &mut BytesMut) -> Result<(), Self::Error> { //dst.reserve(item.len()); let mut writer = dst.writer(); // TODO(225): Don't use `dump` here. let data = ResponseCodec::default().encode(item).dump(); writer.write_all(&data)?; Ok(()) } } #[cfg(test)] mod tests { use bytes::BytesMut; use imap_codec::imap_types::{ command::{Command, CommandBody}, core::{AString, AtomExt, IString, Literal}, secret::Secret, }; #[cfg(feature = "quirk_crlf_relaxed")] use imap_types::core::Tag; use tokio_util::codec::Decoder; use super::*; #[test] fn test_decoder_line() { let tests = [ (b"".as_ref(), Ok(None)), (b"a noop", Ok(None)), (b"\r", Ok(None)), ( b"\n", Ok(Some(Event::Command( Command::new("a", CommandBody::Noop).unwrap(), ))), ), (b"", Ok(None)), (b"xxxx", Ok(None)), ( b"\r\n", Err(ImapServerCodecError::ParsingFailed(BytesMut::from( b"xxxx\r\n".as_ref(), ))), ), ]; let mut src = BytesMut::new(); let mut codec = ImapServerCodec::new(1024); for (test, expected) in tests { src.extend_from_slice(test); let got = codec.decode(&mut src); assert_eq!(expected, got); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); } } #[test] fn test_decoder_literal() { let tests = [ (b"".as_ref(), Ok(None)), (b"a login", Ok(None)), (b" {", Ok(None)), (b"5", Ok(None)), (b"}", Ok(None)), ( b"\r\n", Ok(Some(Event::ActionRequired(Action::SendLiteralAck(5)))), ), (b"a", Ok(None)), (b"l", Ok(None)), (b"i", Ok(None)), (b"ce", Ok(None)), (b" ", Ok(None)), ( b"password\r\n", Ok(Some(Event::Command( Command::new( "a", CommandBody::Login { username: AString::String(IString::Literal( Literal::try_from(b"alice".as_ref()).unwrap(), )), password: Secret::new(AString::Atom( AtomExt::try_from("password").unwrap(), )), }, ) .unwrap(), ))), ), ]; let mut src = BytesMut::new(); let mut codec = ImapServerCodec::new(1024); for (test, expected) in tests { src.extend_from_slice(test); let got = codec.decode(&mut src); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); assert_eq!(expected, got); } } #[test] fn test_decoder_error() { let tests = [ ( b"xxx\r\n".as_ref(), Err(ImapServerCodecError::ParsingFailed(BytesMut::from( b"xxx\r\n".as_ref(), ))), ), ( b"a noop\n", #[cfg(not(feature = "quirk_crlf_relaxed"))] Err(ImapServerCodecError::Framing(FramingError::NotCrLf)), #[cfg(feature = "quirk_crlf_relaxed")] Ok(Some(Event::Command(Command { tag: Tag::unvalidated("a"), body: CommandBody::Noop, }))), ), ( b"a login alice {16}\r\n", Ok(Some(Event::ActionRequired(Action::SendLiteralAck(16)))), ), ( b"aaaaaaaaaaaaaaaa\r\n", Ok(Some(Event::Command( Command::new( "a", CommandBody::login("alice", Literal::try_from("aaaaaaaaaaaaaaaa").unwrap()) .unwrap(), ) .unwrap(), ))), ), ( b"a login alice {17}\r\n", Ok(Some(Event::ActionRequired(Action::SendLiteralReject(17)))), ), ( b"a login alice {1-}\r\n", Err(ImapServerCodecError::ParsingFailed(BytesMut::from( b"a login alice {1-}\r\n".as_ref(), ))), ), ( // Ohhhhhh, IMAP :-/ b"a login alice }\r\n", Ok(Some(Event::Command( Command::new("a", CommandBody::login("alice", "}").unwrap()).unwrap(), ))), ), ]; let mut src = BytesMut::new(); let mut codec = ImapServerCodec::new(16); for (test, expected) in tests { src.extend_from_slice(test); dbg!(&src, &codec); let got = codec.decode(&mut src); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); assert_eq!(expected, got); } } } imap-codec-1.0.0/assets/docker/000077500000000000000000000000001447115025300162645ustar00rootroot00000000000000imap-codec-1.0.0/assets/docker/ImapTest/000077500000000000000000000000001447115025300200125ustar00rootroot00000000000000imap-codec-1.0.0/assets/docker/ImapTest/Dockerfile000066400000000000000000000010621447115025300220030ustar00rootroot00000000000000FROM ubuntu RUN apt-get update &&\ apt-get -y install\ autoconf\ automake\ bison\ curl\ flex\ gettext\ git\ libssl-dev\ libtool\ make\ pkg-config\ wget\ zlib1g\ zlib1g-dev RUN git clone https://github.com/dovecot/core dovecot WORKDIR dovecot RUN ./autogen.sh RUN PANDOC=false ./configure --enable-maintainer-mode RUN make WORKDIR .. RUN git clone https://github.com/dovecot/imaptest WORKDIR imaptest RUN ./autogen.sh RUN ./configure --with-dovecot=../dovecot RUN make RUN curl --location -O http://www.dovecot.org/tmp/dovecot-crlf WORKDIR .. imap-codec-1.0.0/assets/docker/grcov/000077500000000000000000000000001447115025300174045ustar00rootroot00000000000000imap-codec-1.0.0/assets/docker/grcov/Dockerfile000066400000000000000000000007331447115025300214010ustar00rootroot00000000000000# docker run -v :/opt/imap-codec -it from rust:latest RUN cargo install grcov RUN rustup component add llvm-tools-preview WORKDIR /opt/imap-codec ENV RUSTFLAGS="-Cinstrument-coverage" ENV LLVM_PROFILE_FILE="coverage-%m-%p.profraw" ENTRYPOINT ["/bin/bash"] #cargo clean #cargo test --workspace --all-features #grcov . --source-dir . --binary-path target/debug -t html --branch -o target/debug/coverage --keep-only '{src/**,imap-types/src/**}' imap-codec-1.0.0/deny.toml000066400000000000000000000002161447115025300153460ustar00rootroot00000000000000[sources] unknown-registry = "deny" unknown-git = "deny" [licenses] allow = [ "Apache-2.0", "MIT", "BSD-3-Clause", "Unicode-DFS-2016" ] imap-codec-1.0.0/imap-codec/000077500000000000000000000000001447115025300155145ustar00rootroot00000000000000imap-codec-1.0.0/imap-codec/Cargo.toml000066400000000000000000000041151447115025300174450ustar00rootroot00000000000000[package] name = "imap-codec" description = "Rock-solid and complete codec for IMAP" keywords = ["email", "imap", "codec", "parser"] categories = ["email", "parser-implementations", "encoding", "network-programming"] version = "1.0.0" authors = ["Damian Poddebniak "] repository = "https://github.com/duesee/imap-codec" license = "MIT OR Apache-2.0" edition = "2021" [features] default = ["quirk_rectify_numbers", "quirk_missing_text"] # arbitrary = ["imap-types/arbitrary"] bounded-static = ["dep:bounded-static", "imap-types/bounded-static"] serde = ["dep:serde", "chrono/serde", "imap-types/serde"] # IMAP starttls = ["imap-types/starttls"] # IMAP Extensions ext_condstore_qresync = ["imap-types/ext_condstore_qresync"] ext_login_referrals = ["imap-types/ext_login_referrals"] ext_mailbox_referrals = ["imap-types/ext_mailbox_referrals"] # # IMAP quirks # # These features bypass interoperability issues to allow safe processing of *almost* correct message. # # Make `\r` in `\r\n` optional. quirk_crlf_relaxed = [] # # Rectify (invalid) numbers. # Observed in ... # * Dovecot (`-1`) quirk_rectify_numbers = [] # Add missing `text` by adding [" "] "". # Observed in ... # * Gmail `* OK [HIGHESTMODSEQ ]\r\n` quirk_missing_text = [] [dependencies] abnf-core = "0.6.0" base64 = "0.21" bounded-static = { version = "0.5.0", optional = true } chrono = { version = "0.4", default-features = false, features = ["alloc"] } imap-types = { version = "1.0.0", default-features = false, features = ["unvalidated"] } nom = "7" serde = { version = "1", features = ["derive"], optional = true } thiserror = "1.0.29" log = "0.4.19" [dev-dependencies] criterion = "0.5.1" # Make `cargo +nightly -Z minimal-versions update` work. regex = "1.5.3" [[bench]] name = "serialize_command" harness = false [[bench]] name = "serialize_response" harness = false [[bench]] name = "parse_command" harness = false [[bench]] name = "parse_response" harness = false [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] imap-codec-1.0.0/imap-codec/README.md000066400000000000000000000032531447115025300167760ustar00rootroot00000000000000# imap-codec This library provides parsing and serialization for [IMAP4rev1] implementations. It is based on [imap-types] and a [rock-solid] and [well-documented] building block for IMAP client and server implementations in Rust. The complete [formal syntax] of IMAP4rev1 and several IMAP [extensions] are implemented. ## Features * Parsing works in streaming mode. `Incomplete` is returned when there is insufficient data to make a final decision. No message will be truncated. * Parsing is zero-copy by default. Allocation is avoided during parsing, but all messages can explicitly be converted into more flexible owned variants. * Fuzzing and property-based tests exercise the library. The library is fuzz-tested never to produce a message it can't parse itself. ## Usage ```rust use imap_codec::{ codec::{Decode, Encode}, command::Command, }; fn main() { let input = b"ABCD UID FETCH 1,2:* (BODY.PEEK[1.2.3.4.MIME]<42.1337>)\r\n"; let (remainder, parsed) = Command::decode(input).unwrap(); println!("# Parsed\n\n{:#?}\n\n", parsed); let buffer = parsed.encode().dump(); // Note: IMAP4rev1 may produce messages that are not valid UTF-8. println!("# Serialized\n\n{:?}", std::str::from_utf8(&buffer)); } ``` # License This crate is dual-licensed under Apache 2.0 and MIT terms. [IMAP4rev1]: https://tools.ietf.org/html/rfc3501 [imap-types]: https://docs.rs/imap-types/latest/imap_types/ [rock-solid]: https://github.com/duesee/imap-codec/tree/main/imap-codec/fuzz [well-documented]: https://docs.rs/imap-codec/latest/imap_codec/ [formal syntax]: https://tools.ietf.org/html/rfc3501#section-9 [extensions]: https://docs.rs/imap-codec/latest/imap_codec/#features imap-codec-1.0.0/imap-codec/benches/000077500000000000000000000000001447115025300171235ustar00rootroot00000000000000imap-codec-1.0.0/imap-codec/benches/parse_command.rs000066400000000000000000000013041447115025300222770ustar00rootroot00000000000000use criterion::{black_box, criterion_group, criterion_main, Criterion}; use imap_codec::{decode::Decoder, imap_types::command::Command, CommandCodec}; fn parse_command(input: &[u8]) -> Command { let (_remaining, cmd) = CommandCodec::default().decode(input).unwrap(); cmd } fn criterion_benchmark(c: &mut Criterion) { // # Setup let input = b"! FETCH 7 (BODY[1768386412.HEADER.FIELDS.NOT (\"\" `)] BODY[HEADER.FIELDS.NOT (\"\" !`)] BODY[HEADER.FIELDS.NOT (\"\" {0}\r\n)])\r\n"; c.bench_function("parse_command", |b| { b.iter(|| { parse_command(black_box(&input[..])); }) }); } criterion_group!(benches, criterion_benchmark); criterion_main!(benches); imap-codec-1.0.0/imap-codec/benches/parse_response.rs000066400000000000000000000022551447115025300225250ustar00rootroot00000000000000use criterion::{black_box, criterion_group, criterion_main, Criterion}; use imap_codec::{decode::Decoder, imap_types::response::Response, ResponseCodec}; fn parse_response(input: &[u8]) -> Response { let (_remaining, rsp) = ResponseCodec::default().decode(input).unwrap(); rsp } fn criterion_benchmark(c: &mut Criterion) { // # Setup let input = b"* 12 FETCH (FLAGS (\\Seen) INTERNALDATE \"17-Jul-1996 02:44:25 -0700\" RFC822.SIZE 4286 ENVELOPE (\"Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\" \"IMAP4rev1 WG mtg summary and minutes\" ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((NIL NIL \"imap\" \"cac.washington.edu\")) ((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\")(\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) NIL NIL \"\") BODY (\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL \"7BIT\" 3028 92))\r\n"; c.bench_function("parse_response", |b| { b.iter(|| { parse_response(black_box(&input[..])); }) }); } criterion_group!(benches, criterion_benchmark); criterion_main!(benches); imap-codec-1.0.0/imap-codec/benches/serialize_command.rs000066400000000000000000000031351447115025300231600ustar00rootroot00000000000000use std::num::NonZeroU32; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use imap_codec::{ encode::Encoder, imap_types::{ command::{Command, CommandBody}, fetch::{MacroOrMessageDataItemNames, MessageDataItemName, Section}, }, CommandCodec, }; fn criterion_benchmark(c: &mut Criterion) { // # Setup // // Create a `Command` ... // TODO: What about other instances of `Command`? let cmd = Command::new( "C123", CommandBody::fetch( "1:*,2,3,4,5,6,7,8,9", MacroOrMessageDataItemNames::MessageDataItemNames(vec![ MessageDataItemName::Rfc822Size, MessageDataItemName::BodyExt { section: Some(Section::Text(None)), peek: true, partial: Some((1, NonZeroU32::try_from(100).unwrap())), }, MessageDataItemName::BodyStructure, MessageDataItemName::Body, MessageDataItemName::Envelope, ]), true, ) .unwrap(), ) .unwrap(); // ... and preallocate some memory to serialize the `Command` into. let mut out = Vec::with_capacity(512); c.bench_function("serialize_command", |b| { b.iter(|| { let tmp = CommandCodec::default().encode(&cmd).dump(); out.extend_from_slice(black_box(&tmp)); // TODO: This should be a single instruction... should... out.clear(); }) }); } criterion_group!(benches, criterion_benchmark); criterion_main!(benches); imap-codec-1.0.0/imap-codec/benches/serialize_response.rs000066400000000000000000000021031447115025300233720ustar00rootroot00000000000000use std::num::NonZeroU32; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use imap_codec::{ encode::Encoder, imap_types::response::{Code, Response, Status}, ResponseCodec, }; fn criterion_benchmark(c: &mut Criterion) { // # Setup // // Create a `Response` ... // TODO: What about other instances of `Response`? let rsp = Response::Status( Status::ok( Some("ABC1234567".try_into().unwrap()), Some(Code::Unseen(NonZeroU32::new(12345).unwrap())), "xyz...", ) .unwrap(), ); // ... and preallocate some memory to serialize the `Command` into. let mut out = Vec::with_capacity(512); c.bench_function("serialize_response", |b| { b.iter(|| { let tmp = ResponseCodec::default().encode(&rsp).dump(); out.extend_from_slice(black_box(&tmp)); // TODO: This should be a single instruction... should... out.clear(); }) }); } criterion_group!(benches, criterion_benchmark); criterion_main!(benches); imap-codec-1.0.0/imap-codec/examples/000077500000000000000000000000001447115025300173325ustar00rootroot00000000000000imap-codec-1.0.0/imap-codec/examples/common/000077500000000000000000000000001447115025300206225ustar00rootroot00000000000000imap-codec-1.0.0/imap-codec/examples/common/common.rs000066400000000000000000000020151447115025300224560ustar00rootroot00000000000000#![allow(dead_code)] use std::io::Write; pub const COLOR_SERVER: &str = "\x1b[34m"; pub const COLOR_CLIENT: &str = "\x1b[31m"; pub const RESET: &str = "\x1b[0m"; #[derive(Clone, Copy, Debug)] pub enum Role { Client, Server, } pub fn read_more(buffer: &mut Vec, role: Role) { let prompt = if buffer.is_empty() { match role { Role::Client => "C: ", Role::Server => "S: ", } } else { ".. " }; let line = read_line(prompt, role); if line.trim() == "exit" { println!("Exiting."); std::process::exit(0); } buffer.extend_from_slice(line.as_bytes()); } fn read_line(prompt: &str, role: Role) -> String { match role { Role::Client => print!("{}{COLOR_CLIENT}", prompt), Role::Server => print!("{}{COLOR_SERVER}", prompt), } std::io::stdout().flush().unwrap(); let mut line = String::new(); std::io::stdin().read_line(&mut line).unwrap(); print!("{RESET}"); line.replace('\n', "\r\n") } imap-codec-1.0.0/imap-codec/examples/parse_command.rs000066400000000000000000000036431447115025300225160ustar00rootroot00000000000000use imap_codec::{ decode::{CommandDecodeError, Decoder}, CommandCodec, }; #[path = "common/common.rs"] mod common; use common::{read_more, COLOR_SERVER, RESET}; use crate::common::Role; const WELCOME: &str = r#"# Parsing of IMAP commands "C:" denotes the client, "S:" denotes the server, and ".." denotes the continuation of an (incomplete) command, e.g., due to the use of an IMAP literal. Note: "\n" will be automatically replaced by "\r\n". -------------------------------------------------------------------------------------------------- Enter IMAP command (or "exit"). "#; fn main() { println!("{}", WELCOME); let mut buffer = Vec::new(); loop { // Try to parse the first command in `buffer`. match CommandCodec::default().decode(&buffer) { // Parser succeeded. Ok((remaining, command)) => { // Do something with the command ... println!("{:#?}", command); // ... and proceed with the remaining data. buffer = remaining.to_vec(); } // Parser needs more data. Err(CommandDecodeError::Incomplete) => { // Read more data. read_more(&mut buffer, Role::Client); } // Parser needs more data, and a command continuation request is expected. Err(CommandDecodeError::LiteralFound { .. }) => { // Simulate literal acknowledgement ... println!("S: {COLOR_SERVER}+ {RESET}"); // ... and read more data. read_more(&mut buffer, Role::Client); } // Parser failed. Err(CommandDecodeError::Failed) => { println!("Error parsing command."); println!("Clearing buffer."); // Clear the buffer and proceed with loop. buffer.clear(); } } } } imap-codec-1.0.0/imap-codec/examples/parse_greeting.rs000066400000000000000000000026431447115025300227030ustar00rootroot00000000000000use imap_codec::{ decode::{Decoder, GreetingDecodeError}, GreetingCodec, }; #[path = "common/common.rs"] mod common; use common::read_more; use crate::common::Role; const WELCOME: &str = r#"# Parsing of IMAP greetings "S:" denotes the server. Note: "\n" will be automatically replaced by "\r\n". -------------------------------------------------------------------------------------------------- Enter IMAP greeting (or "exit"). "#; fn main() { println!("{}", WELCOME); let mut buffer = Vec::new(); loop { // Try to parse the first greeting in `buffer`. match GreetingCodec::default().decode(&buffer) { // Parser succeeded. Ok((remaining, greeting)) => { // Do something with the greeting ... println!("{:#?}", greeting); // ... and proceed with the remaining data. buffer = remaining.to_vec(); } // Parser needs more data. Err(GreetingDecodeError::Incomplete) => { // Read more data. read_more(&mut buffer, Role::Server); } // Parser failed. Err(GreetingDecodeError::Failed) => { println!("Error parsing greeting."); println!("Clearing buffer."); // Clear the buffer and proceed with loop. buffer.clear(); } } } } imap-codec-1.0.0/imap-codec/examples/parse_response.rs000066400000000000000000000040551447115025300227340ustar00rootroot00000000000000use imap_codec::{ decode::{Decoder, ResponseDecodeError}, ResponseCodec, }; #[path = "common/common.rs"] mod common; use common::read_more; use crate::common::Role; const WELCOME: &str = r#"# Parsing of IMAP responses "S:" denotes the server, and ".." denotes the continuation of an (incomplete) response, e.g., due to the use of an IMAP literal. Note: "\n" will be automatically replaced by "\r\n". -------------------------------------------------------------------------------------------------- Enter IMAP response (or "exit"). "#; fn main() { println!("{}", WELCOME); let mut buffer = Vec::new(); loop { // Try to parse the first response in `buffer`. match ResponseCodec::default().decode(&buffer) { // Parser succeeded. Ok((remaining, response)) => { // Do something with the response ... println!("{:#?}", response); // ... and proceed with the remaining data. buffer = remaining.to_vec(); } // Parser needs more data. Err(ResponseDecodeError::Incomplete) => { // Read more data. read_more(&mut buffer, Role::Server); } // Parser needs more data. // // A client MUST receive any literal and can't reject it. However, if the literal is too // large, the client would have the (semi-optimal) option to still *read it* but discard // the data chunk by chunk. It could also close the connection. This is why we have this // option. Err(ResponseDecodeError::LiteralFound { .. }) => { // Read more data. read_more(&mut buffer, Role::Server); } // Parser failed. Err(ResponseDecodeError::Failed) => { println!("Error parsing response."); println!("Clearing buffer."); // Clear the buffer and proceed with loop. buffer.clear(); } } } } imap-codec-1.0.0/imap-codec/fuzz/000077500000000000000000000000001447115025300165125ustar00rootroot00000000000000imap-codec-1.0.0/imap-codec/fuzz/.gitignore000066400000000000000000000000311447115025300204740ustar00rootroot00000000000000 target corpus artifacts imap-codec-1.0.0/imap-codec/fuzz/Cargo.toml000066400000000000000000000034211447115025300204420ustar00rootroot00000000000000[package] name = "imap-codec-fuzz" version = "0.0.0" authors = ["Automatically generated"] publish = false edition = "2021" license = "MIT OR Apache-2.0" [package.metadata] cargo-fuzz = true [features] # # IMAP starttls = ["imap-codec/starttls"] # IMAP Extensions ext_condstore_qresync = ["imap-codec/ext_condstore_qresync"] ext_login_referrals = ["imap-codec/ext_login_referrals"] ext_mailbox_referrals = ["imap-codec/ext_mailbox_referrals"] # IMAP quirks quirk_crlf_relaxed = ["imap-codec/quirk_crlf_relaxed"] # # Use (most) IMAP extensions. ext = [ "starttls", "ext_condstore_qresync", #"ext_login_referrals", #"ext_mailbox_referrals", ] # Enable `Debug`-printing during parsing. This is useful to analyze crashes. debug = [] # Enable testing of incomplete fragments. split = [] [dependencies] libfuzzer-sys = "0.4" imap-codec = { path = "..", features = ["arbitrary"] } [[bin]] name = "greeting" path = "fuzz_targets/greeting.rs" test = false doc = false [[bin]] name = "command" path = "fuzz_targets/command.rs" test = false doc = false [[bin]] name = "response" path = "fuzz_targets/response.rs" test = false doc = false [[bin]] name = "authenticate_data" path = "fuzz_targets/authenticate_data.rs" test = false doc = false [[bin]] name = "greeting_to_bytes_and_back" path = "fuzz_targets/greeting_to_bytes_and_back.rs" test = false doc = false [[bin]] name = "command_to_bytes_and_back" path = "fuzz_targets/command_to_bytes_and_back.rs" test = false doc = false [[bin]] name = "response_to_bytes_and_back" path = "fuzz_targets/response_to_bytes_and_back.rs" test = false doc = false [[bin]] name = "authenticate_data_to_bytes_and_back" path = "fuzz_targets/authenticate_data_to_bytes_and_back.rs" test = false doc = false imap-codec-1.0.0/imap-codec/fuzz/README.md000066400000000000000000000162051447115025300177750ustar00rootroot00000000000000# Fuzzing ## Setup Cargo fuzz requires a nightly compiler. You can install it via ... ```sh rustup install nightly ``` ... and invoke it by adding the "+nightly" flag to cargo like ... ```sh cargo +nightly fuzz ``` Alternatively, you can override the default toolchain for the current directory by using ... ```sh rustup override set nightly ``` Don't forget to unset it with ... ```sh rustup override unset ``` ... as imap-codec should work with stable. ## Provided fuzz targets You can start the fuzzing process by running ... ```sh cargo +nightly fuzz run ``` ... with `` being ... | Name of the fuzz target `` | Purpose | Expectation | |------------------------------------|------------------------|----------------| | `greeting` | Test parsing | Must not fail. | | `command` | Test parsing | Must not fail. | | `response` | Test parsing | Must not fail. | | `greeting_to_bytes_and_back` | Test misuse-resistance | Must not fail. | | `command_to_bytes_and_back` | Test misuse-resistance | Must not fail. | | `response_to_bytes_and_back` | Test misuse-resistance | Must not fail. | Three first three fuzz targets are used to test the parsing routines. The fuzzers all do the same: try to parse the input from libFuzzer (and hope that the parsers don't crash), then, if parsing was successful, serialize the obtained object (and hope that the serialization routines don't crash), and then, parse the serialized output again and compare it to the first one (and hoping that they match). This is motivated by the fact, that the library must certainly be able to parse the data it has produced on its own. The last three fuzz targets are used to test for misuse-resistance and currently are a work-in-progress. The `Greeting`/`Command`/`Response`/... structs implements the `Arbitrary` trait that will produce a random instance of the type. Any instance generated in this way must be parsable and valid. It should not be possible to create a message object via the API, which is invalid according to the IMAP specification. If a crash was found, it is helpful to uncomment the `println!(...)` statements in the fuzz target and rerun the crashing input. ## Try to be more effective * Use `terminals.dict` as fuzzing dictionary. It contains all terminals (>1 character) from the IMAP4rev1 formal syntax and ABNFs core rules. * The `imap.dict` dictionary contains a full IMAP trace. `blns.dict` is the "big list of naughty strings". * Decrease the the input size to e.g. 64 bytes. Short inputs might still trigger complex parsing routines. * Use multiple processes. * Try to use `-ascii_only` to exclude inputs, which are less likely to be valid (useful to test serializing.) ```sh cargo +nightly fuzz run -j 32 -- -dict=terminals.dict -max_len=64 -only_ascii=1 ``` ## Structured fuzzing with `Arbitrary` This beautiful `Command`¹ ... ```rust Command { tag: Tag( "!", ), body: Fetch { sequence_set: SequenceSet( [ Single( Value( 7, ), ), ], ), attributes: FetchAttributes( [ BodyExt { section: Some( HeaderFieldsNot( Some( Part( NonEmptyVec( [ 1768386412, ], ), ), ), NonEmptyVec( [ String( Quoted( Quoted( "", ), ), ), Atom( Atom( "`", ), ), ], ), ), ), partial: None, peek: false, }, BodyExt { section: Some( HeaderFieldsNot( None, NonEmptyVec( [ String( Quoted( Quoted( "", ), ), ), Atom( Atom( "!`", ), ), ], ), ), ), partial: None, peek: false, }, BodyExt { section: Some( HeaderFieldsNot( None, NonEmptyVec( [ String( Quoted( Quoted( "", ), ), ), String( Literal( Literal( [], ), ), ), ], ), ), ), partial: None, peek: false, }, ], ), uid: false, }, } ``` ... was generated by `Command::arbitrary(...)` and serializes into ... ```imap ! FETCH 7 (BODY[1768386412.HEADER.FIELDS.NOT ("" `)] BODY[HEADER.FIELDS.NOT ("" !`)] BODY[HEADER.FIELDS.NOT ("" {0} )]) ``` # Known crashes I am not able to crash the `greeting`, `command`, and `response` targets anymore. However, they already uncovered interesting serialization issues. Similarly, I can not create any invalid `Greeting` or `Command` anymore. Please try for yourself and file a bug report if you can do it! ¹ This may become outdated when new versions are published.imap-codec-1.0.0/imap-codec/fuzz/blns.dict000066400000000000000000002632761447115025300203350ustar00rootroot00000000000000"\x75\x6e\x64\x65\x66\x69\x6e\x65\x64" "\x75\x6e\x64\x65\x66" "\x6e\x75\x6c\x6c" "\x4e\x55\x4c\x4c" "\x28\x6e\x75\x6c\x6c\x29" "\x6e\x69\x6c" "\x4e\x49\x4c" "\x74\x72\x75\x65" "\x66\x61\x6c\x73\x65" "\x54\x72\x75\x65" "\x46\x61\x6c\x73\x65" "\x54\x52\x55\x45" "\x46\x41\x4c\x53\x45" "\x4e\x6f\x6e\x65" "\x68\x61\x73\x4f\x77\x6e\x50\x72\x6f\x70\x65\x72\x74\x79" "\x74\x68\x65\x6e" "\x5c" "\x5c\x5c" "\x30" "\x31" "\x31\x2e\x30\x30" "\x24\x31\x2e\x30\x30" "\x31\x2f\x32" "\x31\x45\x32" "\x31\x45\x30\x32" "\x31\x45\x2b\x30\x32" "\x2d\x31" "\x2d\x31\x2e\x30\x30" "\x2d\x24\x31\x2e\x30\x30" "\x2d\x31\x2f\x32" "\x2d\x31\x45\x32" "\x2d\x31\x45\x30\x32" "\x2d\x31\x45\x2b\x30\x32" "\x31\x2f\x30" "\x30\x2f\x30" "\x2d\x32\x31\x34\x37\x34\x38\x33\x36\x34\x38\x2f\x2d\x31" "\x2d\x39\x32\x32\x33\x33\x37\x32\x30\x33\x36\x38\x35\x34\x37\x37\x35\x38\x30\x38\x2f\x2d\x31" "\x2d\x30" "\x2d\x30\x2e\x30" "\x2b\x30" "\x2b\x30\x2e\x30" "\x30\x2e\x30\x30" "\x30\x2e\x2e\x30" "\x2e" "\x30\x2e\x30\x2e\x30" "\x30\x2c\x30\x30" "\x30\x2c\x2c\x30" "\x2c" "\x30\x2c\x30\x2c\x30" "\x30\x2e\x30\x2f\x30" "\x31\x2e\x30\x2f\x30\x2e\x30" "\x30\x2e\x30\x2f\x30\x2e\x30" "\x31\x2c\x30\x2f\x30\x2c\x30" "\x30\x2c\x30\x2f\x30\x2c\x30" "\x2d\x2d\x31" "\x2d" "\x2d\x2e" "\x2d\x2c" "\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39" "\x4e\x61\x4e" "\x49\x6e\x66\x69\x6e\x69\x74\x79" "\x2d\x49\x6e\x66\x69\x6e\x69\x74\x79" "\x49\x4e\x46" "\x31\x23\x49\x4e\x46" "\x2d\x31\x23\x49\x4e\x44" "\x31\x23\x51\x4e\x41\x4e" "\x31\x23\x53\x4e\x41\x4e" "\x31\x23\x49\x4e\x44" "\x30\x78\x30" "\x30\x78\x66\x66\x66\x66\x66\x66\x66\x66" "\x30\x78\x66\x66\x66\x66\x66\x66\x66\x66\x66\x66\x66\x66\x66\x66\x66\x66" "\x30\x78\x61\x62\x61\x64\x31\x64\x65\x61" "\x31\x32\x33\x34\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39" "\x31\x2c\x30\x30\x30\x2e\x30\x30" "\x31\x20\x30\x30\x30\x2e\x30\x30" "\x31\x27\x30\x30\x30\x2e\x30\x30" "\x31\x2c\x30\x30\x30\x2c\x30\x30\x30\x2e\x30\x30" "\x31\x20\x30\x30\x30\x20\x30\x30\x30\x2e\x30\x30" "\x31\x27\x30\x30\x30\x27\x30\x30\x30\x2e\x30\x30" "\x31\x2e\x30\x30\x30\x2c\x30\x30" "\x31\x20\x30\x30\x30\x2c\x30\x30" "\x31\x27\x30\x30\x30\x2c\x30\x30" "\x31\x2e\x30\x30\x30\x2e\x30\x30\x30\x2c\x30\x30" "\x31\x20\x30\x30\x30\x20\x30\x30\x30\x2c\x30\x30" "\x31\x27\x30\x30\x30\x27\x30\x30\x30\x2c\x30\x30" "\x30\x31\x30\x30\x30" "\x30\x38" "\x30\x39" "\x32\x2e\x32\x32\x35\x30\x37\x33\x38\x35\x38\x35\x30\x37\x32\x30\x31\x31\x65\x2d\x33\x30\x38" "\x2c\x2e\x2f\x3b\x27\x5b\x5d\x5c\x2d\x3d" "\x3c\x3e\x3f\x3a\x22\x7b\x7d\x7c\x5f\x2b" "\x21\x40\x23\x24\x25\x5e\x26\x2a\x28\x29\x60\x7e" "\x01\x02\x03\x04\x05\x06\x07\x08\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f" "\xc2\x80\xc2\x81\xc2\x82\xc2\x83\xc2\x84\xc2\x86\xc2\x87\xc2\x88\xc2\x89\xc2\x8a\xc2\x8b\xc2\x8c\xc2\x8d\xc2\x8e\xc2\x8f\xc2\x90\xc2\x91\xc2\x92\xc2\x93\xc2\x94\xc2\x95\xc2\x96\xc2\x97\xc2\x98\xc2\x99\xc2\x9a\xc2\x9b\xc2\x9c\xc2\x9d\xc2\x9e\xc2\x9f" "\x09\x0b\x0c\x20\xc2\x85\xc2\xa0\xe1\x9a\x80\xe2\x80\x82\xe2\x80\x83\xe2\x80\x82\xe2\x80\x83\xe2\x80\x84\xe2\x80\x85\xe2\x80\x86\xe2\x80\x87\xe2\x80\x88\xe2\x80\x89\xe2\x80\x8a\xe2\x80\x8b\xe2\x80\xa8\xe2\x80\xa9\xe2\x80\xaf\xe2\x81\x9f\xe3\x80\x80" "\xc2\xad\xd8\x80\xd8\x81\xd8\x82\xd8\x83\xd8\x84\xd8\x85\xd8\x9c\xdb\x9d\xdc\x8f\xe1\xa0\x8e\xe2\x80\x8b\xe2\x80\x8c\xe2\x80\x8d\xe2\x80\x8e\xe2\x80\x8f\xe2\x80\xaa\xe2\x80\xab\xe2\x80\xac\xe2\x80\xad\xe2\x80\xae\xe2\x81\xa0\xe2\x81\xa1\xe2\x81\xa2\xe2\x81\xa3\xe2\x81\xa4\xe2\x81\xa6\xe2\x81\xa7\xe2\x81\xa8\xe2\x81\xa9\xe2\x81\xaa\xe2\x81\xab\xe2\x81\xac\xe2\x81\xad\xe2\x81\xae\xe2\x81\xaf\xef\xbb\xbf\xef\xbf\xb9\xef\xbf\xba\xef\xbf\xbb\xf0\x91\x82\xbd\xf0\x9b\xb2\xa0\xf0\x9b\xb2\xa1\xf0\x9b\xb2\xa2\xf0\x9b\xb2\xa3\xf0\x9d\x85\xb3\xf0\x9d\x85\xb4\xf0\x9d\x85\xb5\xf0\x9d\x85\xb6\xf0\x9d\x85\xb7\xf0\x9d\x85\xb8\xf0\x9d\x85\xb9\xf0\x9d\x85\xba\xf3\xa0\x80\x81\xf3\xa0\x80\xa0\xf3\xa0\x80\xa1\xf3\xa0\x80\xa2\xf3\xa0\x80\xa3\xf3\xa0\x80\xa4\xf3\xa0\x80\xa5\xf3\xa0\x80\xa6\xf3\xa0\x80\xa7\xf3\xa0\x80\xa8\xf3\xa0\x80\xa9\xf3\xa0\x80\xaa\xf3\xa0\x80\xab\xf3\xa0\x80\xac\xf3\xa0\x80\xad\xf3\xa0\x80\xae\xf3\xa0\x80\xaf\xf3\xa0\x80\xb0\xf3\xa0\x80\xb1\xf3\xa0\x80\xb2\xf3\xa0\x80\xb3\xf3\xa0\x80\xb4\xf3\xa0\x80\xb5\xf3\xa0\x80\xb6\xf3\xa0\x80\xb7\xf3\xa0\x80\xb8\xf3\xa0\x80\xb9\xf3\xa0\x80\xba\xf3\xa0\x80\xbb\xf3\xa0\x80\xbc\xf3\xa0\x80\xbd\xf3\xa0\x80\xbe\xf3\xa0\x80\xbf\xf3\xa0\x81\x80\xf3\xa0\x81\x81\xf3\xa0\x81\x82\xf3\xa0\x81\x83\xf3\xa0\x81\x84\xf3\xa0\x81\x85\xf3\xa0\x81\x86\xf3\xa0\x81\x87\xf3\xa0\x81\x88\xf3\xa0\x81\x89\xf3\xa0\x81\x8a\xf3\xa0\x81\x8b\xf3\xa0\x81\x8c\xf3\xa0\x81\x8d\xf3\xa0\x81\x8e\xf3\xa0\x81\x8f\xf3\xa0\x81\x90\xf3\xa0\x81\x91\xf3\xa0\x81\x92\xf3\xa0\x81\x93\xf3\xa0\x81\x94\xf3\xa0\x81\x95\xf3\xa0\x81\x96\xf3\xa0\x81\x97\xf3\xa0\x81\x98\xf3\xa0\x81\x99\xf3\xa0\x81\x9a\xf3\xa0\x81\x9b\xf3\xa0\x81\x9c\xf3\xa0\x81\x9d\xf3\xa0\x81\x9e\xf3\xa0\x81\x9f\xf3\xa0\x81\xa0\xf3\xa0\x81\xa1\xf3\xa0\x81\xa2\xf3\xa0\x81\xa3\xf3\xa0\x81\xa4\xf3\xa0\x81\xa5\xf3\xa0\x81\xa6\xf3\xa0\x81\xa7\xf3\xa0\x81\xa8\xf3\xa0\x81\xa9\xf3\xa0\x81\xaa\xf3\xa0\x81\xab\xf3\xa0\x81\xac\xf3\xa0\x81\xad\xf3\xa0\x81\xae\xf3\xa0\x81\xaf\xf3\xa0\x81\xb0\xf3\xa0\x81\xb1\xf3\xa0\x81\xb2\xf3\xa0\x81\xb3\xf3\xa0\x81\xb4\xf3\xa0\x81\xb5\xf3\xa0\x81\xb6\xf3\xa0\x81\xb7\xf3\xa0\x81\xb8\xf3\xa0\x81\xb9\xf3\xa0\x81\xba\xf3\xa0\x81\xbb\xf3\xa0\x81\xbc\xf3\xa0\x81\xbd\xf3\xa0\x81\xbe\xf3\xa0\x81\xbf" "\xef\xbb\xbf" "\xef\xbf\xbe" "\xce\xa9\xe2\x89\x88\xc3\xa7\xe2\x88\x9a\xe2\x88\xab\xcb\x9c\xc2\xb5\xe2\x89\xa4\xe2\x89\xa5\xc3\xb7" "\xc3\xa5\xc3\x9f\xe2\x88\x82\xc6\x92\xc2\xa9\xcb\x99\xe2\x88\x86\xcb\x9a\xc2\xac\xe2\x80\xa6\xc3\xa6" "\xc5\x93\xe2\x88\x91\xc2\xb4\xc2\xae\xe2\x80\xa0\xc2\xa5\xc2\xa8\xcb\x86\xc3\xb8\xcf\x80\xe2\x80\x9c\xe2\x80\x98" "\xc2\xa1\xe2\x84\xa2\xc2\xa3\xc2\xa2\xe2\x88\x9e\xc2\xa7\xc2\xb6\xe2\x80\xa2\xc2\xaa\xc2\xba\xe2\x80\x93\xe2\x89\xa0" "\xc2\xb8\xcb\x9b\xc3\x87\xe2\x97\x8a\xc4\xb1\xcb\x9c\xc3\x82\xc2\xaf\xcb\x98\xc2\xbf" "\xc3\x85\xc3\x8d\xc3\x8e\xc3\x8f\xcb\x9d\xc3\x93\xc3\x94\xef\xa3\xbf\xc3\x92\xc3\x9a\xc3\x86\xe2\x98\x83" "\xc5\x92\xe2\x80\x9e\xc2\xb4\xe2\x80\xb0\xcb\x87\xc3\x81\xc2\xa8\xcb\x86\xc3\x98\xe2\x88\x8f\xe2\x80\x9d\xe2\x80\x99" "\x60\xe2\x81\x84\xe2\x82\xac\xe2\x80\xb9\xe2\x80\xba\xef\xac\x81\xef\xac\x82\xe2\x80\xa1\xc2\xb0\xc2\xb7\xe2\x80\x9a\xe2\x80\x94\xc2\xb1" "\xe2\x85\x9b\xe2\x85\x9c\xe2\x85\x9d\xe2\x85\x9e" "\xd0\x81\xd0\x82\xd0\x83\xd0\x84\xd0\x85\xd0\x86\xd0\x87\xd0\x88\xd0\x89\xd0\x8a\xd0\x8b\xd0\x8c\xd0\x8d\xd0\x8e\xd0\x8f\xd0\x90\xd0\x91\xd0\x92\xd0\x93\xd0\x94\xd0\x95\xd0\x96\xd0\x97\xd0\x98\xd0\x99\xd0\x9a\xd0\x9b\xd0\x9c\xd0\x9d\xd0\x9e\xd0\x9f\xd0\xa0\xd0\xa1\xd0\xa2\xd0\xa3\xd0\xa4\xd0\xa5\xd0\xa6\xd0\xa7\xd0\xa8\xd0\xa9\xd0\xaa\xd0\xab\xd0\xac\xd0\xad\xd0\xae\xd0\xaf\xd0\xb0\xd0\xb1\xd0\xb2\xd0\xb3\xd0\xb4\xd0\xb5\xd0\xb6\xd0\xb7\xd0\xb8\xd0\xb9\xd0\xba\xd0\xbb\xd0\xbc\xd0\xbd\xd0\xbe\xd0\xbf\xd1\x80\xd1\x81\xd1\x82\xd1\x83\xd1\x84\xd1\x85\xd1\x86\xd1\x87\xd1\x88\xd1\x89\xd1\x8a\xd1\x8b\xd1\x8c\xd1\x8d\xd1\x8e\xd1\x8f" "\xd9\xa0\xd9\xa1\xd9\xa2\xd9\xa3\xd9\xa4\xd9\xa5\xd9\xa6\xd9\xa7\xd9\xa8\xd9\xa9" "\xe2\x81\xb0\xe2\x81\xb4\xe2\x81\xb5" "\xe2\x82\x80\xe2\x82\x81\xe2\x82\x82" "\xe2\x81\xb0\xe2\x81\xb4\xe2\x81\xb5\xe2\x82\x80\xe2\x82\x81\xe2\x82\x82" "\xe0\xb8\x94\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\x20\xe0\xb8\x94\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\x20\xe0\xb8\x94\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x89\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87\xe0\xb9\x87" "\x27" "\x22" "\x27\x27" "\x22\x22" "\x27\x22\x27" "\x22\x27\x27\x27\x27\x22\x27\x22" "\x22\x27\x22\x27\x22\x27\x27\x27\x27\x22" "\x3c\x66\x6f\x6f\x20\x76\x61\x6c\x3d\xe2\x80\x9c\x62\x61\x72\xe2\x80\x9d\x20\x2f\x3e" "\x3c\x66\x6f\x6f\x20\x76\x61\x6c\x3d\xe2\x80\x9c\x62\x61\x72\xe2\x80\x9d\x20\x2f\x3e" "\x3c\x66\x6f\x6f\x20\x76\x61\x6c\x3d\xe2\x80\x9d\x62\x61\x72\xe2\x80\x9c\x20\x2f\x3e" "\x3c\x66\x6f\x6f\x20\x76\x61\x6c\x3d\x60\x62\x61\x72\x27\x20\x2f\x3e" "\xe7\x94\xb0\xe4\xb8\xad\xe3\x81\x95\xe3\x82\x93\xe3\x81\xab\xe3\x81\x82\xe3\x81\x92\xe3\x81\xa6\xe4\xb8\x8b\xe3\x81\x95\xe3\x81\x84" "\xe3\x83\x91\xe3\x83\xbc\xe3\x83\x86\xe3\x82\xa3\xe3\x83\xbc\xe3\x81\xb8\xe8\xa1\x8c\xe3\x81\x8b\xe3\x81\xaa\xe3\x81\x84\xe3\x81\x8b" "\xe5\x92\x8c\xe8\xa3\xbd\xe6\xbc\xa2\xe8\xaa\x9e" "\xe9\x83\xa8\xe8\x90\xbd\xe6\xa0\xbc" "\xec\x82\xac\xed\x9a\x8c\xea\xb3\xbc\xed\x95\x99\xec\x9b\x90\x20\xec\x96\xb4\xed\x95\x99\xec\x97\xb0\xea\xb5\xac\xec\x86\x8c" "\xec\xb0\xa6\xec\xb0\xa8\xeb\xa5\xbc\x20\xed\x83\x80\xea\xb3\xa0\x20\xec\x98\xa8\x20\xed\x8e\xb2\xec\x8b\x9c\xeb\xa7\xa8\xea\xb3\xbc\x20\xec\x91\x9b\xeb\x8b\xa4\xeb\xa6\xac\x20\xeb\x98\xa0\xeb\xb0\xa9\xea\xb0\x81\xed\x95\x98" "\xe7\xa4\xbe\xe6\x9c\x83\xe7\xa7\x91\xe5\xad\xb8\xe9\x99\xa2\xe8\xaa\x9e\xe5\xad\xb8\xe7\xa0\x94\xe7\xa9\xb6\xe6\x89\x80" "\xec\x9a\xb8\xeb\x9e\x80\xeb\xb0\x94\xed\x86\xa0\xeb\xa5\xb4" "\xf0\xa0\x9c\x8e\xf0\xa0\x9c\xb1\xf0\xa0\x9d\xb9\xf0\xa0\xb1\x93\xf0\xa0\xb1\xb8\xf0\xa0\xb2\x96\xf0\xa0\xb3\x8f" "\xf0\x90\x90\x9c\x20\xf0\x90\x90\x94\xf0\x90\x90\x87\xf0\x90\x90\x9d\xf0\x90\x90\x80\xf0\x90\x90\xa1\xf0\x90\x90\x87\xf0\x90\x90\x93\x20\xf0\x90\x90\x99\xf0\x90\x90\x8a\xf0\x90\x90\xa1\xf0\x90\x90\x9d\xf0\x90\x90\x93\x2f\xf0\x90\x90\x9d\xf0\x90\x90\x87\xf0\x90\x90\x97\xf0\x90\x90\x8a\xf0\x90\x90\xa4\xf0\x90\x90\x94\x20\xf0\x90\x90\x92\xf0\x90\x90\x8b\xf0\x90\x90\x97\x20\xf0\x90\x90\x92\xf0\x90\x90\x8c\x20\xf0\x90\x90\x9c\x20\xf0\x90\x90\xa1\xf0\x90\x90\x80\xf0\x90\x90\x96\xf0\x90\x90\x87\xf0\x90\x90\xa4\xf0\x90\x90\x93\xf0\x90\x90\x9d\x20\xf0\x90\x90\xb1\xf0\x90\x91\x82\x20\xf0\x90\x91\x84\x20\xf0\x90\x90\x94\xf0\x90\x90\x87\xf0\x90\x90\x9d\xf0\x90\x90\x80\xf0\x90\x90\xa1\xf0\x90\x90\x87\xf0\x90\x90\x93\x20\xf0\x90\x90\x8f\xf0\x90\x90\x86\xf0\x90\x90\x85\xf0\x90\x90\xa4\xf0\x90\x90\x86\xf0\x90\x90\x9a\xf0\x90\x90\x8a\xf0\x90\x90\xa1\xf0\x90\x90\x9d\xf0\x90\x90\x86\xf0\x90\x90\x93\xf0\x90\x90\x86" "\xe8\xa1\xa8\xe3\x83\x9d\xe3\x81\x82\x41\xe9\xb7\x97\xc5\x92\xc3\xa9\xef\xbc\xa2\xe9\x80\x8d\xc3\x9c\xc3\x9f\xc2\xaa\xc4\x85\xc3\xb1\xe4\xb8\x82\xe3\x90\x80\xf0\xa0\x80\x80" "\xc8\xba" "\xc8\xbe" "\xe3\x83\xbd\xe0\xbc\xbc\xe0\xba\x88\xd9\x84\xcd\x9c\xe0\xba\x88\xe0\xbc\xbd\xef\xbe\x89\x20\xe3\x83\xbd\xe0\xbc\xbc\xe0\xba\x88\xd9\x84\xcd\x9c\xe0\xba\x88\xe0\xbc\xbd\xef\xbe\x89" "\x28\xef\xbd\xa1\xe2\x97\x95\x20\xe2\x88\x80\x20\xe2\x97\x95\xef\xbd\xa1\x29" "\xef\xbd\x80\xef\xbd\xa8\x28\xc2\xb4\xe2\x88\x80\xef\xbd\x80\xe2\x88\xa9" "\x5f\x5f\xef\xbe\x9b\x28\x2c\x5f\x2c\x2a\x29" "\xe3\x83\xbb\x28\xef\xbf\xa3\xe2\x88\x80\xef\xbf\xa3\x29\xe3\x83\xbb\x3a\x2a\x3a" "\xef\xbe\x9f\xef\xbd\xa5\xe2\x9c\xbf\xe3\x83\xbe\xe2\x95\xb2\x28\xef\xbd\xa1\xe2\x97\x95\xe2\x80\xbf\xe2\x97\x95\xef\xbd\xa1\x29\xe2\x95\xb1\xe2\x9c\xbf\xef\xbd\xa5\xef\xbe\x9f" "\x2c\xe3\x80\x82\xe3\x83\xbb\x3a\x2a\x3a\xe3\x83\xbb\xe3\x82\x9c\xe2\x80\x99\x28\x20\xe2\x98\xbb\x20\xcf\x89\x20\xe2\x98\xbb\x20\x29\xe3\x80\x82\xe3\x83\xbb\x3a\x2a\x3a\xe3\x83\xbb\xe3\x82\x9c\xe2\x80\x99" "\x28\xe2\x95\xaf\xc2\xb0\xe2\x96\xa1\xc2\xb0\xef\xbc\x89\xe2\x95\xaf\xef\xb8\xb5\x20\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x29" "\x28\xef\xbe\x89\xe0\xb2\xa5\xe7\x9b\x8a\xe0\xb2\xa5\xef\xbc\x89\xef\xbe\x89\xef\xbb\xbf\x20\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb" "\xe2\x94\xac\xe2\x94\x80\xe2\x94\xac\xe3\x83\x8e\x28\x20\xc2\xba\x20\x5f\x20\xc2\xba\xe3\x83\x8e\x29" "\x28\x20\xcd\xa1\xc2\xb0\x20\xcd\x9c\xca\x96\x20\xcd\xa1\xc2\xb0\x29" "\xc2\xaf\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf" "\xf0\x9f\x98\x8d" "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd" "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xb0\x20\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb0\x20\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xb1\x20\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb1\x20\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f" "\xf0\x9f\x91\xbe\x20\xf0\x9f\x99\x87\x20\xf0\x9f\x92\x81\x20\xf0\x9f\x99\x85\x20\xf0\x9f\x99\x86\x20\xf0\x9f\x99\x8b\x20\xf0\x9f\x99\x8e\x20\xf0\x9f\x99\x8d" "\xf0\x9f\x90\xb5\x20\xf0\x9f\x99\x88\x20\xf0\x9f\x99\x89\x20\xf0\x9f\x99\x8a" "\xe2\x9d\xa4\xef\xb8\x8f\x20\xf0\x9f\x92\x94\x20\xf0\x9f\x92\x8c\x20\xf0\x9f\x92\x95\x20\xf0\x9f\x92\x9e\x20\xf0\x9f\x92\x93\x20\xf0\x9f\x92\x97\x20\xf0\x9f\x92\x96\x20\xf0\x9f\x92\x98\x20\xf0\x9f\x92\x9d\x20\xf0\x9f\x92\x9f\x20\xf0\x9f\x92\x9c\x20\xf0\x9f\x92\x9b\x20\xf0\x9f\x92\x9a\x20\xf0\x9f\x92\x99" "\xe2\x9c\x8b\xf0\x9f\x8f\xbf\x20\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbf\x20\xf0\x9f\x91\x90\xf0\x9f\x8f\xbf\x20\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbf\x20\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbf\x20\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbf" "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6\x20\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6\x20\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6\x20\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\x20\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6\x20\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6\x20\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6\x20\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6" "\xf0\x9f\x9a\xbe\x20\xf0\x9f\x86\x92\x20\xf0\x9f\x86\x93\x20\xf0\x9f\x86\x95\x20\xf0\x9f\x86\x96\x20\xf0\x9f\x86\x97\x20\xf0\x9f\x86\x99\x20\xf0\x9f\x8f\xa7" "\x30\xef\xb8\x8f\xe2\x83\xa3\x20\x31\xef\xb8\x8f\xe2\x83\xa3\x20\x32\xef\xb8\x8f\xe2\x83\xa3\x20\x33\xef\xb8\x8f\xe2\x83\xa3\x20\x34\xef\xb8\x8f\xe2\x83\xa3\x20\x35\xef\xb8\x8f\xe2\x83\xa3\x20\x36\xef\xb8\x8f\xe2\x83\xa3\x20\x37\xef\xb8\x8f\xe2\x83\xa3\x20\x38\xef\xb8\x8f\xe2\x83\xa3\x20\x39\xef\xb8\x8f\xe2\x83\xa3\x20\xf0\x9f\x94\x9f" "\xf0\x9f\x87\xba\xf0\x9f\x87\xb8\xf0\x9f\x87\xb7\xf0\x9f\x87\xba\xf0\x9f\x87\xb8\x20\xf0\x9f\x87\xa6\xf0\x9f\x87\xab\xf0\x9f\x87\xa6\xf0\x9f\x87\xb2\xf0\x9f\x87\xb8" "\xf0\x9f\x87\xba\xf0\x9f\x87\xb8\xf0\x9f\x87\xb7\xf0\x9f\x87\xba\xf0\x9f\x87\xb8\xf0\x9f\x87\xa6\xf0\x9f\x87\xab\xf0\x9f\x87\xa6\xf0\x9f\x87\xb2" "\xf0\x9f\x87\xba\xf0\x9f\x87\xb8\xf0\x9f\x87\xb7\xf0\x9f\x87\xba\xf0\x9f\x87\xb8\xf0\x9f\x87\xa6" "\xef\xbc\x91\xef\xbc\x92\xef\xbc\x93" "\xd9\xa1\xd9\xa2\xd9\xa3" "\xd8\xab\xd9\x85\x20\xd9\x86\xd9\x81\xd8\xb3\x20\xd8\xb3\xd9\x82\xd8\xb7\xd8\xaa\x20\xd9\x88\xd8\xa8\xd8\xa7\xd9\x84\xd8\xaa\xd8\xad\xd8\xaf\xd9\x8a\xd8\xaf\xd8\x8c\x2c\x20\xd8\xac\xd8\xb2\xd9\x8a\xd8\xb1\xd8\xaa\xd9\x8a\x20\xd8\xa8\xd8\xa7\xd8\xb3\xd8\xaa\xd8\xae\xd8\xaf\xd8\xa7\xd9\x85\x20\xd8\xa3\xd9\x86\x20\xd8\xaf\xd9\x86\xd9\x88\x2e\x20\xd8\xa5\xd8\xb0\x20\xd9\x87\xd9\x86\xd8\xa7\xd8\x9f\x20\xd8\xa7\xd9\x84\xd8\xb3\xd8\xaa\xd8\xa7\xd8\xb1\x20\xd9\x88\xd8\xaa\xd9\x86\xd8\xb5\xd9\x8a\xd8\xa8\x20\xd9\x83\xd8\xa7\xd9\x86\x2e\x20\xd8\xa3\xd9\x87\xd9\x91\xd9\x84\x20\xd8\xa7\xd9\x8a\xd8\xb7\xd8\xa7\xd9\x84\xd9\x8a\xd8\xa7\xd8\x8c\x20\xd8\xa8\xd8\xb1\xd9\x8a\xd8\xb7\xd8\xa7\xd9\x86\xd9\x8a\xd8\xa7\x2d\xd9\x81\xd8\xb1\xd9\x86\xd8\xb3\xd8\xa7\x20\xd9\x82\xd8\xaf\x20\xd8\xa3\xd8\xae\xd8\xb0\x2e\x20\xd8\xb3\xd9\x84\xd9\x8a\xd9\x85\xd8\xa7\xd9\x86\xd8\x8c\x20\xd8\xa5\xd8\xaa\xd9\x81\xd8\xa7\xd9\x82\xd9\x8a\xd8\xa9\x20\xd8\xa8\xd9\x8a\xd9\x86\x20\xd9\x85\xd8\xa7\x2c\x20\xd9\x8a\xd8\xb0\xd9\x83\xd8\xb1\x20\xd8\xa7\xd9\x84\xd8\xad\xd8\xaf\xd9\x88\xd8\xaf\x20\xd8\xa3\xd9\x8a\x20\xd8\xa8\xd8\xb9\xd8\xaf\x2c\x20\xd9\x85\xd8\xb9\xd8\xa7\xd9\x85\xd9\x84\xd8\xa9\x20\xd8\xa8\xd9\x88\xd9\x84\xd9\x86\xd8\xaf\xd8\xa7\xd8\x8c\x20\xd8\xa7\xd9\x84\xd8\xa5\xd8\xb7\xd9\x84\xd8\xa7\xd9\x82\x20\xd8\xb9\xd9\x84\x20\xd8\xa5\xd9\x8a\xd9\x88\x2e" "\xd7\x91\xd6\xb0\xd6\xbc\xd7\xa8\xd6\xb5\xd7\x90\xd7\xa9\xd6\xb4\xd7\x81\xd7\x99\xd7\xaa\x2c\x20\xd7\x91\xd6\xb8\xd6\xbc\xd7\xa8\xd6\xb8\xd7\x90\x20\xd7\x90\xd6\xb1\xd7\x9c\xd6\xb9\xd7\x94\xd6\xb4\xd7\x99\xd7\x9d\x2c\x20\xd7\x90\xd6\xb5\xd7\xaa\x20\xd7\x94\xd6\xb7\xd7\xa9\xd6\xb8\xd6\xbc\xd7\x81\xd7\x9e\xd6\xb7\xd7\x99\xd6\xb4\xd7\x9d\x2c\x20\xd7\x95\xd6\xb0\xd7\x90\xd6\xb5\xd7\xaa\x20\xd7\x94\xd6\xb8\xd7\x90\xd6\xb8\xd7\xa8\xd6\xb6\xd7\xa5" "\xd7\x94\xd6\xb8\xd7\x99\xd6\xb0\xd7\xaa\xd6\xb8\xd7\x94\x74\x65\x73\x74\xd8\xa7\xd9\x84\xd8\xb5\xd9\x81\xd8\xad\xd8\xa7\xd8\xaa\x20\xd8\xa7\xd9\x84\xd8\xaa\xd9\x91\xd8\xad\xd9\x88\xd9\x84" "\xef\xb7\xbd" "\xef\xb7\xba" "\xd9\x85\xd9\x8f\xd9\x86\xd9\x8e\xd8\xa7\xd9\x82\xd9\x8e\xd8\xb4\xd9\x8e\xd8\xa9\xd9\x8f\x20\xd8\xb3\xd9\x8f\xd8\xa8\xd9\x8f\xd9\x84\xd9\x90\x20\xd8\xa7\xd9\x90\xd8\xb3\xd9\x92\xd8\xaa\xd9\x90\xd8\xae\xd9\x92\xd8\xaf\xd9\x8e\xd8\xa7\xd9\x85\xd9\x90\x20\xd8\xa7\xd9\x84\xd9\x84\xd9\x8f\xd9\x91\xd8\xba\xd9\x8e\xd8\xa9\xd9\x90\x20\xd9\x81\xd9\x90\xd9\x8a\x20\xd8\xa7\xd9\x84\xd9\x86\xd9\x8f\xd9\x91\xd8\xb8\xd9\x8f\xd9\x85\xd9\x90\x20\xd8\xa7\xd9\x84\xd9\x92\xd9\x82\xd9\x8e\xd8\xa7\xd8\xa6\xd9\x90\xd9\x85\xd9\x8e\xd8\xa9\xd9\x90\x20\xd9\x88\xd9\x8e\xd9\x81\xd9\x90\xd9\x8a\xd9\x85\x20\xd9\x8a\xd9\x8e\xd8\xae\xd9\x8f\xd8\xb5\xd9\x8e\xd9\x91\x20\xd8\xa7\xd9\x84\xd8\xaa\xd9\x8e\xd9\x91\xd8\xb7\xd9\x92\xd8\xa8\xd9\x90\xd9\x8a\xd9\x82\xd9\x8e\xd8\xa7\xd8\xaa\xd9\x8f\x20\xd8\xa7\xd9\x84\xd9\x92\xd8\xad\xd8\xa7\xd8\xb3\xd9\x8f\xd9\x88\xd8\xa8\xd9\x90\xd9\x8a\xd9\x8e\xd9\x91\xd8\xa9\xd9\x8f\xd8\x8c\x20" "\xe1\x9a\x9b\xe1\x9a\x84\xe1\x9a\x93\xe1\x9a\x90\xe1\x9a\x8b\xe1\x9a\x92\xe1\x9a\x84\xe1\x9a\x80\xe1\x9a\x91\xe1\x9a\x84\xe1\x9a\x82\xe1\x9a\x91\xe1\x9a\x8f\xe1\x9a\x85\xe1\x9a\x9c\xe2\x80\xaa\xe2\x80\xaa\xe2\x80\xaa" "\xe2\x80\xaa\xe2\x80\xaa\xe1\x9a\x9b\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x80\xe1\x9a\x9c\xe2\x80\xaa" "\xe2\x80\xaa\xe2\x80\xaa\x74\x65\x73\x74\xe2\x80\xaa" "\xe2\x80\xab\x74\x65\x73\x74\xe2\x80\xab" "\xe2\x80\xa9\x74\x65\x73\x74\xe2\x80\xa9" "\x74\x65\x73\x74\xe2\x81\xa0\x74\x65\x73\x74\xe2\x80\xab" "\xe2\x81\xa6\x74\x65\x73\x74\xe2\x81\xa7" "\xe1\xb9\xb0\xcc\xba\xcc\xba\xcc\x95\x6f\xcd\x9e\x20\xcc\xb7\x69\xcc\xb2\xcc\xac\xcd\x87\xcc\xaa\xcd\x99\x6e\xcc\x9d\xcc\x97\xcd\x95\x76\xcc\x9f\xcc\x9c\xcc\x98\xcc\xa6\xcd\x9f\x6f\xcc\xb6\xcc\x99\xcc\xb0\xcc\xa0\x6b\xc3\xa8\xcd\x9a\xcc\xae\xcc\xba\xcc\xaa\xcc\xb9\xcc\xb1\xcc\xa4\x20\xcc\x96\x74\xcc\x9d\xcd\x95\xcc\xb3\xcc\xa3\xcc\xbb\xcc\xaa\xcd\x9e\x68\xcc\xbc\xcd\x93\xcc\xb2\xcc\xa6\xcc\xb3\xcc\x98\xcc\xb2\x65\xcd\x87\xcc\xa3\xcc\xb0\xcc\xa6\xcc\xac\xcd\x8e\x20\xcc\xa2\xcc\xbc\xcc\xbb\xcc\xb1\xcc\x98\x68\xcd\x9a\xcd\x8e\xcd\x99\xcc\x9c\xcc\xa3\xcc\xb2\xcd\x85\x69\xcc\xa6\xcc\xb2\xcc\xa3\xcc\xb0\xcc\xa4\x76\xcc\xbb\xcd\x8d\x65\xcc\xba\xcc\xad\xcc\xb3\xcc\xaa\xcc\xb0\x2d\x6d\xcc\xa2\x69\xcd\x85\x6e\xcc\x96\xcc\xba\xcc\x9e\xcc\xb2\xcc\xaf\xcc\xb0\x64\xcc\xb5\xcc\xbc\xcc\x9f\xcd\x99\xcc\xa9\xcc\xbc\xcc\x98\xcc\xb3\x20\xcc\x9e\xcc\xa5\xcc\xb1\xcc\xb3\xcc\xad\x72\xcc\x9b\xcc\x97\xcc\x98\x65\xcd\x99\x70\xcd\xa0\x72\xcc\xbc\xcc\x9e\xcc\xbb\xcc\xad\xcc\x97\x65\xcc\xba\xcc\xa0\xcc\xa3\xcd\x9f\x73\xcc\x98\xcd\x87\xcc\xb3\xcd\x8d\xcc\x9d\xcd\x89\x65\xcd\x89\xcc\xa5\xcc\xaf\xcc\x9e\xcc\xb2\xcd\x9a\xcc\xac\xcd\x9c\xc7\xb9\xcc\xac\xcd\x8e\xcd\x8e\xcc\x9f\xcc\x96\xcd\x87\xcc\xa4\x74\xcd\x8d\xcc\xac\xcc\xa4\xcd\x93\xcc\xbc\xcc\xad\xcd\x98\xcd\x85\x69\xcc\xaa\xcc\xb1\x6e\xcd\xa0\x67\xcc\xb4\xcd\x89\x20\xcd\x8f\xcd\x89\xcd\x85\x63\xcc\xac\xcc\x9f\x68\xcd\xa1\x61\xcc\xab\xcc\xbb\xcc\xaf\xcd\x98\x6f\xcc\xab\xcc\x9f\xcc\x96\xcd\x8d\xcc\x99\xcc\x9d\xcd\x89\x73\xcc\x97\xcc\xa6\xcc\xb2\x2e\xcc\xa8\xcc\xb9\xcd\x88\xcc\xa3" "\xcc\xa1\xcd\x93\xcc\x9e\xcd\x85\x49\xcc\x97\xcc\x98\xcc\xa6\xcd\x9d\x6e\xcd\x87\xcd\x87\xcd\x99\x76\xcc\xae\xcc\xab\x6f\x6b\xcc\xb2\xcc\xab\xcc\x99\xcd\x88\x69\xcc\x96\xcd\x99\xcc\xad\xcc\xb9\xcc\xa0\xcc\x9e\x6e\xcc\xa1\xcc\xbb\xcc\xae\xcc\xa3\xcc\xba\x67\xcc\xb2\xcd\x88\xcd\x99\xcc\xad\xcd\x99\xcc\xac\xcd\x8e\x20\xcc\xb0\x74\xcd\x94\xcc\xa6\x68\xcc\x9e\xcc\xb2\x65\xcc\xa2\xcc\xa4\x20\xcd\x8d\xcc\xac\xcc\xb2\xcd\x96\x66\xcc\xb4\xcc\x98\xcd\x95\xcc\xa3\xc3\xa8\xcd\x96\xe1\xba\xb9\xcc\xa5\xcc\xa9\x6c\xcd\x96\xcd\x94\xcd\x9a\x69\xcd\x93\xcd\x9a\xcc\xa6\xcd\xa0\x6e\xcd\x96\xcd\x8d\xcc\x97\xcd\x93\xcc\xb3\xcc\xae\x67\xcd\x8d\x20\xcc\xa8\x6f\xcd\x9a\xcc\xaa\xcd\xa1\x66\xcc\x98\xcc\xa3\xcc\xac\x20\xcc\x96\xcc\x98\xcd\x96\xcc\x9f\xcd\x99\xcc\xae\x63\xd2\x89\xcd\x94\xcc\xab\xcd\x96\xcd\x93\xcd\x87\xcd\x96\xcd\x85\x68\xcc\xb5\xcc\xa4\xcc\xa3\xcd\x9a\xcd\x94\xc3\xa1\xcc\x97\xcc\xbc\xcd\x95\xcd\x85\x6f\xcc\xbc\xcc\xa3\xcc\xa5\x73\xcc\xb1\xcd\x88\xcc\xba\xcc\x96\xcc\xa6\xcc\xbb\xcd\xa2\x2e\xcc\x9b\xcc\x96\xcc\x9e\xcc\xa0\xcc\xab\xcc\xb0" "\xcc\x97\xcc\xba\xcd\x96\xcc\xb9\xcc\xaf\xcd\x93\xe1\xb9\xae\xcc\xa4\xcd\x8d\xcc\xa5\xcd\x87\xcd\x88\x68\xcc\xb2\xcc\x81\x65\xcd\x8f\xcd\x93\xcc\xbc\xcc\x97\xcc\x99\xcc\xbc\xcc\xa3\xcd\x94\x20\xcd\x87\xcc\x9c\xcc\xb1\xcc\xa0\xcd\x93\xcd\x8d\xcd\x85\x4e\xcd\x95\xcd\xa0\x65\xcc\x97\xcc\xb1\x7a\xcc\x98\xcc\x9d\xcc\x9c\xcc\xba\xcd\x99\x70\xcc\xa4\xcc\xba\xcc\xb9\xcd\x8d\xcc\xaf\xcd\x9a\x65\xcc\xa0\xcc\xbb\xcc\xa0\xcd\x9c\x72\xcc\xa8\xcc\xa4\xcd\x8d\xcc\xba\xcc\x96\xcd\x94\xcc\x96\xcc\x96\x64\xcc\xa0\xcc\x9f\xcc\xad\xcc\xac\xcc\x9d\xcd\x9f\x69\xcc\xa6\xcd\x96\xcc\xa9\xcd\x93\xcd\x94\xcc\xa4\x61\xcc\xa0\xcc\x97\xcc\xac\xcd\x89\xcc\x99\x6e\xcd\x9a\xcd\x9c\x20\xcc\xbb\xcc\x9e\xcc\xb0\xcd\x9a\xcd\x85\x68\xcc\xb5\xcd\x89\x69\xcc\xb3\xcc\x9e\x76\xcc\xa2\xcd\x87\xe1\xb8\x99\xcd\x8e\xcd\x9f\x2d\xd2\x89\xcc\xad\xcc\xa9\xcc\xbc\xcd\x94\x6d\xcc\xa4\xcc\xad\xcc\xab\x69\xcd\x95\xcd\x87\xcc\x9d\xcc\xa6\x6e\xcc\x97\xcd\x99\xe1\xb8\x8d\xcc\x9f\x20\xcc\xaf\xcc\xb2\xcd\x95\xcd\x9e\xc7\xab\xcc\x9f\xcc\xaf\xcc\xb0\xcc\xb2\xcd\x99\xcc\xbb\xcc\x9d\x66\x20\xcc\xaa\xcc\xb0\xcc\xb0\xcc\x97\xcc\x96\xcc\xad\xcc\x98\xcd\x98\x63\xcc\xa6\xcd\x8d\xcc\xb2\xcc\x9e\xcd\x8d\xcc\xa9\xcc\x99\xe1\xb8\xa5\xcd\x9a\x61\xcc\xae\xcd\x8e\xcc\x9f\xcc\x99\xcd\x9c\xc6\xa1\xcc\xa9\xcc\xb9\xcd\x8e\x73\xcc\xa4\x2e\xcc\x9d\xcc\x9d\x20\xd2\x89\x5a\xcc\xa1\xcc\x96\xcc\x9c\xcd\x96\xcc\xb0\xcc\xa3\xcd\x89\xcc\x9c\x61\xcd\x96\xcc\xb0\xcd\x99\xcc\xac\xcd\xa1\x6c\xcc\xb2\xcc\xab\xcc\xb3\xcd\x8d\xcc\xa9\x67\xcc\xa1\xcc\x9f\xcc\xbc\xcc\xb1\xcd\x9a\xcc\x9e\xcc\xac\xcd\x85\x6f\xcc\x97\xcd\x9c\x2e\xcc\x9f" "\xcc\xa6\x48\xcc\xac\xcc\xa4\xcc\x97\xcc\xa4\xcd\x9d\x65\xcd\x9c\x20\xcc\x9c\xcc\xa5\xcc\x9d\xcc\xbb\xcd\x8d\xcc\x9f\xcc\x81\x77\xcc\x95\x68\xcc\x96\xcc\xaf\xcd\x93\x6f\xcc\x9d\xcd\x99\xcc\x96\xcd\x8e\xcc\xb1\xcc\xae\x20\xd2\x89\xcc\xba\xcc\x99\xcc\x9e\xcc\x9f\xcd\x88\x57\xcc\xb7\xcc\xbc\xcc\xad\x61\xcc\xba\xcc\xaa\xcd\x8d\xc4\xaf\xcd\x88\xcd\x95\xcc\xad\xcd\x99\xcc\xaf\xcc\x9c\x74\xcc\xb6\xcc\xbc\xcc\xae\x73\xcc\x98\xcd\x99\xcd\x96\xcc\x95\x20\xcc\xa0\xcc\xab\xcc\xa0\x42\xcc\xbb\xcd\x8d\xcd\x99\xcd\x89\xcc\xb3\xcd\x85\x65\xcc\xb5\x68\xcc\xb5\xcc\xac\xcd\x87\xcc\xab\xcd\x99\x69\xcc\xb9\xcd\x93\xcc\xb3\xcc\xb3\xcc\xae\xcd\x8e\xcc\xab\xcc\x95\x6e\xcd\x9f\x64\xcc\xb4\xcc\xaa\xcc\x9c\xcc\x96\x20\xcc\xb0\xcd\x89\xcc\xa9\xcd\x87\xcd\x99\xcc\xb2\xcd\x9e\xcd\x85\x54\xcd\x96\xcc\xbc\xcd\x93\xcc\xaa\xcd\xa2\x68\xcd\x8f\xcd\x93\xcc\xae\xcc\xbb\x65\xcc\xac\xcc\x9d\xcc\x9f\xcd\x85\x20\xcc\xa4\xcc\xb9\xcc\x9d\x57\xcd\x99\xcc\x9e\xcc\x9d\xcd\x94\xcd\x87\xcd\x9d\xcd\x85\x61\xcd\x8f\xcd\x93\xcd\x94\xcc\xb9\xcc\xbc\xcc\xa3\x6c\xcc\xb4\xcd\x94\xcc\xb0\xcc\xa4\xcc\x9f\xcd\x94\xe1\xb8\xbd\xcc\xab\x2e\xcd\x95" "\x5a\xcc\xae\xcc\x9e\xcc\xa0\xcd\x99\xcd\x94\xcd\x85\xe1\xb8\x80\xcc\x97\xcc\x9e\xcd\x88\xcc\xbb\xcc\x97\xe1\xb8\xb6\xcd\x99\xcd\x8e\xcc\xaf\xcc\xb9\xcc\x9e\xcd\x93\x47\xcc\xbb\x4f\xcc\xad\xcc\x97\xcc\xae" "\xcb\x99\xc9\x90\x6e\x62\xe1\xb4\x89\x6c\xc9\x90\x20\xc9\x90\x75\xc6\x83\xc9\x90\xc9\xaf\x20\xc7\x9d\xc9\xb9\x6f\x6c\x6f\x70\x20\xca\x87\xc7\x9d\x20\xc7\x9d\xc9\xb9\x6f\x71\xc9\x90\x6c\x20\xca\x87\x6e\x20\xca\x87\x75\x6e\x70\xe1\xb4\x89\x70\xe1\xb4\x89\xc9\x94\x75\xe1\xb4\x89\x20\xc9\xb9\x6f\x64\xc9\xaf\xc7\x9d\xca\x87\x20\x70\x6f\xc9\xaf\x73\x6e\xe1\xb4\x89\xc7\x9d\x20\x6f\x70\x20\x70\xc7\x9d\x73\x20\x27\xca\x87\xe1\xb4\x89\x6c\xc7\x9d\x20\xc6\x83\x75\xe1\xb4\x89\xc9\x94\x73\xe1\xb4\x89\x64\xe1\xb4\x89\x70\xc9\x90\x20\xc9\xb9\x6e\xca\x87\xc7\x9d\xca\x87\xc9\x94\xc7\x9d\x73\x75\x6f\xc9\x94\x20\x27\xca\x87\xc7\x9d\xc9\xaf\xc9\x90\x20\xca\x87\xe1\xb4\x89\x73\x20\xc9\xb9\x6f\x6c\x6f\x70\x20\xc9\xaf\x6e\x73\x64\xe1\xb4\x89\x20\xc9\xaf\xc7\x9d\xc9\xb9\x6f\xcb\xa5" "\x30\x30\xcb\x99\xc6\x96\x24\x2d" "\xef\xbc\xb4\xef\xbd\x88\xef\xbd\x85\x20\xef\xbd\x91\xef\xbd\x95\xef\xbd\x89\xef\xbd\x83\xef\xbd\x8b\x20\xef\xbd\x82\xef\xbd\x92\xef\xbd\x8f\xef\xbd\x97\xef\xbd\x8e\x20\xef\xbd\x86\xef\xbd\x8f\xef\xbd\x98\x20\xef\xbd\x8a\xef\xbd\x95\xef\xbd\x8d\xef\xbd\x90\xef\xbd\x93\x20\xef\xbd\x8f\xef\xbd\x96\xef\xbd\x85\xef\xbd\x92\x20\xef\xbd\x94\xef\xbd\x88\xef\xbd\x85\x20\xef\xbd\x8c\xef\xbd\x81\xef\xbd\x9a\xef\xbd\x99\x20\xef\xbd\x84\xef\xbd\x8f\xef\xbd\x87" "\xf0\x9d\x90\x93\xf0\x9d\x90\xa1\xf0\x9d\x90\x9e\x20\xf0\x9d\x90\xaa\xf0\x9d\x90\xae\xf0\x9d\x90\xa2\xf0\x9d\x90\x9c\xf0\x9d\x90\xa4\x20\xf0\x9d\x90\x9b\xf0\x9d\x90\xab\xf0\x9d\x90\xa8\xf0\x9d\x90\xb0\xf0\x9d\x90\xa7\x20\xf0\x9d\x90\x9f\xf0\x9d\x90\xa8\xf0\x9d\x90\xb1\x20\xf0\x9d\x90\xa3\xf0\x9d\x90\xae\xf0\x9d\x90\xa6\xf0\x9d\x90\xa9\xf0\x9d\x90\xac\x20\xf0\x9d\x90\xa8\xf0\x9d\x90\xaf\xf0\x9d\x90\x9e\xf0\x9d\x90\xab\x20\xf0\x9d\x90\xad\xf0\x9d\x90\xa1\xf0\x9d\x90\x9e\x20\xf0\x9d\x90\xa5\xf0\x9d\x90\x9a\xf0\x9d\x90\xb3\xf0\x9d\x90\xb2\x20\xf0\x9d\x90\x9d\xf0\x9d\x90\xa8\xf0\x9d\x90\xa0" "\xf0\x9d\x95\xbf\xf0\x9d\x96\x8d\xf0\x9d\x96\x8a\x20\xf0\x9d\x96\x96\xf0\x9d\x96\x9a\xf0\x9d\x96\x8e\xf0\x9d\x96\x88\xf0\x9d\x96\x90\x20\xf0\x9d\x96\x87\xf0\x9d\x96\x97\xf0\x9d\x96\x94\xf0\x9d\x96\x9c\xf0\x9d\x96\x93\x20\xf0\x9d\x96\x8b\xf0\x9d\x96\x94\xf0\x9d\x96\x9d\x20\xf0\x9d\x96\x8f\xf0\x9d\x96\x9a\xf0\x9d\x96\x92\xf0\x9d\x96\x95\xf0\x9d\x96\x98\x20\xf0\x9d\x96\x94\xf0\x9d\x96\x9b\xf0\x9d\x96\x8a\xf0\x9d\x96\x97\x20\xf0\x9d\x96\x99\xf0\x9d\x96\x8d\xf0\x9d\x96\x8a\x20\xf0\x9d\x96\x91\xf0\x9d\x96\x86\xf0\x9d\x96\x9f\xf0\x9d\x96\x9e\x20\xf0\x9d\x96\x89\xf0\x9d\x96\x94\xf0\x9d\x96\x8c" "\xf0\x9d\x91\xbb\xf0\x9d\x92\x89\xf0\x9d\x92\x86\x20\xf0\x9d\x92\x92\xf0\x9d\x92\x96\xf0\x9d\x92\x8a\xf0\x9d\x92\x84\xf0\x9d\x92\x8c\x20\xf0\x9d\x92\x83\xf0\x9d\x92\x93\xf0\x9d\x92\x90\xf0\x9d\x92\x98\xf0\x9d\x92\x8f\x20\xf0\x9d\x92\x87\xf0\x9d\x92\x90\xf0\x9d\x92\x99\x20\xf0\x9d\x92\x8b\xf0\x9d\x92\x96\xf0\x9d\x92\x8e\xf0\x9d\x92\x91\xf0\x9d\x92\x94\x20\xf0\x9d\x92\x90\xf0\x9d\x92\x97\xf0\x9d\x92\x86\xf0\x9d\x92\x93\x20\xf0\x9d\x92\x95\xf0\x9d\x92\x89\xf0\x9d\x92\x86\x20\xf0\x9d\x92\x8d\xf0\x9d\x92\x82\xf0\x9d\x92\x9b\xf0\x9d\x92\x9a\x20\xf0\x9d\x92\x85\xf0\x9d\x92\x90\xf0\x9d\x92\x88" "\xf0\x9d\x93\xa3\xf0\x9d\x93\xb1\xf0\x9d\x93\xae\x20\xf0\x9d\x93\xba\xf0\x9d\x93\xbe\xf0\x9d\x93\xb2\xf0\x9d\x93\xac\xf0\x9d\x93\xb4\x20\xf0\x9d\x93\xab\xf0\x9d\x93\xbb\xf0\x9d\x93\xb8\xf0\x9d\x94\x80\xf0\x9d\x93\xb7\x20\xf0\x9d\x93\xaf\xf0\x9d\x93\xb8\xf0\x9d\x94\x81\x20\xf0\x9d\x93\xb3\xf0\x9d\x93\xbe\xf0\x9d\x93\xb6\xf0\x9d\x93\xb9\xf0\x9d\x93\xbc\x20\xf0\x9d\x93\xb8\xf0\x9d\x93\xbf\xf0\x9d\x93\xae\xf0\x9d\x93\xbb\x20\xf0\x9d\x93\xbd\xf0\x9d\x93\xb1\xf0\x9d\x93\xae\x20\xf0\x9d\x93\xb5\xf0\x9d\x93\xaa\xf0\x9d\x94\x83\xf0\x9d\x94\x82\x20\xf0\x9d\x93\xad\xf0\x9d\x93\xb8\xf0\x9d\x93\xb0" "\xf0\x9d\x95\x8b\xf0\x9d\x95\x99\xf0\x9d\x95\x96\x20\xf0\x9d\x95\xa2\xf0\x9d\x95\xa6\xf0\x9d\x95\x9a\xf0\x9d\x95\x94\xf0\x9d\x95\x9c\x20\xf0\x9d\x95\x93\xf0\x9d\x95\xa3\xf0\x9d\x95\xa0\xf0\x9d\x95\xa8\xf0\x9d\x95\x9f\x20\xf0\x9d\x95\x97\xf0\x9d\x95\xa0\xf0\x9d\x95\xa9\x20\xf0\x9d\x95\x9b\xf0\x9d\x95\xa6\xf0\x9d\x95\x9e\xf0\x9d\x95\xa1\xf0\x9d\x95\xa4\x20\xf0\x9d\x95\xa0\xf0\x9d\x95\xa7\xf0\x9d\x95\x96\xf0\x9d\x95\xa3\x20\xf0\x9d\x95\xa5\xf0\x9d\x95\x99\xf0\x9d\x95\x96\x20\xf0\x9d\x95\x9d\xf0\x9d\x95\x92\xf0\x9d\x95\xab\xf0\x9d\x95\xaa\x20\xf0\x9d\x95\x95\xf0\x9d\x95\xa0\xf0\x9d\x95\x98" "\xf0\x9d\x9a\x83\xf0\x9d\x9a\x91\xf0\x9d\x9a\x8e\x20\xf0\x9d\x9a\x9a\xf0\x9d\x9a\x9e\xf0\x9d\x9a\x92\xf0\x9d\x9a\x8c\xf0\x9d\x9a\x94\x20\xf0\x9d\x9a\x8b\xf0\x9d\x9a\x9b\xf0\x9d\x9a\x98\xf0\x9d\x9a\xa0\xf0\x9d\x9a\x97\x20\xf0\x9d\x9a\x8f\xf0\x9d\x9a\x98\xf0\x9d\x9a\xa1\x20\xf0\x9d\x9a\x93\xf0\x9d\x9a\x9e\xf0\x9d\x9a\x96\xf0\x9d\x9a\x99\xf0\x9d\x9a\x9c\x20\xf0\x9d\x9a\x98\xf0\x9d\x9a\x9f\xf0\x9d\x9a\x8e\xf0\x9d\x9a\x9b\x20\xf0\x9d\x9a\x9d\xf0\x9d\x9a\x91\xf0\x9d\x9a\x8e\x20\xf0\x9d\x9a\x95\xf0\x9d\x9a\x8a\xf0\x9d\x9a\xa3\xf0\x9d\x9a\xa2\x20\xf0\x9d\x9a\x8d\xf0\x9d\x9a\x98\xf0\x9d\x9a\x90" "\xe2\x92\xaf\xe2\x92\xa3\xe2\x92\xa0\x20\xe2\x92\xac\xe2\x92\xb0\xe2\x92\xa4\xe2\x92\x9e\xe2\x92\xa6\x20\xe2\x92\x9d\xe2\x92\xad\xe2\x92\xaa\xe2\x92\xb2\xe2\x92\xa9\x20\xe2\x92\xa1\xe2\x92\xaa\xe2\x92\xb3\x20\xe2\x92\xa5\xe2\x92\xb0\xe2\x92\xa8\xe2\x92\xab\xe2\x92\xae\x20\xe2\x92\xaa\xe2\x92\xb1\xe2\x92\xa0\xe2\x92\xad\x20\xe2\x92\xaf\xe2\x92\xa3\xe2\x92\xa0\x20\xe2\x92\xa7\xe2\x92\x9c\xe2\x92\xb5\xe2\x92\xb4\x20\xe2\x92\x9f\xe2\x92\xaa\xe2\x92\xa2" "\x3c\x73\x63\x72\x69\x70\x74\x3e\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x26\x6c\x74\x3b\x73\x63\x72\x69\x70\x74\x26\x67\x74\x3b\x61\x6c\x65\x72\x74\x28\x26\x23\x33\x39\x3b\x31\x32\x33\x26\x23\x33\x39\x3b\x29\x3b\x26\x6c\x74\x3b\x2f\x73\x63\x72\x69\x70\x74\x26\x67\x74\x3b" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x20\x2f\x3e" "\x3c\x73\x76\x67\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x31\x32\x33\x3c\x31\x3e\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x3c\x20\x2f\x20\x73\x63\x72\x69\x70\x74\x20\x3e\x3c\x20\x73\x63\x72\x69\x70\x74\x20\x3e\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3c\x20\x2f\x20\x73\x63\x72\x69\x70\x74\x20\x3e" "\x20\x6f\x6e\x66\x6f\x63\x75\x73\x3d\x4a\x61\x56\x61\x53\x43\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x20\x61\x75\x74\x6f\x66\x6f\x63\x75\x73" "\x22\x20\x6f\x6e\x66\x6f\x63\x75\x73\x3d\x4a\x61\x56\x61\x53\x43\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x20\x61\x75\x74\x6f\x66\x6f\x63\x75\x73" "\x27\x20\x6f\x6e\x66\x6f\x63\x75\x73\x3d\x4a\x61\x56\x61\x53\x43\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x20\x61\x75\x74\x6f\x66\x6f\x63\x75\x73" "\xef\xbc\x9c\x73\x63\x72\x69\x70\x74\xef\xbc\x9e\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\xef\xbc\x9c\x2f\x73\x63\x72\x69\x70\x74\xef\xbc\x9e" "\x3c\x73\x63\x3c\x73\x63\x72\x69\x70\x74\x3e\x72\x69\x70\x74\x3e\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3c\x2f\x73\x63\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x72\x69\x70\x74\x3e" "\x2d\x2d\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x3b\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3b\x74\x3d\x22" "\x27\x3b\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3b\x74\x3d\x27" "\x4a\x61\x76\x61\x53\x43\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29" "\x3b\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3b" "\x73\x72\x63\x3d\x4a\x61\x56\x61\x53\x43\x72\x69\x70\x74\x3a\x70\x72\x6f\x6d\x70\x74\x28\x31\x33\x32\x29" "\x22\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3b\x3c\x2f\x73\x63\x72\x69\x70\x74\x20\x78\x3d\x22" "\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3b\x3c\x2f\x73\x63\x72\x69\x70\x74\x20\x78\x3d\x27" "\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3b\x3c\x2f\x73\x63\x72\x69\x70\x74\x20\x78\x3d" "\x22\x20\x61\x75\x74\x6f\x66\x6f\x63\x75\x73\x20\x6f\x6e\x6b\x65\x79\x75\x70\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29" "\x27\x20\x61\x75\x74\x6f\x66\x6f\x63\x75\x73\x20\x6f\x6e\x6b\x65\x79\x75\x70\x3d\x27\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29" "\x3c\x73\x63\x72\x69\x70\x74\x5c\x78\x32\x30\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x22\x3e\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3b\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x3c\x73\x63\x72\x69\x70\x74\x5c\x78\x33\x45\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x22\x3e\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3b\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x3c\x73\x63\x72\x69\x70\x74\x5c\x78\x30\x44\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x22\x3e\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3b\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x3c\x73\x63\x72\x69\x70\x74\x5c\x78\x30\x39\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x22\x3e\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3b\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x3c\x73\x63\x72\x69\x70\x74\x5c\x78\x30\x43\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x22\x3e\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3b\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x3c\x73\x63\x72\x69\x70\x74\x5c\x78\x32\x46\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x22\x3e\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3b\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x3c\x73\x63\x72\x69\x70\x74\x5c\x78\x30\x41\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x22\x3e\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3b\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x27\x60\x22\x3e\x3c\x5c\x78\x33\x43\x73\x63\x72\x69\x70\x74\x3e\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x27\x60\x22\x3e\x3c\x5c\x78\x30\x30\x73\x63\x72\x69\x70\x74\x3e\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x5c\x78\x33\x41\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x5c\x78\x35\x43\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x5c\x78\x30\x30\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x65\x78\x70\x5c\x78\x30\x30\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x65\x78\x70\x5c\x78\x35\x43\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x30\x41\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x30\x39\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x45\x33\x5c\x78\x38\x30\x5c\x78\x38\x30\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x34\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x43\x32\x5c\x78\x41\x30\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x30\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x41\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x30\x44\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x30\x43\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x37\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x45\x46\x5c\x78\x42\x42\x5c\x78\x42\x46\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x32\x30\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x38\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x30\x30\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x42\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x36\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x35\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x32\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x30\x42\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x31\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x33\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x41\x42\x43\x3c\x64\x69\x76\x20\x73\x74\x79\x6c\x65\x3d\x22\x78\x3a\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x39\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e\x44\x45\x46" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x42\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x46\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x43\x32\x5c\x78\x41\x30\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x35\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x31\x5c\x78\x41\x30\x5c\x78\x38\x45\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x38\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x31\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x38\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x39\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x30\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x37\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x33\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x45\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x41\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x30\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x30\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x32\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x32\x30\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x33\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x39\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x41\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x34\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x39\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x41\x46\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x46\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x31\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x44\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x37\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x37\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x31\x5c\x78\x39\x41\x5c\x78\x38\x30\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x33\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x34\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x31\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x38\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x34\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x36\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x33\x5c\x78\x38\x30\x5c\x78\x38\x30\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x32\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x44\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x41\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x43\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x35\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x41\x38\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x36\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x32\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x42\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x30\x36\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x41\x39\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x35\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x45\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x45\x32\x5c\x78\x38\x31\x5c\x78\x39\x46\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x5c\x78\x31\x43\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x5c\x78\x30\x30\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x5c\x78\x33\x41\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x5c\x78\x30\x39\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x5c\x78\x30\x44\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x5c\x78\x30\x41\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x20\x69\x64\x3d\x22\x66\x75\x7a\x7a\x65\x6c\x65\x6d\x65\x6e\x74\x31\x22\x3e\x74\x65\x73\x74\x3c\x2f\x61\x3e" "\x60\x22\x27\x3e\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x78\x78\x3a\x78\x20\x5c\x78\x30\x41\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3e" "\x60\x22\x27\x3e\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x78\x78\x3a\x78\x20\x5c\x78\x32\x32\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3e" "\x60\x22\x27\x3e\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x78\x78\x3a\x78\x20\x5c\x78\x30\x42\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3e" "\x60\x22\x27\x3e\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x78\x78\x3a\x78\x20\x5c\x78\x30\x44\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3e" "\x60\x22\x27\x3e\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x78\x78\x3a\x78\x20\x5c\x78\x32\x46\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3e" "\x60\x22\x27\x3e\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x78\x78\x3a\x78\x20\x5c\x78\x30\x39\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3e" "\x60\x22\x27\x3e\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x78\x78\x3a\x78\x20\x5c\x78\x30\x43\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3e" "\x60\x22\x27\x3e\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x78\x78\x3a\x78\x20\x5c\x78\x30\x30\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3e" "\x60\x22\x27\x3e\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x78\x78\x3a\x78\x20\x5c\x78\x32\x37\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3e" "\x60\x22\x27\x3e\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x78\x78\x3a\x78\x20\x5c\x78\x32\x30\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x33\x42\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x30\x44\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x46\x5c\x78\x42\x42\x5c\x78\x42\x46\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x31\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x34\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x33\x5c\x78\x38\x30\x5c\x78\x38\x30\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x30\x39\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x39\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x35\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x38\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x30\x30\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x41\x38\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x41\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x31\x5c\x78\x39\x41\x5c\x78\x38\x30\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x30\x43\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x32\x42\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x46\x30\x5c\x78\x39\x30\x5c\x78\x39\x36\x5c\x78\x39\x41\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x2d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x30\x41\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x41\x46\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x37\x45\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x37\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x31\x5c\x78\x39\x46\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x41\x39\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x43\x32\x5c\x78\x38\x35\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x46\x5c\x78\x42\x46\x5c\x78\x41\x45\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x33\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x42\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x46\x5c\x78\x42\x46\x5c\x78\x42\x45\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x30\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x32\x31\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x32\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x32\x5c\x78\x38\x30\x5c\x78\x38\x36\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x45\x31\x5c\x78\x41\x30\x5c\x78\x38\x45\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x30\x42\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x32\x30\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x22\x60\x27\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x5c\x78\x43\x32\x5c\x78\x41\x30\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x3c\x69\x6d\x67\x20\x5c\x78\x30\x30\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x5c\x78\x34\x37\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x5c\x78\x31\x31\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x5c\x78\x31\x32\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x5c\x78\x34\x37\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x5c\x78\x31\x30\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x5c\x78\x31\x33\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x5c\x78\x33\x32\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x5c\x78\x34\x37\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x5c\x78\x31\x31\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x5c\x78\x34\x37\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x5c\x78\x33\x34\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x5c\x78\x33\x39\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x5c\x78\x30\x30\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x5c\x78\x30\x39\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x5c\x78\x31\x30\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x5c\x78\x31\x33\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x5c\x78\x33\x32\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x5c\x78\x31\x32\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x5c\x78\x31\x31\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x5c\x78\x30\x30\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x5c\x78\x34\x37\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x5c\x78\x30\x39\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x5c\x78\x31\x30\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x5c\x78\x31\x31\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x5c\x78\x31\x32\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x5c\x78\x31\x33\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x5b\x61\x5d\x5b\x62\x5d\x5b\x63\x5d\x73\x72\x63\x5b\x64\x5d\x3d\x78\x5b\x65\x5d\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x5b\x66\x5d\x22\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x5c\x78\x30\x39\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x5c\x78\x31\x30\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x5c\x78\x31\x31\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x5c\x78\x31\x32\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x5c\x78\x33\x32\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x5c\x78\x30\x30\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x22\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x6a\x61\x76\x61\x26\x23\x31\x26\x23\x32\x26\x23\x33\x26\x23\x34\x26\x23\x35\x26\x23\x36\x26\x23\x37\x26\x23\x38\x26\x23\x31\x31\x26\x23\x31\x32\x73\x63\x72\x69\x70\x74\x3a\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3e\x58\x58\x58\x3c\x2f\x61\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x22\x78\x60\x20\x60\x3c\x73\x63\x72\x69\x70\x74\x3e\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x22\x60\x20\x60\x3e" "\x3c\x69\x6d\x67\x20\x73\x72\x63\x20\x6f\x6e\x65\x72\x72\x6f\x72\x20\x2f\x22\x20\x27\x22\x3d\x20\x61\x6c\x74\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x2f\x2f\x22\x3e" "\x3c\x74\x69\x74\x6c\x65\x20\x6f\x6e\x70\x72\x6f\x70\x65\x72\x74\x79\x63\x68\x61\x6e\x67\x65\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3e\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x3c\x74\x69\x74\x6c\x65\x20\x74\x69\x74\x6c\x65\x3d\x3e" "\x3c\x61\x20\x68\x72\x65\x66\x3d\x68\x74\x74\x70\x3a\x2f\x2f\x66\x6f\x6f\x2e\x62\x61\x72\x2f\x23\x78\x3d\x60\x79\x3e\x3c\x2f\x61\x3e\x3c\x69\x6d\x67\x20\x61\x6c\x74\x3d\x22\x60\x3e\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x3a\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3e\x3c\x2f\x61\x3e\x22\x3e" "\x3c\x21\x2d\x2d\x5b\x69\x66\x5d\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x20\x2d\x2d\x3e" "\x3c\x21\x2d\x2d\x5b\x69\x66\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x31\x29\x2f\x2f\x5d\x3e\x20\x2d\x2d\x3e" "\x3c\x73\x63\x72\x69\x70\x74\x20\x73\x72\x63\x3d\x22\x2f\x5c\x25\x28\x6a\x73\x63\x72\x69\x70\x74\x29\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x3c\x73\x63\x72\x69\x70\x74\x20\x73\x72\x63\x3d\x22\x5c\x5c\x25\x28\x6a\x73\x63\x72\x69\x70\x74\x29\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x3c\x49\x4d\x47\x20\x22\x22\x22\x3e\x3c\x53\x43\x52\x49\x50\x54\x3e\x61\x6c\x65\x72\x74\x28\x22\x58\x53\x53\x22\x29\x3c\x2f\x53\x43\x52\x49\x50\x54\x3e\x22\x3e" "\x3c\x49\x4d\x47\x20\x53\x52\x43\x3d\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x53\x74\x72\x69\x6e\x67\x2e\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65\x28\x38\x38\x2c\x38\x33\x2c\x38\x33\x29\x29\x3e" "\x3c\x49\x4d\x47\x20\x53\x52\x43\x3d\x23\x20\x6f\x6e\x6d\x6f\x75\x73\x65\x6f\x76\x65\x72\x3d\x22\x61\x6c\x65\x72\x74\x28\x27\x78\x78\x73\x27\x29\x22\x3e" "\x3c\x49\x4d\x47\x20\x53\x52\x43\x3d\x20\x6f\x6e\x6d\x6f\x75\x73\x65\x6f\x76\x65\x72\x3d\x22\x61\x6c\x65\x72\x74\x28\x27\x78\x78\x73\x27\x29\x22\x3e" "\x3c\x49\x4d\x47\x20\x6f\x6e\x6d\x6f\x75\x73\x65\x6f\x76\x65\x72\x3d\x22\x61\x6c\x65\x72\x74\x28\x27\x78\x78\x73\x27\x29\x22\x3e" "\x3c\x49\x4d\x47\x20\x53\x52\x43\x3d\x26\x23\x31\x30\x36\x3b\x26\x23\x39\x37\x3b\x26\x23\x31\x31\x38\x3b\x26\x23\x39\x37\x3b\x26\x23\x31\x31\x35\x3b\x26\x23\x39\x39\x3b\x26\x23\x31\x31\x34\x3b\x26\x23\x31\x30\x35\x3b\x26\x23\x31\x31\x32\x3b\x26\x23\x31\x31\x36\x3b\x26\x23\x35\x38\x3b\x26\x23\x39\x37\x3b\x26\x23\x31\x30\x38\x3b\x26\x23\x31\x30\x31\x3b\x26\x23\x31\x31\x34\x3b\x26\x23\x31\x31\x36\x3b\x26\x23\x34\x30\x3b\x26\x23\x33\x39\x3b\x26\x23\x38\x38\x3b\x26\x23\x38\x33\x3b\x26\x23\x38\x33\x3b\x26\x23\x33\x39\x3b\x26\x23\x34\x31\x3b\x3e" "\x3c\x49\x4d\x47\x20\x53\x52\x43\x3d\x26\x23\x30\x30\x30\x30\x31\x30\x36\x26\x23\x30\x30\x30\x30\x30\x39\x37\x26\x23\x30\x30\x30\x30\x31\x31\x38\x26\x23\x30\x30\x30\x30\x30\x39\x37\x26\x23\x30\x30\x30\x30\x31\x31\x35\x26\x23\x30\x30\x30\x30\x30\x39\x39\x26\x23\x30\x30\x30\x30\x31\x31\x34\x26\x23\x30\x30\x30\x30\x31\x30\x35\x26\x23\x30\x30\x30\x30\x31\x31\x32\x26\x23\x30\x30\x30\x30\x31\x31\x36\x26\x23\x30\x30\x30\x30\x30\x35\x38\x26\x23\x30\x30\x30\x30\x30\x39\x37\x26\x23\x30\x30\x30\x30\x31\x30\x38\x26\x23\x30\x30\x30\x30\x31\x30\x31\x26\x23\x30\x30\x30\x30\x31\x31\x34\x26\x23\x30\x30\x30\x30\x31\x31\x36\x26\x23\x30\x30\x30\x30\x30\x34\x30\x26\x23\x30\x30\x30\x30\x30\x33\x39\x26\x23\x30\x30\x30\x30\x30\x38\x38\x26\x23\x30\x30\x30\x30\x30\x38\x33\x26\x23\x30\x30\x30\x30\x30\x38\x33\x26\x23\x30\x30\x30\x30\x30\x33\x39\x26\x23\x30\x30\x30\x30\x30\x34\x31\x3e" "\x3c\x49\x4d\x47\x20\x53\x52\x43\x3d\x26\x23\x78\x36\x41\x26\x23\x78\x36\x31\x26\x23\x78\x37\x36\x26\x23\x78\x36\x31\x26\x23\x78\x37\x33\x26\x23\x78\x36\x33\x26\x23\x78\x37\x32\x26\x23\x78\x36\x39\x26\x23\x78\x37\x30\x26\x23\x78\x37\x34\x26\x23\x78\x33\x41\x26\x23\x78\x36\x31\x26\x23\x78\x36\x43\x26\x23\x78\x36\x35\x26\x23\x78\x37\x32\x26\x23\x78\x37\x34\x26\x23\x78\x32\x38\x26\x23\x78\x32\x37\x26\x23\x78\x35\x38\x26\x23\x78\x35\x33\x26\x23\x78\x35\x33\x26\x23\x78\x32\x37\x26\x23\x78\x32\x39\x3e" "\x3c\x49\x4d\x47\x20\x53\x52\x43\x3d\x22\x6a\x61\x76\x20\x20\x20\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x27\x58\x53\x53\x27\x29\x3b\x22\x3e" "\x3c\x49\x4d\x47\x20\x53\x52\x43\x3d\x22\x6a\x61\x76\x26\x23\x78\x30\x39\x3b\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x27\x58\x53\x53\x27\x29\x3b\x22\x3e" "\x3c\x49\x4d\x47\x20\x53\x52\x43\x3d\x22\x6a\x61\x76\x26\x23\x78\x30\x41\x3b\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x27\x58\x53\x53\x27\x29\x3b\x22\x3e" "\x3c\x49\x4d\x47\x20\x53\x52\x43\x3d\x22\x6a\x61\x76\x26\x23\x78\x30\x44\x3b\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x27\x58\x53\x53\x27\x29\x3b\x22\x3e" "\x70\x65\x72\x6c\x20\x2d\x65\x20\x27\x70\x72\x69\x6e\x74\x20\x22\x3c\x49\x4d\x47\x20\x53\x52\x43\x3d\x6a\x61\x76\x61\x5c\x30\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x5c\x22\x58\x53\x53\x5c\x22\x29\x3e\x22\x3b\x27\x20\x3e\x20\x6f\x75\x74" "\x3c\x49\x4d\x47\x20\x53\x52\x43\x3d\x22\x20\x26\x23\x31\x34\x3b\x20\x20\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x27\x58\x53\x53\x27\x29\x3b\x22\x3e" "\x3c\x53\x43\x52\x49\x50\x54\x2f\x58\x53\x53\x20\x53\x52\x43\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x68\x61\x2e\x63\x6b\x65\x72\x73\x2e\x6f\x72\x67\x2f\x78\x73\x73\x2e\x6a\x73\x22\x3e\x3c\x2f\x53\x43\x52\x49\x50\x54\x3e" "\x3c\x42\x4f\x44\x59\x20\x6f\x6e\x6c\x6f\x61\x64\x21\x23\x24\x25\x26\x28\x29\x2a\x7e\x2b\x2d\x5f\x2e\x2c\x3a\x3b\x3f\x40\x5b\x2f\x7c\x5c\x5d\x5e\x60\x3d\x61\x6c\x65\x72\x74\x28\x22\x58\x53\x53\x22\x29\x3e" "\x3c\x53\x43\x52\x49\x50\x54\x2f\x53\x52\x43\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x68\x61\x2e\x63\x6b\x65\x72\x73\x2e\x6f\x72\x67\x2f\x78\x73\x73\x2e\x6a\x73\x22\x3e\x3c\x2f\x53\x43\x52\x49\x50\x54\x3e" "\x3c\x3c\x53\x43\x52\x49\x50\x54\x3e\x61\x6c\x65\x72\x74\x28\x22\x58\x53\x53\x22\x29\x3b\x2f\x2f\x3c\x3c\x2f\x53\x43\x52\x49\x50\x54\x3e" "\x3c\x53\x43\x52\x49\x50\x54\x20\x53\x52\x43\x3d\x68\x74\x74\x70\x3a\x2f\x2f\x68\x61\x2e\x63\x6b\x65\x72\x73\x2e\x6f\x72\x67\x2f\x78\x73\x73\x2e\x6a\x73\x3f\x3c\x20\x42\x20\x3e" "\x3c\x53\x43\x52\x49\x50\x54\x20\x53\x52\x43\x3d\x2f\x2f\x68\x61\x2e\x63\x6b\x65\x72\x73\x2e\x6f\x72\x67\x2f\x2e\x6a\x3e" "\x3c\x49\x4d\x47\x20\x53\x52\x43\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3a\x61\x6c\x65\x72\x74\x28\x27\x58\x53\x53\x27\x29\x22" "\x3c\x69\x66\x72\x61\x6d\x65\x20\x73\x72\x63\x3d\x68\x74\x74\x70\x3a\x2f\x2f\x68\x61\x2e\x63\x6b\x65\x72\x73\x2e\x6f\x72\x67\x2f\x73\x63\x72\x69\x70\x74\x6c\x65\x74\x2e\x68\x74\x6d\x6c\x20\x3c" "\x5c\x22\x3b\x61\x6c\x65\x72\x74\x28\x27\x58\x53\x53\x27\x29\x3b\x2f\x2f" "\x3c\x75\x20\x6f\x6e\x63\x6f\x70\x79\x3d\x61\x6c\x65\x72\x74\x28\x29\x3e\x20\x43\x6f\x70\x79\x20\x6d\x65\x3c\x2f\x75\x3e" "\x3c\x69\x20\x6f\x6e\x77\x68\x65\x65\x6c\x3d\x61\x6c\x65\x72\x74\x28\x31\x29\x3e\x20\x53\x63\x72\x6f\x6c\x6c\x20\x6f\x76\x65\x72\x20\x6d\x65\x20\x3c\x2f\x69\x3e" "\x3c\x70\x6c\x61\x69\x6e\x74\x65\x78\x74\x3e" "\x68\x74\x74\x70\x3a\x2f\x2f\x61\x2f\x25\x25\x33\x30\x25\x33\x30" "\x3c\x2f\x74\x65\x78\x74\x61\x72\x65\x61\x3e\x3c\x73\x63\x72\x69\x70\x74\x3e\x61\x6c\x65\x72\x74\x28\x31\x32\x33\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e" "\x31\x3b\x44\x52\x4f\x50\x20\x54\x41\x42\x4c\x45\x20\x75\x73\x65\x72\x73" "\x31\x27\x3b\x20\x44\x52\x4f\x50\x20\x54\x41\x42\x4c\x45\x20\x75\x73\x65\x72\x73\x2d\x2d\x20\x31" "\x27\x20\x4f\x52\x20\x31\x3d\x31\x20\x2d\x2d\x20\x31" "\x27\x20\x4f\x52\x20\x27\x31\x27\x3d\x27\x31" "\x27\x3b\x20\x45\x58\x45\x43\x20\x73\x70\x5f\x4d\x53\x46\x6f\x72\x45\x61\x63\x68\x54\x61\x62\x6c\x65\x20\x27\x44\x52\x4f\x50\x20\x54\x41\x42\x4c\x45\x20\x3f\x27\x3b\x20\x2d\x2d" "\x20" "\x25" "\x5f" "\x2d" "\x2d\x2d" "\x2d\x2d\x76\x65\x72\x73\x69\x6f\x6e" "\x2d\x2d\x68\x65\x6c\x70" "\x24\x55\x53\x45\x52" "\x2f\x64\x65\x76\x2f\x6e\x75\x6c\x6c\x3b\x20\x74\x6f\x75\x63\x68\x20\x2f\x74\x6d\x70\x2f\x62\x6c\x6e\x73\x2e\x66\x61\x69\x6c\x20\x3b\x20\x65\x63\x68\x6f" "\x60\x74\x6f\x75\x63\x68\x20\x2f\x74\x6d\x70\x2f\x62\x6c\x6e\x73\x2e\x66\x61\x69\x6c\x60" "\x24\x28\x74\x6f\x75\x63\x68\x20\x2f\x74\x6d\x70\x2f\x62\x6c\x6e\x73\x2e\x66\x61\x69\x6c\x29" "\x40\x7b\x5b\x73\x79\x73\x74\x65\x6d\x20\x22\x74\x6f\x75\x63\x68\x20\x2f\x74\x6d\x70\x2f\x62\x6c\x6e\x73\x2e\x66\x61\x69\x6c\x22\x5d\x7d" "\x65\x76\x61\x6c\x28\x22\x70\x75\x74\x73\x20\x27\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x27\x22\x29" "\x53\x79\x73\x74\x65\x6d\x28\x22\x6c\x73\x20\x2d\x61\x6c\x20\x2f\x22\x29" "\x60\x6c\x73\x20\x2d\x61\x6c\x20\x2f\x60" "\x4b\x65\x72\x6e\x65\x6c\x2e\x65\x78\x65\x63\x28\x22\x6c\x73\x20\x2d\x61\x6c\x20\x2f\x22\x29" "\x4b\x65\x72\x6e\x65\x6c\x2e\x65\x78\x69\x74\x28\x31\x29" "\x25\x78\x28\x27\x6c\x73\x20\x2d\x61\x6c\x20\x2f\x27\x29" "\x3c\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x49\x53\x4f\x2d\x38\x38\x35\x39\x2d\x31\x22\x3f\x3e\x3c\x21\x44\x4f\x43\x54\x59\x50\x45\x20\x66\x6f\x6f\x20\x5b\x20\x3c\x21\x45\x4c\x45\x4d\x45\x4e\x54\x20\x66\x6f\x6f\x20\x41\x4e\x59\x20\x3e\x3c\x21\x45\x4e\x54\x49\x54\x59\x20\x78\x78\x65\x20\x53\x59\x53\x54\x45\x4d\x20\x22\x66\x69\x6c\x65\x3a\x2f\x2f\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64\x22\x20\x3e\x5d\x3e\x3c\x66\x6f\x6f\x3e\x26\x78\x78\x65\x3b\x3c\x2f\x66\x6f\x6f\x3e" "\x24\x48\x4f\x4d\x45" "\x24\x45\x4e\x56\x7b\x27\x48\x4f\x4d\x45\x27\x7d" "\x25\x64" "\x25\x73\x25\x73\x25\x73\x25\x73\x25\x73" "\x7b\x30\x7d" "\x25\x2a\x2e\x2a\x73" "\x25\x40" "\x25\x6e" "\x46\x69\x6c\x65\x3a\x2f\x2f\x2f" "\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64\x25\x30\x30" "\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x2e\x2e\x2f\x65\x74\x63\x2f\x68\x6f\x73\x74\x73" "\x28\x29\x20\x7b\x20\x30\x3b\x20\x7d\x3b\x20\x74\x6f\x75\x63\x68\x20\x2f\x74\x6d\x70\x2f\x62\x6c\x6e\x73\x2e\x73\x68\x65\x6c\x6c\x73\x68\x6f\x63\x6b\x31\x2e\x66\x61\x69\x6c\x3b" "\x28\x29\x20\x7b\x20\x5f\x3b\x20\x7d\x20\x3e\x5f\x5b\x24\x28\x24\x28\x29\x29\x5d\x20\x7b\x20\x74\x6f\x75\x63\x68\x20\x2f\x74\x6d\x70\x2f\x62\x6c\x6e\x73\x2e\x73\x68\x65\x6c\x6c\x73\x68\x6f\x63\x6b\x32\x2e\x66\x61\x69\x6c\x3b\x20\x7d" "\x3c\x3c\x3c\x20\x25\x73\x28\x75\x6e\x3d\x27\x25\x73\x27\x29\x20\x3d\x20\x25\x75" "\x2b\x2b\x2b\x41\x54\x48\x30" "\x43\x4f\x4e" "\x50\x52\x4e" "\x41\x55\x58" "\x43\x4c\x4f\x43\x4b\x24" "\x4e\x55\x4c" "\x41\x3a" "\x5a\x5a\x3a" "\x43\x4f\x4d\x31" "\x4c\x50\x54\x31" "\x4c\x50\x54\x32" "\x4c\x50\x54\x33" "\x43\x4f\x4d\x32" "\x43\x4f\x4d\x33" "\x43\x4f\x4d\x34" "\x44\x43\x43\x20\x53\x45\x4e\x44\x20\x53\x54\x41\x52\x54\x4b\x45\x59\x4c\x4f\x47\x47\x45\x52\x20\x30\x20\x30\x20\x30" "\x53\x63\x75\x6e\x74\x68\x6f\x72\x70\x65\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x48\x6f\x73\x70\x69\x74\x61\x6c" "\x50\x65\x6e\x69\x73\x74\x6f\x6e\x65\x20\x43\x6f\x6d\x6d\x75\x6e\x69\x74\x79\x20\x43\x68\x75\x72\x63\x68" "\x4c\x69\x67\x68\x74\x77\x61\x74\x65\x72\x20\x43\x6f\x75\x6e\x74\x72\x79\x20\x50\x61\x72\x6b" "\x4a\x69\x6d\x6d\x79\x20\x43\x6c\x69\x74\x68\x65\x72\x6f\x65" "\x48\x6f\x72\x6e\x69\x6d\x61\x6e\x20\x4d\x75\x73\x65\x75\x6d" "\x73\x68\x69\x74\x61\x6b\x65\x20\x6d\x75\x73\x68\x72\x6f\x6f\x6d\x73" "\x52\x6f\x6d\x61\x6e\x73\x49\x6e\x53\x75\x73\x73\x65\x78\x2e\x63\x6f\x2e\x75\x6b" "\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x63\x75\x6d\x2e\x71\x63\x2e\x63\x61\x2f" "\x43\x72\x61\x69\x67\x20\x43\x6f\x63\x6b\x62\x75\x72\x6e\x2c\x20\x53\x6f\x66\x74\x77\x61\x72\x65\x20\x53\x70\x65\x63\x69\x61\x6c\x69\x73\x74" "\x4c\x69\x6e\x64\x61\x20\x43\x61\x6c\x6c\x61\x68\x61\x6e" "\x44\x72\x2e\x20\x48\x65\x72\x6d\x61\x6e\x20\x49\x2e\x20\x4c\x69\x62\x73\x68\x69\x74\x7a" "\x6d\x61\x67\x6e\x61\x20\x63\x75\x6d\x20\x6c\x61\x75\x64\x65" "\x53\x75\x70\x65\x72\x20\x42\x6f\x77\x6c\x20\x58\x58\x58" "\x6d\x65\x64\x69\x65\x76\x61\x6c\x20\x65\x72\x65\x63\x74\x69\x6f\x6e\x20\x6f\x66\x20\x70\x61\x72\x61\x70\x65\x74\x73" "\x65\x76\x61\x6c\x75\x61\x74\x65" "\x6d\x6f\x63\x68\x61" "\x65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e" "\x41\x72\x73\x65\x6e\x61\x6c\x20\x63\x61\x6e\x61\x6c" "\x63\x6c\x61\x73\x73\x69\x63" "\x54\x79\x73\x6f\x6e\x20\x47\x61\x79" "\x44\x69\x63\x6b\x20\x56\x61\x6e\x20\x44\x79\x6b\x65" "\x62\x61\x73\x65\x6d\x65\x6e\x74" "\x49\x66\x20\x79\x6f\x75\x27\x72\x65\x20\x72\x65\x61\x64\x69\x6e\x67\x20\x74\x68\x69\x73\x2c\x20\x79\x6f\x75\x27\x76\x65\x20\x62\x65\x65\x6e\x20\x69\x6e\x20\x61\x20\x63\x6f\x6d\x61\x20\x66\x6f\x72\x20\x61\x6c\x6d\x6f\x73\x74\x20\x32\x30\x20\x79\x65\x61\x72\x73\x20\x6e\x6f\x77\x2e\x20\x57\x65\x27\x72\x65\x20\x74\x72\x79\x69\x6e\x67\x20\x61\x20\x6e\x65\x77\x20\x74\x65\x63\x68\x6e\x69\x71\x75\x65\x2e\x20\x57\x65\x20\x64\x6f\x6e\x27\x74\x20\x6b\x6e\x6f\x77\x20\x77\x68\x65\x72\x65\x20\x74\x68\x69\x73\x20\x6d\x65\x73\x73\x61\x67\x65\x20\x77\x69\x6c\x6c\x20\x65\x6e\x64\x20\x75\x70\x20\x69\x6e\x20\x79\x6f\x75\x72\x20\x64\x72\x65\x61\x6d\x2c\x20\x62\x75\x74\x20\x77\x65\x20\x68\x6f\x70\x65\x20\x69\x74\x20\x77\x6f\x72\x6b\x73\x2e\x20\x50\x6c\x65\x61\x73\x65\x20\x77\x61\x6b\x65\x20\x75\x70\x2c\x20\x77\x65\x20\x6d\x69\x73\x73\x20\x79\x6f\x75\x2e" "\x52\x6f\x73\x65\x73\x20\x61\x72\x65\x20\x1b\x5b\x30\x3b\x33\x31\x6d\x72\x65\x64\x1b\x5b\x30\x6d\x2c\x20\x76\x69\x6f\x6c\x65\x74\x73\x20\x61\x72\x65\x20\x1b\x5b\x30\x3b\x33\x34\x6d\x62\x6c\x75\x65\x2e\x20\x48\x6f\x70\x65\x20\x79\x6f\x75\x20\x65\x6e\x6a\x6f\x79\x20\x74\x65\x72\x6d\x69\x6e\x61\x6c\x20\x68\x75\x65" "\x42\x75\x74\x20\x6e\x6f\x77\x2e\x2e\x2e\x1b\x5b\x32\x30\x43\x66\x6f\x72\x20\x6d\x79\x20\x67\x72\x65\x61\x74\x65\x73\x74\x20\x74\x72\x69\x63\x6b\x2e\x2e\x2e\x1b\x5b\x38\x6d" "\x54\x68\x65\x20\x71\x75\x69\x63\x08\x08\x08\x08\x08\x08\x6b\x20\x62\x72\x6f\x77\x6e\x20\x66\x6f\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x78\x2e\x2e\x2e\x20\x5b\x42\x65\x65\x65\x65\x70\x5d" "\x50\x6f\x77\x65\x72\xd9\x84\xd9\x8f\xd9\x84\xd9\x8f\xd8\xb5\xd9\x91\xd8\xa8\xd9\x8f\xd9\x84\xd9\x8f\xd9\x84\xd8\xb5\xd9\x91\xd8\xa8\xd9\x8f\xd8\xb1\xd8\xb1\xd9\x8b\x20\xe0\xa5\xa3\x20\xe0\xa5\xa3\x68\x20\xe0\xa5\xa3\x20\xe0\xa5\xa3\xe5\x86\x97" "\xf0\x9f\x8f\xb3\x30\xf0\x9f\x8c\x88\xef\xb8\x8f" "\xe0\xb0\x9c\xe0\xb1\x8d\xe0\xb0\x9e\xe2\x80\x8c\xe0\xb0\xbe" "\xda\xaf\xda\x86\xd9\xbe\xda\x98" "\x7b\x25\x20\x70\x72\x69\x6e\x74\x20\x27\x78\x27\x20\x2a\x20\x36\x34\x20\x2a\x20\x31\x30\x32\x34\x2a\x2a\x33\x20\x25\x7d" "\x7b\x7b\x20\x22\x22\x2e\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f\x2e\x5f\x5f\x6d\x72\x6f\x5f\x5f\x5b\x32\x5d\x2e\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f\x28\x29\x5b\x34\x30\x5d\x28\x22\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64\x22\x29\x2e\x72\x65\x61\x64\x28\x29\x20\x7d\x7d" imap-codec-1.0.0/imap-codec/fuzz/fuzz_targets/000077500000000000000000000000001447115025300212415ustar00rootroot00000000000000imap-codec-1.0.0/imap-codec/fuzz/fuzz_targets/authenticate_data.rs000066400000000000000000000002121447115025300252510ustar00rootroot00000000000000#![no_main] use imap_codec::AuthenticateDataCodec; use imap_codec_fuzz::impl_decode_target; impl_decode_target!(AuthenticateDataCodec); imap-codec-1.0.0/imap-codec/fuzz/fuzz_targets/authenticate_data_to_bytes_and_back.rs000066400000000000000000000003121447115025300307640ustar00rootroot00000000000000#![no_main] use imap_codec::{imap_types::auth::AuthenticateData, AuthenticateDataCodec}; use imap_codec_fuzz::impl_to_bytes_and_back; impl_to_bytes_and_back!(AuthenticateDataCodec, AuthenticateData); imap-codec-1.0.0/imap-codec/fuzz/fuzz_targets/command.rs000066400000000000000000000001701447115025300232230ustar00rootroot00000000000000#![no_main] use imap_codec::CommandCodec; use imap_codec_fuzz::impl_decode_target; impl_decode_target!(CommandCodec); imap-codec-1.0.0/imap-codec/fuzz/fuzz_targets/command_to_bytes_and_back.rs000066400000000000000000000002511447115025300267350ustar00rootroot00000000000000#![no_main] use imap_codec::{imap_types::command::Command, CommandCodec}; use imap_codec_fuzz::impl_to_bytes_and_back; impl_to_bytes_and_back!(CommandCodec, Command); imap-codec-1.0.0/imap-codec/fuzz/fuzz_targets/greeting.rs000066400000000000000000000001721447115025300234130ustar00rootroot00000000000000#![no_main] use imap_codec::GreetingCodec; use imap_codec_fuzz::impl_decode_target; impl_decode_target!(GreetingCodec); imap-codec-1.0.0/imap-codec/fuzz/fuzz_targets/greeting_to_bytes_and_back.rs000066400000000000000000000002561447115025300271300ustar00rootroot00000000000000#![no_main] use imap_codec::{imap_types::response::Greeting, GreetingCodec}; use imap_codec_fuzz::impl_to_bytes_and_back; impl_to_bytes_and_back!(GreetingCodec, Greeting); imap-codec-1.0.0/imap-codec/fuzz/fuzz_targets/response.rs000066400000000000000000000001721447115025300234450ustar00rootroot00000000000000#![no_main] use imap_codec::ResponseCodec; use imap_codec_fuzz::impl_decode_target; impl_decode_target!(ResponseCodec); imap-codec-1.0.0/imap-codec/fuzz/fuzz_targets/response_to_bytes_and_back.rs000066400000000000000000000002561447115025300271620ustar00rootroot00000000000000#![no_main] use imap_codec::{imap_types::response::Response, ResponseCodec}; use imap_codec_fuzz::impl_to_bytes_and_back; impl_to_bytes_and_back!(ResponseCodec, Response); imap-codec-1.0.0/imap-codec/fuzz/imap.dict000066400000000000000000000022601447115025300203050ustar00rootroot00000000000000"a001 login mrc secret" "a001 OK LOGIN completed" "a002 select inbox" "* 18 EXISTS" "* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)" "* 2 RECENT" "* OK [UNSEEN 17] Message 17 is the first unseen message" "* OK [UIDVALIDITY 3857529045] UIDs valid" "a002 OK [READ-WRITE] SELECT completed" "a003 fetch 12 full" "* 12 FETCH (FLAGS (\\Seen) INTERNALDATE \"17-Jul-1996 02:44:25 -0700\" RFC822.SIZE 4286 ENVELOPE (\"Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\" \"IMAP4rev1 WG mtg summary and minutes\" ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((NIL NIL \"imap\" \"cac.washington.edu\")) ((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\")(\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) NIL NIL \"\") BODY (\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL \"7BIT\" 3028 92))" "a003 OK FETCH completed" "a004 fetch 12 body[header]" "a004 OK FETCH completed" "a005 store 12 +flags \\deleted" "* 12 FETCH (FLAGS (\\Seen \\Deleted))" "a005 OK +FLAGS completed" "a006 logout" "* BYE IMAP4rev1 server terminating connection" "a006 OK LOGOUT completed" imap-codec-1.0.0/imap-codec/fuzz/src/000077500000000000000000000000001447115025300173015ustar00rootroot00000000000000imap-codec-1.0.0/imap-codec/fuzz/src/lib.rs000066400000000000000000000077051447115025300204260ustar00rootroot00000000000000#[macro_export] macro_rules! impl_decode_target { ($codec:ident) => { use libfuzzer_sys::fuzz_target; fuzz_target!(|input: &[u8]| { #[cfg(feature = "debug")] use imap_codec::imap_types::utils::escape_byte_string; use imap_codec::{decode::Decoder, encode::Encoder}; #[cfg(feature = "debug")] println!("[!] Input: {}", escape_byte_string(input)); if let Ok((_rem, parsed1)) = $codec::default().decode(input) { #[cfg(feature = "debug")] { let input = &input[..input.len() - _rem.len()]; println!("[!] Consumed: {}", escape_byte_string(input)); println!("[!] Parsed1: {parsed1:?}"); } let output = $codec::default().encode(&parsed1).dump(); #[cfg(feature = "debug")] println!("[!] Serialized: {}", escape_byte_string(&output)); let (rem, parsed2) = $codec::default().decode(&output).unwrap(); #[cfg(feature = "debug")] println!("[!] Parsed2: {parsed2:?}"); assert!(rem.is_empty()); assert_eq!(parsed1, parsed2); /* #[cfg(feature = "split")] { // Check splits ... #[cfg(feature = "debug")] println!("[!] Full: {}", escape_byte_string(&output)); #[cfg(feature = "debug")] println!("[!] Full: {parsed2:?}"); for index in 0..=output.len() { let partial = &output[..index]; #[cfg(feature = "debug")] println!("[!] Split (..{index:>3}): {}", escape_byte_string(partial)); match <$decoder>::decode(partial) { Ok((rem, out)) => { assert!(rem.is_empty()); assert_eq!(index, output.len()); print!("\r{index}"); } Err(error) => match error { DecodeError::Incomplete => { assert!(index < output.len()); } DecodeError::LiteralFound { .. } => { assert!(index < output.len()); } DecodeError::Failed => { panic!("Expected `Ok` or `Incomplete`, got `Failed`"); } }, } } } */ } else { #[cfg(feature = "debug")] println!("[!] "); } #[cfg(feature = "debug")] println!("{}", str::repeat("-", 120)); }); }; } #[macro_export] macro_rules! impl_to_bytes_and_back { ($codec:tt, $object:tt) => { use libfuzzer_sys::fuzz_target; fuzz_target!(|input: $object| { #[cfg(feature = "debug")] use imap_codec::imap_types::utils::escape_byte_string; use imap_codec::{decode::Decoder, encode::Encoder}; #[cfg(feature = "debug")] println!("[!] Input: {:?}", input); let buffer = <$codec>::default().encode(&input).dump(); #[cfg(feature = "debug")] println!("[!] Serialized: {}", escape_byte_string(&buffer)); let (rem, parsed) = <$codec>::default().decode(&buffer).unwrap(); assert!(rem.is_empty()); #[cfg(feature = "debug")] println!("[!] Parsed: {parsed:?}"); assert_eq!(input, parsed); #[cfg(feature = "debug")] println!("{}", str::repeat("-", 120)); }); }; } imap-codec-1.0.0/imap-codec/fuzz/terminals.dict000066400000000000000000000022451447115025300213600ustar00rootroot00000000000000"==" "\\*" "7BIT" "8BIT" "ALERT" "ALL" "\\Answered" "ANSWERED" "APPEND" "APPLICATION" "Apr" "AUDIO" "Aug" "AUTH=" "AUTHENTICATE" "BAD" "BADCHARSET" "BASE64" "BCC" "BEFORE" "BINARY" "BODY" "BODY.PEEK" "BYE" "CAPABILITY" "CC" "CHARSET" "CHECK" "CLOSE" "COPY" "CREATE" "Dec" "DELETE" "\\Deleted" "DELETED" "\\Draft" "DRAFT" "ENVELOPE" "EXAMINE" "EXISTS" "EXPUNGE" "FAST" "Feb" "FETCH" "\\Flagged" "FLAGGED" "FLAGS" "FROM" "FULL" ".HEADER" "HEADER" "HEADER.FIELDS" "IMAGE" "IMAP4rev1" "INBOX" "INTERNALDATE" "Jan" "Jul" "Jun" "KEYWORD" "LARGER" "LIST" "LOGIN" "LOGOUT" "LSUB" "Mar" "\\Marked" "May" "MESSAGE" "MESSAGES" "MIME" "NEW" "NIL" "NO" "\\Noinferiors" "NOOP" "\\Noselect" ".NOT" "NOT" "Nov" "Oct" "OK" "OLD" "ON" "OR" "PARSE" "PERMANENTFLAGS" "PREAUTH" "QUOTED-PRINTABLE" "READ-ONLY" "READ-WRITE" "\\Recent" "RECENT" "RENAME" "RFC822" "RFC822.SIZE" "SEARCH" "\\Seen" "SEEN" "SELECT" "SENTBEFORE" "SENTON" "SENTSINCE" "Sep" ".SILENT" "SINCE" ".SIZE" "SMALLER" "STARTTLS" "STATUS" "STORE" "STRUCTURE" "SUBJECT" "SUBSCRIBE" ".TEXT" "TEXT" "TO" "TRYCREATE" "UID" "UIDNEXT" "UIDVALIDITY" "UNANSWERED" "UNDELETED" "UNDRAFT" "UNFLAGGED" "UNKEYWORD" "\\Unmarked" "UNSEEN" "UNSUBSCRIBE" "VIDEO"imap-codec-1.0.0/imap-codec/src/000077500000000000000000000000001447115025300163035ustar00rootroot00000000000000imap-codec-1.0.0/imap-codec/src/auth.rs000066400000000000000000000053361447115025300176210ustar00rootroot00000000000000#[cfg(not(feature = "quirk_crlf_relaxed"))] use abnf_core::streaming::crlf; #[cfg(feature = "quirk_crlf_relaxed")] use abnf_core::streaming::crlf_relaxed as crlf; use imap_types::{ auth::{AuthMechanism, AuthenticateData}, secret::Secret, }; use nom::{combinator::map, sequence::terminated}; use crate::{ core::{atom, base64}, decode::IMAPResult, }; // ----- Unsorted IMAP parsers ----- /// `auth-type = atom` /// /// Note: Defined by \[SASL\] pub(crate) fn auth_type(input: &[u8]) -> IMAPResult<&[u8], AuthMechanism> { let (rem, atom) = atom(input)?; Ok((rem, AuthMechanism::from(atom))) } /// `authenticate = "AUTHENTICATE" SP auth-type *(CRLF base64)` (edited) /// /// ```text /// authenticate = base64 CRLF /// vvvvvvvvvvvv /// | /// This is parsed here. /// CRLF is additionally parsed in this parser. /// FIXME: Multiline base64 currently does not work. /// ``` pub(crate) fn authenticate_data(input: &[u8]) -> IMAPResult<&[u8], AuthenticateData> { map(terminated(base64, crlf), |data| { AuthenticateData(Secret::new(data)) })(input) // FIXME: many0 deleted } #[cfg(test)] mod tests { use super::*; use crate::testing::{known_answer_test_encode, known_answer_test_parse}; #[test] fn test_encode_auth_mechanism() { let tests = [ (AuthMechanism::Plain, b"PLAIN".as_ref()), (AuthMechanism::Login, b"LOGIN"), (AuthMechanism::try_from("PLAINX").unwrap(), b"PLAINX"), (AuthMechanism::try_from("LOGINX").unwrap(), b"LOGINX"), (AuthMechanism::try_from("XOAUTH2X").unwrap(), b"XOAUTH2X"), ]; for test in tests { known_answer_test_encode(test); } } #[test] fn test_parse_auth_type() { let tests = [ (b"plain ".as_ref(), b" ".as_ref(), AuthMechanism::Plain), (b"pLaiN ", b" ", AuthMechanism::Plain), (b"lOgiN ", b" ", AuthMechanism::Login), (b"login ", b" ", AuthMechanism::Login), (b"loginX ", b" ", AuthMechanism::try_from("loginX").unwrap()), ( b"loginX ", b" ", AuthMechanism::try_from(b"loginX".as_ref()).unwrap(), ), (b"Xplain ", b" ", AuthMechanism::try_from("Xplain").unwrap()), ( b"Xplain ", b" ", AuthMechanism::try_from(b"Xplain".as_ref()).unwrap(), ), (b"xoauth2 ".as_ref(), b" ".as_ref(), AuthMechanism::XOAuth2), (b"xOauTh2 ", b" ", AuthMechanism::XOAuth2), ]; for test in tests { known_answer_test_parse(test, auth_type); } } } imap-codec-1.0.0/imap-codec/src/body.rs000066400000000000000000000634141447115025300176160ustar00rootroot00000000000000use abnf_core::streaming::sp; use imap_types::{ body::{ BasicFields, Body, BodyExtension, BodyStructure, Disposition, Language, Location, MultiPartExtensionData, SinglePartExtensionData, SpecificFields, }, core::{IString, NString, NonEmptyVec}, }; use nom::{ branch::alt, bytes::streaming::{tag, tag_no_case}, combinator::{map, opt}, multi::{many0, many1, separated_list1}, sequence::{delimited, preceded, tuple}, }; use crate::{ core::{nil, nstring, number, string}, decode::{IMAPErrorKind, IMAPParseError, IMAPResult}, envelope::envelope, }; /// `body = "(" (body-type-1part / body-type-mpart) ")"` /// /// Note: This parser is recursively defined. Thus, in order to not overflow the stack, /// it is needed to limit how may recursions are allowed. (8 should suffice). pub(crate) fn body( remaining_recursions: usize, ) -> impl Fn(&[u8]) -> IMAPResult<&[u8], BodyStructure> { move |input: &[u8]| body_limited(input, remaining_recursions) } fn body_limited<'a>( input: &'a [u8], remaining_recursions: usize, ) -> IMAPResult<&'a [u8], BodyStructure> { if remaining_recursions == 0 { return Err(nom::Err::Failure(IMAPParseError { input, kind: IMAPErrorKind::RecursionLimitExceeded, })); } let body_type_1part = move |input: &'a [u8]| { body_type_1part_limited(input, remaining_recursions.saturating_sub(1)) }; let body_type_mpart = move |input: &'a [u8]| { body_type_mpart_limited(input, remaining_recursions.saturating_sub(1)) }; delimited( tag(b"("), alt((body_type_1part, body_type_mpart)), tag(b")"), )(input) } /// `body-type-1part = ( /// body-type-basic / /// body-type-msg / /// body-type-text /// ) /// [SP body-ext-1part]` /// /// Note: This parser is recursively defined. Thus, in order to not overflow the stack, /// it is needed to limit how may recursions are allowed. fn body_type_1part_limited<'a>( input: &'a [u8], remaining_recursions: usize, ) -> IMAPResult<&'a [u8], BodyStructure> { if remaining_recursions == 0 { return Err(nom::Err::Failure(IMAPParseError { input, kind: IMAPErrorKind::RecursionLimitExceeded, })); } let body_type_msg = move |input: &'a [u8]| body_type_msg_limited(input, 8); let mut parser = tuple(( alt((body_type_msg, body_type_text, body_type_basic)), opt(preceded(sp, body_ext_1part)), )); let (remaining, ((basic, specific), extension_data)) = parser(input)?; Ok(( remaining, BodyStructure::Single { body: Body { basic, specific }, extension_data, }, )) } /// `body-type-basic = media-basic SP body-fields` /// /// MESSAGE subtype MUST NOT be "RFC822" pub(crate) fn body_type_basic(input: &[u8]) -> IMAPResult<&[u8], (BasicFields, SpecificFields)> { let mut parser = tuple((media_basic, sp, body_fields)); let (remaining, ((type_, subtype), _, basic)) = parser(input)?; Ok(( remaining, ( basic, SpecificFields::Basic { r#type: type_, subtype, }, ), )) } /// `body-type-msg = media-message SP /// body-fields SP /// envelope SP /// body SP /// body-fld-lines` /// /// Note: This parser is recursively defined. Thus, in order to not overflow the stack, /// it is needed to limit how may recursions are allowed. (8 should suffice). fn body_type_msg_limited<'a>( input: &'a [u8], remaining_recursions: usize, ) -> IMAPResult<&'a [u8], (BasicFields, SpecificFields)> { if remaining_recursions == 0 { return Err(nom::Err::Failure(IMAPParseError { input, kind: IMAPErrorKind::RecursionLimitExceeded, })); } let body = move |input: &'a [u8]| body_limited(input, remaining_recursions.saturating_sub(1)); let mut parser = tuple(( media_message, sp, body_fields, sp, envelope, sp, body, sp, body_fld_lines, )); let (remaining, (_, _, basic, _, envelope, _, body_structure, _, number_of_lines)) = parser(input)?; Ok(( remaining, ( basic, SpecificFields::Message { envelope: Box::new(envelope), body_structure: Box::new(body_structure), number_of_lines, }, ), )) } /// `body-type-text = media-text SP /// body-fields SP /// body-fld-lines` pub(crate) fn body_type_text(input: &[u8]) -> IMAPResult<&[u8], (BasicFields, SpecificFields)> { let mut parser = tuple((media_text, sp, body_fields, sp, body_fld_lines)); let (remaining, (subtype, _, basic, _, number_of_lines)) = parser(input)?; Ok(( remaining, ( basic, SpecificFields::Text { subtype, number_of_lines, }, ), )) } /// `body-fields = body-fld-param SP /// body-fld-id SP /// body-fld-desc SP /// body-fld-enc SP /// body-fld-octets` pub(crate) fn body_fields(input: &[u8]) -> IMAPResult<&[u8], BasicFields> { let mut parser = tuple(( body_fld_param, sp, body_fld_id, sp, body_fld_desc, sp, body_fld_enc, sp, body_fld_octets, )); let (remaining, (parameter_list, _, id, _, description, _, content_transfer_encoding, _, size)) = parser(input)?; Ok(( remaining, BasicFields { parameter_list, id, description, content_transfer_encoding, size, }, )) } /// `body-fld-param = "(" /// string SP string /// *(SP string SP string) /// ")" / nil` pub(crate) fn body_fld_param(input: &[u8]) -> IMAPResult<&[u8], Vec<(IString, IString)>> { let mut parser = alt(( delimited( tag(b"("), separated_list1( sp, map(tuple((string, sp, string)), |(key, _, value)| (key, value)), ), tag(b")"), ), map(nil, |_| vec![]), )); let (remaining, parsed_body_fld_param) = parser(input)?; Ok((remaining, parsed_body_fld_param)) } #[inline] /// `body-fld-id = nstring` pub(crate) fn body_fld_id(input: &[u8]) -> IMAPResult<&[u8], NString> { nstring(input) } #[inline] /// `body-fld-desc = nstring` pub(crate) fn body_fld_desc(input: &[u8]) -> IMAPResult<&[u8], NString> { nstring(input) } #[inline] /// `body-fld-enc = ( /// DQUOTE ( /// "7BIT" / /// "8BIT" / /// "BINARY" / /// "BASE64"/ /// "QUOTED-PRINTABLE" /// ) DQUOTE /// ) / string` /// /// Simplified... /// /// `body-fld-enc = string` /// /// TODO: why the special case? pub(crate) fn body_fld_enc(input: &[u8]) -> IMAPResult<&[u8], IString> { string(input) } #[inline] /// `body-fld-octets = number` /// /// # Quirks /// /// The following erroneous messages were observed: /// /// * A negative number, specifically `-1`, in Dovecot. #[allow(clippy::needless_return)] pub(crate) fn body_fld_octets(input: &[u8]) -> IMAPResult<&[u8], u32> { #[cfg(not(feature = "quirk_rectify_numbers"))] return number(input); #[cfg(feature = "quirk_rectify_numbers")] { return alt(( number, map(tuple((tag("-"), number)), |(_, _)| { log::warn!("Rectified negative number to 0"); 0 }), ))(input); } } #[inline] /// `body-fld-lines = number` pub(crate) fn body_fld_lines(input: &[u8]) -> IMAPResult<&[u8], u32> { number(input) } /// ```abnf /// body-ext-1part = body-fld-md5 /// [SP body-fld-dsp /// [SP body-fld-lang /// [SP body-fld-loc *(SP body-extension)] /// ] /// ] /// ``` /// /// Note: MUST NOT be returned on non-extensible "BODY" fetch. pub(crate) fn body_ext_1part(input: &[u8]) -> IMAPResult<&[u8], SinglePartExtensionData> { map( tuple(( body_fld_md5, opt(map( tuple(( preceded(sp, body_fld_dsp), opt(map( tuple(( preceded(sp, body_fld_lang), opt(map( tuple(( preceded(sp, body_fld_loc), many0(preceded(sp, body_extension(8))), )), |(location, extensions)| Location { location, extensions, }, )), )), |(language, tail)| Language { language, tail }, )), )), |(disposition, tail)| Disposition { disposition, tail }, )), )), |(md5, tail)| SinglePartExtensionData { md5, tail }, )(input) } #[inline] /// `body-fld-md5 = nstring` pub(crate) fn body_fld_md5(input: &[u8]) -> IMAPResult<&[u8], NString> { nstring(input) } /// `body-fld-dsp = "(" string SP body-fld-param ")" / nil` #[allow(clippy::type_complexity)] pub(crate) fn body_fld_dsp( input: &[u8], ) -> IMAPResult<&[u8], Option<(IString, Vec<(IString, IString)>)>> { alt(( delimited( tag(b"("), map( tuple((string, sp, body_fld_param)), |(string, _, body_fld_param)| Some((string, body_fld_param)), ), tag(b")"), ), map(nil, |_| None), ))(input) } /// `body-fld-lang = nstring / "(" string *(SP string) ")"` pub(crate) fn body_fld_lang(input: &[u8]) -> IMAPResult<&[u8], Vec> { alt(( map(nstring, |nstring| match nstring.0 { Some(item) => vec![item], None => vec![], }), delimited(tag(b"("), separated_list1(sp, string), tag(b")")), ))(input) } #[inline] /// `body-fld-loc = nstring` pub(crate) fn body_fld_loc(input: &[u8]) -> IMAPResult<&[u8], NString> { nstring(input) } /// Future expansion. /// /// Client implementations MUST accept body-extension fields. /// Server implementations MUST NOT generate body-extension fields except as defined by /// future standard or standards-track revisions of this specification. /// /// ```abnf /// body-extension = nstring / /// number / /// "(" body-extension *(SP body-extension) ")" /// ``` /// /// Note: This parser is recursively defined. Thus, in order to not overflow the stack, /// it is needed to limit how may recursions are allowed. (8 should suffice). pub(crate) fn body_extension( remaining_recursions: usize, ) -> impl Fn(&[u8]) -> IMAPResult<&[u8], BodyExtension> { move |input: &[u8]| body_extension_limited(input, remaining_recursions) } fn body_extension_limited<'a>( input: &'a [u8], remaining_recursion: usize, ) -> IMAPResult<&'a [u8], BodyExtension> { if remaining_recursion == 0 { return Err(nom::Err::Failure(IMAPParseError { input, kind: IMAPErrorKind::RecursionLimitExceeded, })); } let body_extension = move |input: &'a [u8]| body_extension_limited(input, remaining_recursion.saturating_sub(1)); alt(( map(nstring, BodyExtension::NString), map(number, BodyExtension::Number), map( delimited(tag(b"("), separated_list1(sp, body_extension), tag(b")")), |body_extensions| BodyExtension::List(NonEmptyVec::unvalidated(body_extensions)), ), ))(input) } // --- /// `body-type-mpart = 1*body SP media-subtype [SP body-ext-mpart]` /// /// Note: This parser is recursively defined. Thus, in order to not overflow the stack, /// it is needed to limit how may recursions are allowed. fn body_type_mpart_limited( input: &[u8], remaining_recursion: usize, ) -> IMAPResult<&[u8], BodyStructure> { if remaining_recursion == 0 { return Err(nom::Err::Failure(IMAPParseError { input, kind: IMAPErrorKind::RecursionLimitExceeded, })); } let mut parser = tuple(( many1(body(remaining_recursion)), sp, media_subtype, opt(preceded(sp, body_ext_mpart)), )); let (remaining, (bodies, _, subtype, extension_data)) = parser(input)?; Ok(( remaining, BodyStructure::Multi { // Safety: `unwrap` can't panic due to the use of `many1`. bodies: NonEmptyVec::try_from(bodies).unwrap(), subtype, extension_data, }, )) } /// ```abnf /// body-ext-mpart = body-fld-param /// [SP body-fld-dsp /// [SP body-fld-lang /// [SP body-fld-loc *(SP body-extension)] /// ] /// ] /// ``` /// /// Note: MUST NOT be returned on non-extensible "BODY" fetch. pub(crate) fn body_ext_mpart(input: &[u8]) -> IMAPResult<&[u8], MultiPartExtensionData> { map( tuple(( body_fld_param, opt(map( tuple(( preceded(sp, body_fld_dsp), opt(map( tuple(( preceded(sp, body_fld_lang), opt(map( tuple(( preceded(sp, body_fld_loc), many0(preceded(sp, body_extension(8))), )), |(location, extensions)| Location { location, extensions, }, )), )), |(language, tail)| Language { language, tail }, )), )), |(disposition, tail)| Disposition { disposition, tail }, )), )), |(parameter_list, tail)| MultiPartExtensionData { parameter_list, tail, }, )(input) } // --- /// `media-basic = ( /// ( DQUOTE /// ( /// "APPLICATION" / /// "AUDIO" / /// "IMAGE" / /// "MESSAGE" / /// "VIDEO" /// ) DQUOTE /// ) / string /// ) SP media-subtype` /// /// Simplified... /// /// `media-basic = string SP media-subtype` /// /// TODO: Why the special case? /// /// Defined in [MIME-IMT] pub(crate) fn media_basic(input: &[u8]) -> IMAPResult<&[u8], (IString, IString)> { let mut parser = tuple((string, sp, media_subtype)); let (remaining, (type_, _, subtype)) = parser(input)?; Ok((remaining, (type_, subtype))) } #[inline] /// `media-subtype = string` /// /// Defined in [MIME-IMT] pub(crate) fn media_subtype(input: &[u8]) -> IMAPResult<&[u8], IString> { string(input) } #[inline] /// `media-message = DQUOTE "MESSAGE" DQUOTE SP /// DQUOTE "RFC822" DQUOTE` /// /// Simplified: /// /// `media-message = "\"MESSAGE\" \"RFC822\""` /// /// Defined in [MIME-IMT] /// /// "message" "rfc822" basic specific-for-message-rfc822 extension pub(crate) fn media_message(input: &[u8]) -> IMAPResult<&[u8], &[u8]> { tag_no_case(b"\"MESSAGE\" \"RFC822\"")(input) } /// `media-text = DQUOTE "TEXT" DQUOTE SP media-subtype` /// /// Defined in [MIME-IMT] /// /// "text" "?????" basic specific-for-text extension pub(crate) fn media_text(input: &[u8]) -> IMAPResult<&[u8], IString> { let mut parser = preceded(tag_no_case(b"\"TEXT\" "), media_subtype); let (remaining, media_subtype) = parser(input)?; Ok((remaining, media_subtype)) } #[cfg(test)] mod tests { use std::num::NonZeroU32; use imap_types::{ core::{Literal, Quoted}, fetch::MessageDataItem, response::{Data, Response}, }; use super::*; use crate::testing::{kat_inverse_response, known_answer_test_encode}; #[test] fn test_parse_media_basic() { media_basic(b"\"application\" \"xxx\"").unwrap(); media_basic(b"\"unknown\" \"test\"").unwrap(); media_basic(b"\"x\" \"xxx\"").unwrap(); } #[test] fn test_parse_media_message() { media_message(b"\"message\" \"rfc822\"").unwrap(); } #[test] fn test_parse_media_text() { media_text(b"\"text\" \"html\"").unwrap(); } #[test] fn test_parse_body_ext_1part() { for test in [ b"nil|xxx".as_ref(), b"\"md5\"|xxx".as_ref(), b"\"md5\" nil|xxx".as_ref(), b"\"md5\" (\"dsp\" nil)|xxx".as_ref(), b"\"md5\" (\"dsp\" (\"key\" \"value\")) nil|xxx".as_ref(), b"\"md5\" (\"dsp\" (\"key\" \"value\")) \"swedish\"|xxx".as_ref(), b"\"md5\" (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\")|xxx".as_ref(), b"\"md5\" (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") nil|xxx".as_ref(), b"\"md5\" (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\"|xxx".as_ref(), b"\"md5\" (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\" (1 \"2\" (nil 4))|xxx".as_ref(), b"\"AABB\" NIL NIL NIL 1337|xxx", b"\"AABB\" NIL NIL NIL (1337)|xxx", b"\"AABB\" NIL NIL NIL (1337 1337)|xxx", b"\"AABB\" NIL NIL NIL (1337 (1337 (1337 \"FOO\" {0}\r\n)))|xxx", ] .iter() { let (rem, out) = body_ext_1part(test).unwrap(); println!("{:?}", out); assert_eq!(rem, b"|xxx"); } } #[test] fn test_body_rec() { let _ = body(8)(str::repeat("(", 1_000_000).as_bytes()); } #[test] fn test_parse_body_ext_mpart() { for test in [ b"nil|xxx".as_ref(), b"(\"key\" \"value\")|xxx".as_ref(), b"(\"key\" \"value\") nil|xxx".as_ref(), b"(\"key\" \"value\") (\"dsp\" nil)|xxx".as_ref(), b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) nil|xxx".as_ref(), b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) \"swedish\"|xxx".as_ref(), b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\")|xxx".as_ref(), b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") nil|xxx".as_ref(), b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\"|xxx".as_ref(), b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\" (1 \"2\" (nil 4))|xxx".as_ref(), b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\" (1 \"2\" (nil 4) {0}\r\n)|xxx".as_ref(), b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\" {0}\r\n {0}\r\n|xxx".as_ref(), ] .iter() { let (rem, out) = body_ext_mpart(test).unwrap(); println!("{:?}", out); assert_eq!(rem, b"|xxx"); } } #[test] fn test_parse_body() { dbg!(body(9)(b"((((((({0}\r\n {0}\r\n NIL NIL NIL {0}\r\n 0 \"FOO\" NIL NIL \"LOCATION\" 1337) \"mixed\") \"mixed\") \"mixed\") \"mixed\") \"mixed\") \"mixed\")|xxx").unwrap()); } #[test] fn test_kat_inverse_response_data() { kat_inverse_response(&[( b"* 3372220415 FETCH (BODYSTRUCTURE ((((((({0}\r\n {0}\r\n NIL NIL NIL {0}\r\n 0 \"FOO\" NIL NIL \"LOCATION\" 1337) \"mixed\") \"mixed\") \"mixed\") \"mixed\") \"mixed\") \"mixed\"))\r\n".as_ref(), b"".as_ref(), Response::Data(Data::Fetch { seq: NonZeroU32::try_from(3372220415).unwrap(), items: NonEmptyVec::from(MessageDataItem::BodyStructure( BodyStructure::Multi { bodies: NonEmptyVec::from(BodyStructure::Multi { bodies: NonEmptyVec::from(BodyStructure::Multi { bodies: NonEmptyVec::from(BodyStructure::Multi { bodies: NonEmptyVec::from(BodyStructure::Multi { bodies: NonEmptyVec::from(BodyStructure::Multi { bodies: NonEmptyVec::from(BodyStructure::Single { body: Body { basic: BasicFields { parameter_list: vec![], id: NString(None), description: NString(None), content_transfer_encoding: IString::from( Literal::try_from(b"".as_ref()) .unwrap(), ), size: 0, }, specific: SpecificFields::Basic { r#type: IString::from( Literal::try_from(b"".as_ref()) .unwrap(), ), subtype: IString::from( Literal::try_from(b"".as_ref()) .unwrap(), ), }, }, extension_data: Some(SinglePartExtensionData { md5: NString::try_from("FOO").unwrap(), tail: Some(Disposition{ disposition: None, tail: Some(Language { language: vec![], tail: Some(Location{ location: NString::try_from("LOCATION").unwrap(), extensions: vec![BodyExtension::Number(1337)], }) }) }) }), }), subtype: IString::try_from("mixed").unwrap(), extension_data: None, }), subtype: IString::try_from("mixed").unwrap(), extension_data: None, }), subtype: IString::try_from("mixed").unwrap(), extension_data: None, }), subtype: IString::try_from("mixed").unwrap(), extension_data: None, }), subtype: IString::try_from("mixed").unwrap(), extension_data: None, }), subtype: IString::try_from("mixed").unwrap(), extension_data: None, }, )), }), )]); } #[test] fn test_encode_single_part_extension_data() { let tests = [( SinglePartExtensionData { md5: NString(None), tail: Some(Disposition { disposition: None, tail: Some(Language { language: vec![], tail: Some(Location { location: NString::from(Quoted::try_from("").unwrap()), extensions: vec![], }), }), }), }, b"NIL NIL NIL \"\"".as_ref(), )]; for test in tests { known_answer_test_encode(test); } } #[test] fn test_number_quirk() { assert_eq!(body_fld_octets(b"0)").unwrap().1, 0); assert_eq!(body_fld_octets(b"1)").unwrap().1, 1); #[cfg(not(feature = "quirk_rectify_numbers"))] { assert!(dbg!(body_fld_octets(b"-0)")).is_err()); assert!(body_fld_octets(b"-1)").is_err()); assert!(body_fld_octets(b"-999999)").is_err()); } #[cfg(feature = "quirk_rectify_numbers")] { assert_eq!(body_fld_octets(b"-0)").unwrap().1, 0); assert_eq!(body_fld_octets(b"-1)").unwrap().1, 0); assert_eq!(body_fld_octets(b"-999999)").unwrap().1, 0); } } } imap-codec-1.0.0/imap-codec/src/codec.rs000066400000000000000000000225471447115025300177400ustar00rootroot00000000000000pub mod decode; pub mod encode; /// Codec for greetings. #[derive(Debug, Default)] // We use `#[non_exhaustive]` to prevent users from using struct literal syntax. // // This allows to add configuration options later. For example, the // codec could transparently replace all literals with non-sync literals. #[non_exhaustive] pub struct GreetingCodec; /// Codec for commands. #[derive(Debug, Default)] #[non_exhaustive] pub struct CommandCodec; /// Codec for authenticate data lines. #[derive(Debug, Default)] #[non_exhaustive] pub struct AuthenticateDataCodec; /// Codec for responses. #[derive(Debug, Default)] #[non_exhaustive] pub struct ResponseCodec; /// Codec for idle dones. #[derive(Debug, Default)] #[non_exhaustive] pub struct IdleDoneCodec; macro_rules! impl_codec_new { ($codec:ty) => { impl $codec { /// Create codec with default configuration. pub fn new() -> Self { Self::default() } } }; } impl_codec_new!(GreetingCodec); impl_codec_new!(CommandCodec); impl_codec_new!(AuthenticateDataCodec); impl_codec_new!(ResponseCodec); impl_codec_new!(IdleDoneCodec); #[cfg(test)] mod tests { use std::num::NonZeroU32; use imap_types::{ auth::AuthenticateData, command::{Command, CommandBody}, core::{IString, Literal, LiteralMode, NString, NonEmptyVec, Tag}, fetch::MessageDataItem, mailbox::Mailbox, response::{Data, Greeting, GreetingKind, Response}, secret::Secret, }; use super::*; use crate::{ decode::{CommandDecodeError, Decoder, GreetingDecodeError, ResponseDecodeError}, testing::{ kat_inverse_authenticate_data, kat_inverse_command, kat_inverse_greeting, kat_inverse_response, }, }; #[test] fn test_kat_inverse_greeting() { kat_inverse_greeting(&[ ( b"* OK ...\r\n".as_ref(), b"".as_ref(), Greeting::new(GreetingKind::Ok, None, "...").unwrap(), ), ( b"* ByE .\r\n???", b"???", Greeting::new(GreetingKind::Bye, None, ".").unwrap(), ), ( b"* preaUth x\r\n?", b"?", Greeting::new(GreetingKind::PreAuth, None, "x").unwrap(), ), ]); } #[test] fn test_kat_inverse_command() { kat_inverse_command(&[ ( b"a nOOP\r\n".as_ref(), b"".as_ref(), Command::new("a", CommandBody::Noop).unwrap(), ), ( b"a NooP\r\n???", b"???", Command::new("a", CommandBody::Noop).unwrap(), ), ( b"a SeLECT {5}\r\ninbox\r\n", b"", Command::new( "a", CommandBody::Select { mailbox: Mailbox::Inbox, }, ) .unwrap(), ), ( b"a SElECT {5}\r\ninbox\r\nxxx", b"xxx", Command::new( "a", CommandBody::Select { mailbox: Mailbox::Inbox, }, ) .unwrap(), ), ]); } #[test] fn test_kat_inverse_response() { kat_inverse_response(&[ ( b"* SEARCH 1\r\n".as_ref(), b"".as_ref(), Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()])), ), ( b"* SEARCH 1\r\n???", b"???", Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()])), ), ( b"* 1 FETCH (RFC822 {5}\r\nhello)\r\n", b"", Response::Data(Data::Fetch { seq: NonZeroU32::new(1).unwrap(), items: NonEmptyVec::from(MessageDataItem::Rfc822(NString(Some( IString::Literal(Literal::try_from(b"hello".as_ref()).unwrap()), )))), }), ), ]); } #[test] fn test_kat_inverse_authenticate_data() { kat_inverse_authenticate_data(&[( b"VGVzdA==\r\n".as_ref(), b"".as_ref(), AuthenticateData(Secret::new(b"Test".to_vec())), )]); } #[test] fn test_greeting_incomplete_failed() { let tests = [ // Incomplete (b"*".as_ref(), Err(GreetingDecodeError::Incomplete)), (b"* ".as_ref(), Err(GreetingDecodeError::Incomplete)), (b"* O".as_ref(), Err(GreetingDecodeError::Incomplete)), (b"* OK".as_ref(), Err(GreetingDecodeError::Incomplete)), (b"* OK ".as_ref(), Err(GreetingDecodeError::Incomplete)), (b"* OK .".as_ref(), Err(GreetingDecodeError::Incomplete)), (b"* OK .\r".as_ref(), Err(GreetingDecodeError::Incomplete)), // Failed (b"**".as_ref(), Err(GreetingDecodeError::Failed)), (b"* NO x\r\n".as_ref(), Err(GreetingDecodeError::Failed)), ]; for (test, expected) in tests { let got = GreetingCodec::default().decode(test); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); assert_eq!(expected, got); #[cfg(feature = "bounded-static")] { let got = GreetingCodec::default().decode_static(test); assert_eq!(expected, got); } } } #[test] fn test_command_incomplete_failed() { let tests = [ // Incomplete (b"a".as_ref(), Err(CommandDecodeError::Incomplete)), (b"a ".as_ref(), Err(CommandDecodeError::Incomplete)), (b"a n".as_ref(), Err(CommandDecodeError::Incomplete)), (b"a no".as_ref(), Err(CommandDecodeError::Incomplete)), (b"a noo".as_ref(), Err(CommandDecodeError::Incomplete)), (b"a noop".as_ref(), Err(CommandDecodeError::Incomplete)), (b"a noop\r".as_ref(), Err(CommandDecodeError::Incomplete)), // LiteralAckRequired ( b"a select {5}\r\n".as_ref(), Err(CommandDecodeError::LiteralFound { tag: Tag::try_from("a").unwrap(), length: 5, mode: LiteralMode::Sync, }), ), ( b"a select {5+}\r\n".as_ref(), Err(CommandDecodeError::LiteralFound { tag: Tag::try_from("a").unwrap(), length: 5, mode: LiteralMode::NonSync, }), ), // Incomplete (after literal) ( b"a select {5}\r\nxxx".as_ref(), Err(CommandDecodeError::Incomplete), ), // Failed (b"* noop\r\n".as_ref(), Err(CommandDecodeError::Failed)), (b"A noop\r\n".as_ref(), Err(CommandDecodeError::Failed)), ]; for (test, expected) in tests { let got = CommandCodec::default().decode(test); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); assert_eq!(expected, got); #[cfg(feature = "bounded-static")] { let got = CommandCodec::default().decode_static(test); assert_eq!(expected, got); } } } #[test] fn test_response_incomplete_failed() { let tests = [ // Incomplete (b"".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"*".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* ".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* S".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* SE".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* SEA".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* SEAR".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* SEARC".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* SEARCH".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* SEARCH ".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* SEARCH 1".as_ref(), Err(ResponseDecodeError::Incomplete)), ( b"* SEARCH 1\r".as_ref(), Err(ResponseDecodeError::Incomplete), ), // LiteralAck treated as Incomplete ( b"* 1 FETCH (RFC822 {5}\r\n".as_ref(), Err(ResponseDecodeError::LiteralFound { length: 5 }), ), // Failed ( b"* search 1 2 3\r\n".as_ref(), Err(ResponseDecodeError::Failed), ), (b"A search\r\n".as_ref(), Err(ResponseDecodeError::Failed)), ]; for (test, expected) in tests { let got = ResponseCodec::default().decode(test); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); assert_eq!(expected, got); #[cfg(feature = "bounded-static")] { let got = ResponseCodec::default().decode_static(test); assert_eq!(expected, got); } } } } imap-codec-1.0.0/imap-codec/src/codec/000077500000000000000000000000001447115025300173605ustar00rootroot00000000000000imap-codec-1.0.0/imap-codec/src/codec/decode.rs000066400000000000000000000556311447115025300211630ustar00rootroot00000000000000//! # Decoding of messages. //! //! You can use [`Decoder`]s to parse messages. //! //! IMAP literals make separating the parsing logic from the application logic difficult. //! When a server recognizes a literal (e.g. `{42}\r\n`) in a command, it first needs to agree to receive more data by sending a so-called "command continuation request" (`+ ...`). //! Without a command continuation request, a client won't send more data, and the command parser on the server would always return `LiteralFound { length: 42, .. }`. //! This makes real-world decoding of IMAP more elaborate. //! //! Have a look at the [parse_command](https://github.com/duesee/imap-codec/blob/main/imap-codec/examples/parse_command.rs) example to see how a real-world application could decode IMAP. use std::num::{ParseIntError, TryFromIntError}; #[cfg(feature = "bounded-static")] use bounded_static::{IntoBoundedStatic, ToStatic}; use imap_types::{ auth::AuthenticateData, command::Command, core::{LiteralMode, Tag}, extensions::idle::IdleDone, response::{Greeting, Response}, }; use nom::error::{ErrorKind, FromExternalError, ParseError}; use crate::{ auth::authenticate_data, command::command, extensions::idle::idle_done, response::{greeting, response}, AuthenticateDataCodec, CommandCodec, GreetingCodec, IdleDoneCodec, ResponseCodec, }; /// An extended version of [`nom::IResult`]. pub(crate) type IMAPResult<'a, I, O> = Result<(I, O), nom::Err>>; /// An extended version of [`nom::error::Error`]. #[derive(Debug)] pub(crate) struct IMAPParseError<'a, I> { #[allow(unused)] pub input: I, pub kind: IMAPErrorKind<'a>, } /// An extended version of [`nom::error::ErrorKind`]. #[derive(Debug)] pub(crate) enum IMAPErrorKind<'a> { Literal { tag: Option>, length: u32, mode: LiteralMode, }, BadNumber, BadBase64, BadDateTime, LiteralContainsNull, RecursionLimitExceeded, Nom(ErrorKind), } impl<'a, I> ParseError for IMAPParseError<'a, I> { fn from_error_kind(input: I, kind: ErrorKind) -> Self { Self { input, kind: IMAPErrorKind::Nom(kind), } } fn append(input: I, kind: ErrorKind, _: Self) -> Self { Self { input, kind: IMAPErrorKind::Nom(kind), } } } impl<'a, I> FromExternalError for IMAPParseError<'a, I> { fn from_external_error(input: I, _: ErrorKind, _: ParseIntError) -> Self { Self { input, kind: IMAPErrorKind::BadNumber, } } } impl<'a, I> FromExternalError for IMAPParseError<'a, I> { fn from_external_error(input: I, _: ErrorKind, _: TryFromIntError) -> Self { Self { input, kind: IMAPErrorKind::BadNumber, } } } impl<'a, I> FromExternalError for IMAPParseError<'a, I> { fn from_external_error(input: I, _: ErrorKind, _: base64::DecodeError) -> Self { Self { input, kind: IMAPErrorKind::BadBase64, } } } /// Decoder. /// /// Implemented for types that know how to decode a specific IMAP message. See [implementors](trait.Decoder.html#implementors). pub trait Decoder { type Message<'a>: Sized; type Error<'a>; fn decode<'a>(&self, input: &'a [u8]) -> Result<(&'a [u8], Self::Message<'a>), Self::Error<'a>>; #[cfg(feature = "bounded-static")] #[cfg_attr(docsrs, doc(cfg(feature = "bounded-static")))] fn decode_static<'a>( &self, input: &'a [u8], ) -> Result<(&'a [u8], Self::Message<'static>), Self::Error<'static>> where Self::Message<'a>: IntoBoundedStatic>, Self::Error<'a>: IntoBoundedStatic>, { let (remaining, value) = self.decode(input).map_err(IntoBoundedStatic::into_static)?; Ok((remaining, value.into_static())) } } /// Error during greeting decoding. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[derive(Clone, Debug, Eq, PartialEq)] pub enum GreetingDecodeError { /// More data is needed. Incomplete, /// Decoding failed. Failed, } /// Error during command decoding. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[derive(Clone, Debug, Eq, PartialEq)] pub enum CommandDecodeError<'a> { /// More data is needed. Incomplete, /// More data is needed (and further action may be necessary). /// /// The decoder stopped at the beginning of literal data. Typically, a server MUST send a /// command continuation request to agree to the receival of the remaining data. This behaviour /// is different when `LITERAL+/LITERAL-` is used. /// /// # With `LITERAL+/LITERAL-` /// /// When the `mode` is sync, everything is the same as above. /// /// When the `mode` is non-sync, *and* the server advertised the LITERAL+ capability, /// it MUST NOT send a command continuation request and accept the data right away. /// /// When the `mode` is non-sync, *and* the server advertised the LITERAL- capability, /// *and* the literal length is smaller or equal than 4096, /// it MUST NOT send a command continuation request and accept the data right away. /// /// When the `mode` is non-sync, *and* the server advertised the LITERAL- capability, /// *and* the literal length is greater than 4096, /// it MUST be handled as sync. /// /// ```rust,ignore /// match mode { /// LiteralMode::Sync => /* Same as sync. */ /// LiteralMode::Sync => match advertised { /// Capability::LiteralPlus => /* Accept data right away. */ /// Capability::LiteralMinus => { /// if literal_length <= 4096 { /// /* Accept data right away. */ /// } else { /// /* Same as sync. */ /// } /// } /// } /// } /// ``` LiteralFound { /// The corresponding command (tag) to which this literal is bound. /// /// This is required to reject literals, e.g., when their size exceeds a limit. tag: Tag<'a>, /// Literal length. length: u32, /// Literal mode, i.e., sync or non-sync. mode: LiteralMode, }, /// Decoding failed. Failed, } /// Error during authenticate data line decoding. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[derive(Clone, Debug, Eq, PartialEq)] pub enum AuthenticateDataDecodeError { /// More data is needed. Incomplete, /// Decoding failed. Failed, } /// Error during response decoding. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[derive(Clone, Debug, Eq, PartialEq)] pub enum ResponseDecodeError { /// More data is needed. Incomplete, /// The decoder stopped at the beginning of literal data. /// /// The client *MUST* accept the literal and has no option to reject it. /// However, when the client ultimately does not want to handle the literal, it can do something /// similar to . /// /// It can implement a discarding mechanism, basically, consuming the whole literal but not /// saving the bytes in memory. Or, it can close the connection. LiteralFound { /// Literal length. length: u32, }, /// Decoding failed. Failed, } /// Error during idle done decoding. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[derive(Clone, Debug, Eq, PartialEq)] pub enum IdleDoneDecodeError { /// More data is needed. Incomplete, /// Decoding failed. Failed, } // ------------------------------------------------------------------------------------------------- impl Decoder for GreetingCodec { type Message<'a> = Greeting<'a>; type Error<'a> = GreetingDecodeError; fn decode<'a>( &self, input: &'a [u8], ) -> Result<(&'a [u8], Self::Message<'a>), Self::Error<'static>> { match greeting(input) { Ok((rem, grt)) => Ok((rem, grt)), Err(nom::Err::Incomplete(_)) => Err(GreetingDecodeError::Incomplete), Err(nom::Err::Failure(_)) | Err(nom::Err::Error(_)) => Err(GreetingDecodeError::Failed), } } } impl Decoder for CommandCodec { type Message<'a> = Command<'a>; type Error<'a> = CommandDecodeError<'a>; fn decode<'a>( &self, input: &'a [u8], ) -> Result<(&'a [u8], Self::Message<'a>), Self::Error<'a>> { match command(input) { Ok((rem, cmd)) => Ok((rem, cmd)), Err(nom::Err::Incomplete(_)) => Err(CommandDecodeError::Incomplete), Err(nom::Err::Failure(error)) => match error { IMAPParseError { input: _, kind: IMAPErrorKind::Literal { tag, length, mode }, } => Err(CommandDecodeError::LiteralFound { // Unwrap: We *must* receive a `tag` during command parsing. tag: tag.expect("Expected `Some(tag)` in `IMAPErrorKind::Literal`, got `None`"), length, mode, }), _ => Err(CommandDecodeError::Failed), }, Err(nom::Err::Error(_)) => Err(CommandDecodeError::Failed), } } } impl Decoder for ResponseCodec { type Message<'a> = Response<'a>; type Error<'a> = ResponseDecodeError; fn decode<'a>( &self, input: &'a [u8], ) -> Result<(&'a [u8], Self::Message<'a>), Self::Error<'static>> { match response(input) { Ok((rem, rsp)) => Ok((rem, rsp)), Err(nom::Err::Incomplete(_)) => Err(ResponseDecodeError::Incomplete), Err(nom::Err::Error(error) | nom::Err::Failure(error)) => match error { IMAPParseError { kind: IMAPErrorKind::Literal { length, .. }, .. } => Err(ResponseDecodeError::LiteralFound { length }), _ => Err(ResponseDecodeError::Failed), }, } } } impl Decoder for AuthenticateDataCodec { type Message<'a> = AuthenticateData; type Error<'a> = AuthenticateDataDecodeError; fn decode<'a>( &self, input: &'a [u8], ) -> Result<(&'a [u8], Self::Message<'a>), Self::Error<'static>> { match authenticate_data(input) { Ok((rem, rsp)) => Ok((rem, rsp)), Err(nom::Err::Incomplete(_)) => Err(AuthenticateDataDecodeError::Incomplete), Err(nom::Err::Failure(_)) | Err(nom::Err::Error(_)) => { Err(AuthenticateDataDecodeError::Failed) } } } } impl Decoder for IdleDoneCodec { type Message<'a> = IdleDone; type Error<'a> = IdleDoneDecodeError; fn decode<'a>( &self, input: &'a [u8], ) -> Result<(&'a [u8], Self::Message<'a>), Self::Error<'static>> { match idle_done(input) { Ok((rem, rsp)) => Ok((rem, rsp)), Err(nom::Err::Incomplete(_)) => Err(IdleDoneDecodeError::Incomplete), Err(nom::Err::Failure(_)) | Err(nom::Err::Error(_)) => Err(IdleDoneDecodeError::Failed), } } } #[cfg(test)] mod tests { use std::num::NonZeroU32; use imap_types::{ command::{Command, CommandBody}, core::{IString, Literal, NString, NonEmptyVec}, extensions::idle::IdleDone, fetch::MessageDataItem, mailbox::Mailbox, response::{Data, Greeting, GreetingKind, Response}, secret::Secret, }; use super::*; #[test] fn test_decode_greeting() { let tests = [ // Ok ( b"* OK ...\r\n".as_ref(), Ok(( b"".as_ref(), Greeting::new(GreetingKind::Ok, None, "...").unwrap(), )), ), ( b"* ByE .\r\n???".as_ref(), Ok(( b"???".as_ref(), Greeting::new(GreetingKind::Bye, None, ".").unwrap(), )), ), ( b"* preaUth x\r\n?".as_ref(), Ok(( b"?".as_ref(), Greeting::new(GreetingKind::PreAuth, None, "x").unwrap(), )), ), // Incomplete (b"*".as_ref(), Err(GreetingDecodeError::Incomplete)), (b"* ".as_ref(), Err(GreetingDecodeError::Incomplete)), (b"* O".as_ref(), Err(GreetingDecodeError::Incomplete)), (b"* OK".as_ref(), Err(GreetingDecodeError::Incomplete)), (b"* OK ".as_ref(), Err(GreetingDecodeError::Incomplete)), (b"* OK .".as_ref(), Err(GreetingDecodeError::Incomplete)), (b"* OK .\r".as_ref(), Err(GreetingDecodeError::Incomplete)), // Failed (b"**".as_ref(), Err(GreetingDecodeError::Failed)), (b"* NO x\r\n".as_ref(), Err(GreetingDecodeError::Failed)), ]; for (test, expected) in tests { let got = GreetingCodec::default().decode(test); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); assert_eq!(expected, got); #[cfg(feature = "bounded-static")] { let got = GreetingCodec::default().decode_static(test); assert_eq!(expected, got); } } } #[test] fn test_decode_command() { let tests = [ // Ok ( b"a noop\r\n".as_ref(), Ok((b"".as_ref(), Command::new("a", CommandBody::Noop).unwrap())), ), ( b"a noop\r\n???".as_ref(), Ok(( b"???".as_ref(), Command::new("a", CommandBody::Noop).unwrap(), )), ), ( b"a select {5}\r\ninbox\r\n".as_ref(), Ok(( b"".as_ref(), Command::new( "a", CommandBody::Select { mailbox: Mailbox::Inbox, }, ) .unwrap(), )), ), ( b"a select {5}\r\ninbox\r\nxxx".as_ref(), Ok(( b"xxx".as_ref(), Command::new( "a", CommandBody::Select { mailbox: Mailbox::Inbox, }, ) .unwrap(), )), ), // Incomplete (b"a".as_ref(), Err(CommandDecodeError::Incomplete)), (b"a ".as_ref(), Err(CommandDecodeError::Incomplete)), (b"a n".as_ref(), Err(CommandDecodeError::Incomplete)), (b"a no".as_ref(), Err(CommandDecodeError::Incomplete)), (b"a noo".as_ref(), Err(CommandDecodeError::Incomplete)), (b"a noop".as_ref(), Err(CommandDecodeError::Incomplete)), (b"a noop\r".as_ref(), Err(CommandDecodeError::Incomplete)), // LiteralAckRequired ( b"a select {5}\r\n".as_ref(), Err(CommandDecodeError::LiteralFound { tag: Tag::try_from("a").unwrap(), length: 5, mode: LiteralMode::Sync, }), ), // Incomplete (after literal) ( b"a select {5}\r\nxxx".as_ref(), Err(CommandDecodeError::Incomplete), ), // Failed (b"* noop\r\n".as_ref(), Err(CommandDecodeError::Failed)), (b"A noop\r\n".as_ref(), Err(CommandDecodeError::Failed)), ]; for (test, expected) in tests { let got = CommandCodec::default().decode(test); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); assert_eq!(expected, got); #[cfg(feature = "bounded-static")] { let got = CommandCodec::default().decode_static(test); assert_eq!(expected, got); } } } #[test] fn test_decode_authenticate_data() { let tests = [ // Ok ( b"VGVzdA==\r\n".as_ref(), Ok(( b"".as_ref(), AuthenticateData(Secret::new(b"Test".to_vec())), )), ), ( b"VGVzdA==\r\nx".as_ref(), Ok(( b"x".as_ref(), AuthenticateData(Secret::new(b"Test".to_vec())), )), ), // Incomplete (b"V".as_ref(), Err(AuthenticateDataDecodeError::Incomplete)), (b"VG".as_ref(), Err(AuthenticateDataDecodeError::Incomplete)), ( b"VGV".as_ref(), Err(AuthenticateDataDecodeError::Incomplete), ), ( b"VGVz".as_ref(), Err(AuthenticateDataDecodeError::Incomplete), ), ( b"VGVzd".as_ref(), Err(AuthenticateDataDecodeError::Incomplete), ), ( b"VGVzdA".as_ref(), Err(AuthenticateDataDecodeError::Incomplete), ), ( b"VGVzdA=".as_ref(), Err(AuthenticateDataDecodeError::Incomplete), ), ( b"VGVzdA==".as_ref(), Err(AuthenticateDataDecodeError::Incomplete), ), ( b"VGVzdA==\r".as_ref(), Err(AuthenticateDataDecodeError::Incomplete), ), ( b"VGVzdA==\r\n".as_ref(), Ok(( b"".as_ref(), AuthenticateData(Secret::new(b"Test".to_vec())), )), ), // Failed ( b"VGVzdA== \r\n".as_ref(), Err(AuthenticateDataDecodeError::Failed), ), ( b" VGVzdA== \r\n".as_ref(), Err(AuthenticateDataDecodeError::Failed), ), ( b" V GVzdA== \r\n".as_ref(), Err(AuthenticateDataDecodeError::Failed), ), ( b" V GVzdA= \r\n".as_ref(), Err(AuthenticateDataDecodeError::Failed), ), ]; for (test, expected) in tests { let got = AuthenticateDataCodec::default().decode(test); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); assert_eq!(expected, got); #[cfg(feature = "bounded-static")] { let got = AuthenticateDataCodec::default().decode_static(test); assert_eq!(expected, got); } } } #[test] fn test_decode_idle_done() { let tests = [ // Ok (b"done\r\n".as_ref(), Ok((b"".as_ref(), IdleDone))), (b"done\r\n?".as_ref(), Ok((b"?".as_ref(), IdleDone))), // Incomplete (b"d".as_ref(), Err(IdleDoneDecodeError::Incomplete)), (b"do".as_ref(), Err(IdleDoneDecodeError::Incomplete)), (b"don".as_ref(), Err(IdleDoneDecodeError::Incomplete)), (b"done".as_ref(), Err(IdleDoneDecodeError::Incomplete)), (b"done\r".as_ref(), Err(IdleDoneDecodeError::Incomplete)), // Failed (b"donee\r\n".as_ref(), Err(IdleDoneDecodeError::Failed)), (b" done\r\n".as_ref(), Err(IdleDoneDecodeError::Failed)), (b"done \r\n".as_ref(), Err(IdleDoneDecodeError::Failed)), (b" done \r\n".as_ref(), Err(IdleDoneDecodeError::Failed)), ]; for (test, expected) in tests { let got = IdleDoneCodec::default().decode(test); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); assert_eq!(expected, got); #[cfg(feature = "bounded-static")] { let got = IdleDoneCodec::default().decode_static(test); assert_eq!(expected, got); } } } #[test] fn test_decode_response() { let tests = [ // Incomplete (b"".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"*".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* ".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* S".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* SE".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* SEA".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* SEAR".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* SEARC".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* SEARCH".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* SEARCH ".as_ref(), Err(ResponseDecodeError::Incomplete)), (b"* SEARCH 1".as_ref(), Err(ResponseDecodeError::Incomplete)), ( b"* SEARCH 1\r".as_ref(), Err(ResponseDecodeError::Incomplete), ), // Ok ( b"* SEARCH 1\r\n".as_ref(), Ok(( b"".as_ref(), Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()])), )), ), ( b"* SEARCH 1\r\n???".as_ref(), Ok(( b"???".as_ref(), Response::Data(Data::Search(vec![NonZeroU32::new(1).unwrap()])), )), ), ( b"* 1 FETCH (RFC822 {5}\r\nhello)\r\n".as_ref(), Ok(( b"".as_ref(), Response::Data(Data::Fetch { seq: NonZeroU32::new(1).unwrap(), items: NonEmptyVec::from(MessageDataItem::Rfc822(NString(Some( IString::Literal(Literal::try_from(b"hello".as_ref()).unwrap()), )))), }), )), ), ( b"* 1 FETCH (RFC822 {5}\r\n".as_ref(), Err(ResponseDecodeError::LiteralFound { length: 5 }), ), // Failed ( b"* search 1 2 3\r\n".as_ref(), Err(ResponseDecodeError::Failed), ), (b"A search\r\n".as_ref(), Err(ResponseDecodeError::Failed)), ]; for (test, expected) in tests { let got = ResponseCodec::default().decode(test); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); assert_eq!(expected, got); #[cfg(feature = "bounded-static")] { let got = ResponseCodec::default().decode_static(test); assert_eq!(expected, got); } } } } imap-codec-1.0.0/imap-codec/src/codec/encode.rs000066400000000000000000001670711447115025300211770ustar00rootroot00000000000000//! # Encoding of messages. //! //! To facilitates handling of literals, [Encoder::encode] returns an instance of [`Encoded`]. //! The idea is that the encoder not only "dumps" the final serialization of a message but can be iterated over. //! //! # Example //! //! ```rust //! use imap_codec::{ //! encode::{Encoder, Fragment}, //! imap_types::{ //! command::{Command, CommandBody}, //! core::LiteralMode, //! }, //! CommandCodec, //! }; //! //! let command = Command::new("A1", CommandBody::login("Alice", "Pa²²W0rD").unwrap()).unwrap(); //! //! for fragment in CommandCodec::default().encode(&command) { //! match fragment { //! Fragment::Line { data } => { //! // A line that is ready to be send. //! println!("C: {}", String::from_utf8(data).unwrap()); //! } //! Fragment::Literal { data, mode } => match mode { //! LiteralMode::Sync => { //! // Wait for a continuation request. //! println!("S: + ...") //! } //! LiteralMode::NonSync => { //! // We don't need to wait for a continuation request //! // as the server will also not send it. //! } //! }, //! } //! } //! ``` //! //! Output of example: //! //! ```imap //! C: A1 LOGIN alice {10} //! S: + ... //! C: Pa²²W0rD //! ``` use std::{borrow::Borrow, io::Write, num::NonZeroU32}; use base64::{engine::general_purpose::STANDARD as base64, Engine}; use chrono::{DateTime as ChronoDateTime, FixedOffset}; use imap_types::{ auth::{AuthMechanism, AuthenticateData}, body::{ BasicFields, Body, BodyExtension, BodyStructure, Disposition, Language, Location, MultiPartExtensionData, SinglePartExtensionData, SpecificFields, }, command::{Command, CommandBody}, core::{ AString, Atom, AtomExt, Charset, IString, Literal, LiteralMode, NString, Quoted, QuotedChar, Tag, Text, }, datetime::{DateTime, NaiveDate}, envelope::{Address, Envelope}, extensions::idle::IdleDone, fetch::{ Macro, MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName, Part, Section, }, flag::{Flag, FlagFetch, FlagNameAttribute, FlagPerm, StoreResponse, StoreType}, mailbox::{ListCharString, ListMailbox, Mailbox, MailboxOther}, response::{ Capability, Code, CodeOther, CommandContinuationRequest, Data, Greeting, GreetingKind, Response, Status, }, search::SearchKey, sequence::{SeqOrUid, Sequence, SequenceSet}, status::{StatusDataItem, StatusDataItemName}, utils::escape_quoted, }; use utils::{join_serializable, List1AttributeValueOrNil, List1OrNil}; use crate::{AuthenticateDataCodec, CommandCodec, GreetingCodec, IdleDoneCodec, ResponseCodec}; /// Encoder. /// /// Implemented for types that know how to encode a specific IMAP message. See [implementors](trait.Encoder.html#implementors). pub trait Encoder { type Message<'a>; /// Encode this message. /// /// This will return an [`Encoded`] message. fn encode(&self, message: &Self::Message<'_>) -> Encoded; } /// An encoded message. /// /// This struct facilitates the implementation of IMAP client- and server implementations by /// yielding the encoding of a message through [`Fragment`]s. This is required, because the usage of /// literals (and some other types) may change the IMAP message flow. Thus, in many cases, it is an /// error to just "dump" a message and send it over the network. /// /// # Example /// /// ```rust /// use imap_codec::{ /// encode::{Encoder, Fragment}, /// imap_types::command::{Command, CommandBody}, /// CommandCodec, /// }; /// /// let cmd = Command::new("A", CommandBody::login("alice", "pass").unwrap()).unwrap(); /// /// for fragment in CommandCodec::default().encode(&cmd) { /// match fragment { /// Fragment::Line { data } => {} /// Fragment::Literal { data, mode } => {} /// } /// } /// ``` #[derive(Clone, Debug)] pub struct Encoded { items: Vec, } impl Encoded { /// Dump the (remaining) encoded data without being guided by [`Fragment`]s. pub fn dump(self) -> Vec { let mut out = Vec::new(); for fragment in self.items { match fragment { Fragment::Line { mut data } => out.append(&mut data), Fragment::Literal { mut data, .. } => out.append(&mut data), } } out } } impl Iterator for Encoded { type Item = Fragment; fn next(&mut self) -> Option { if !self.items.is_empty() { Some(self.items.remove(0)) } else { None } } } /// The intended action of a client or server. #[derive(Clone, Debug, Eq, PartialEq)] pub enum Fragment { /// A line that is ready to be send. Line { data: Vec }, /// A literal that may require an action before it should be send. Literal { data: Vec, mode: LiteralMode }, } //-------------------------------------------------------------------------------------------------- #[derive(Clone, Debug, Default, Eq, PartialEq)] pub(crate) struct EncodeContext { accumulator: Vec, items: Vec, } impl EncodeContext { pub fn new() -> Self { Self::default() } pub fn push_line(&mut self) { self.items.push(Fragment::Line { data: std::mem::take(&mut self.accumulator), }) } pub fn push_literal(&mut self, mode: LiteralMode) { self.items.push(Fragment::Literal { data: std::mem::take(&mut self.accumulator), mode, }) } pub fn into_items(self) -> Vec { let Self { accumulator, mut items, } = self; if !accumulator.is_empty() { items.push(Fragment::Line { data: accumulator }); } items } #[cfg(test)] pub(crate) fn dump(self) -> Vec { let mut out = Vec::new(); for item in self.into_items() { match item { Fragment::Line { data } | Fragment::Literal { data, .. } => { out.extend_from_slice(&data) } } } out } } impl Write for EncodeContext { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.accumulator.extend_from_slice(buf); Ok(buf.len()) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } macro_rules! impl_encoder_for_codec { ($codec:ty, $message:ty) => { impl Encoder for $codec { type Message<'a> = $message; fn encode(&self, message: &Self::Message<'_>) -> Encoded { let mut encode_context = EncodeContext::new(); EncodeIntoContext::encode_ctx(message.borrow(), &mut encode_context).unwrap(); Encoded { items: encode_context.into_items(), } } } }; } impl_encoder_for_codec!(GreetingCodec, Greeting<'a>); impl_encoder_for_codec!(CommandCodec, Command<'a>); impl_encoder_for_codec!(AuthenticateDataCodec, AuthenticateData); impl_encoder_for_codec!(ResponseCodec, Response<'a>); impl_encoder_for_codec!(IdleDoneCodec, IdleDone); // ------------------------------------------------------------------------------------------------- pub(crate) trait EncodeIntoContext { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()>; } // ----- Primitive --------------------------------------------------------------------------------- impl EncodeIntoContext for u32 { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { ctx.write_all(self.to_string().as_bytes()) } } impl EncodeIntoContext for u64 { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { ctx.write_all(self.to_string().as_bytes()) } } // ----- Command ----------------------------------------------------------------------------------- impl<'a> EncodeIntoContext for Command<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { self.tag.encode_ctx(ctx)?; ctx.write_all(b" ")?; self.body.encode_ctx(ctx)?; ctx.write_all(b"\r\n") } } impl<'a> EncodeIntoContext for Tag<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { ctx.write_all(self.inner().as_bytes()) } } impl<'a> EncodeIntoContext for CommandBody<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { CommandBody::Capability => ctx.write_all(b"CAPABILITY"), CommandBody::Noop => ctx.write_all(b"NOOP"), CommandBody::Logout => ctx.write_all(b"LOGOUT"), #[cfg(feature = "starttls")] CommandBody::StartTLS => ctx.write_all(b"STARTTLS"), CommandBody::Authenticate { mechanism, initial_response, } => { ctx.write_all(b"AUTHENTICATE")?; ctx.write_all(b" ")?; mechanism.encode_ctx(ctx)?; if let Some(ir) = initial_response { ctx.write_all(b" ")?; // RFC 4959 (https://datatracker.ietf.org/doc/html/rfc4959#section-3) // "To send a zero-length initial response, the client MUST send a single pad character ("="). // This indicates that the response is present, but is a zero-length string." if ir.declassify().is_empty() { ctx.write_all(b"=")?; } else { ctx.write_all(base64.encode(ir.declassify()).as_bytes())?; }; }; Ok(()) } CommandBody::Login { username, password } => { ctx.write_all(b"LOGIN")?; ctx.write_all(b" ")?; username.encode_ctx(ctx)?; ctx.write_all(b" ")?; password.declassify().encode_ctx(ctx) } CommandBody::Select { mailbox } => { ctx.write_all(b"SELECT")?; ctx.write_all(b" ")?; mailbox.encode_ctx(ctx) } CommandBody::Unselect => ctx.write_all(b"UNSELECT"), CommandBody::Examine { mailbox } => { ctx.write_all(b"EXAMINE")?; ctx.write_all(b" ")?; mailbox.encode_ctx(ctx) } CommandBody::Create { mailbox } => { ctx.write_all(b"CREATE")?; ctx.write_all(b" ")?; mailbox.encode_ctx(ctx) } CommandBody::Delete { mailbox } => { ctx.write_all(b"DELETE")?; ctx.write_all(b" ")?; mailbox.encode_ctx(ctx) } CommandBody::Rename { from: mailbox, to: new_mailbox, } => { ctx.write_all(b"RENAME")?; ctx.write_all(b" ")?; mailbox.encode_ctx(ctx)?; ctx.write_all(b" ")?; new_mailbox.encode_ctx(ctx) } CommandBody::Subscribe { mailbox } => { ctx.write_all(b"SUBSCRIBE")?; ctx.write_all(b" ")?; mailbox.encode_ctx(ctx) } CommandBody::Unsubscribe { mailbox } => { ctx.write_all(b"UNSUBSCRIBE")?; ctx.write_all(b" ")?; mailbox.encode_ctx(ctx) } CommandBody::List { reference, mailbox_wildcard, } => { ctx.write_all(b"LIST")?; ctx.write_all(b" ")?; reference.encode_ctx(ctx)?; ctx.write_all(b" ")?; mailbox_wildcard.encode_ctx(ctx) } CommandBody::Lsub { reference, mailbox_wildcard, } => { ctx.write_all(b"LSUB")?; ctx.write_all(b" ")?; reference.encode_ctx(ctx)?; ctx.write_all(b" ")?; mailbox_wildcard.encode_ctx(ctx) } CommandBody::Status { mailbox, item_names, } => { ctx.write_all(b"STATUS")?; ctx.write_all(b" ")?; mailbox.encode_ctx(ctx)?; ctx.write_all(b" ")?; ctx.write_all(b"(")?; join_serializable(item_names, b" ", ctx)?; ctx.write_all(b")") } CommandBody::Append { mailbox, flags, date, message, } => { ctx.write_all(b"APPEND")?; ctx.write_all(b" ")?; mailbox.encode_ctx(ctx)?; if !flags.is_empty() { ctx.write_all(b" ")?; ctx.write_all(b"(")?; join_serializable(flags, b" ", ctx)?; ctx.write_all(b")")?; } if let Some(date) = date { ctx.write_all(b" ")?; date.encode_ctx(ctx)?; } ctx.write_all(b" ")?; message.encode_ctx(ctx) } CommandBody::Check => ctx.write_all(b"CHECK"), CommandBody::Close => ctx.write_all(b"CLOSE"), CommandBody::Expunge => ctx.write_all(b"EXPUNGE"), CommandBody::Search { charset, criteria, uid, } => { if *uid { ctx.write_all(b"UID SEARCH")?; } else { ctx.write_all(b"SEARCH")?; } if let Some(charset) = charset { ctx.write_all(b" CHARSET ")?; charset.encode_ctx(ctx)?; } ctx.write_all(b" ")?; criteria.encode_ctx(ctx) } CommandBody::Fetch { sequence_set, macro_or_item_names, uid, } => { if *uid { ctx.write_all(b"UID FETCH ")?; } else { ctx.write_all(b"FETCH ")?; } sequence_set.encode_ctx(ctx)?; ctx.write_all(b" ")?; macro_or_item_names.encode_ctx(ctx) } CommandBody::Store { sequence_set, kind, response, flags, uid, } => { if *uid { ctx.write_all(b"UID STORE ")?; } else { ctx.write_all(b"STORE ")?; } sequence_set.encode_ctx(ctx)?; ctx.write_all(b" ")?; match kind { StoreType::Add => ctx.write_all(b"+")?, StoreType::Remove => ctx.write_all(b"-")?, StoreType::Replace => {} } ctx.write_all(b"FLAGS")?; match response { StoreResponse::Answer => {} StoreResponse::Silent => ctx.write_all(b".SILENT")?, } ctx.write_all(b" (")?; join_serializable(flags, b" ", ctx)?; ctx.write_all(b")") } CommandBody::Copy { sequence_set, mailbox, uid, } => { if *uid { ctx.write_all(b"UID COPY ")?; } else { ctx.write_all(b"COPY ")?; } sequence_set.encode_ctx(ctx)?; ctx.write_all(b" ")?; mailbox.encode_ctx(ctx) } CommandBody::Idle => ctx.write_all(b"IDLE"), CommandBody::Enable { capabilities } => { ctx.write_all(b"ENABLE ")?; join_serializable(capabilities.as_ref(), b" ", ctx) } CommandBody::Compress { algorithm } => { ctx.write_all(b"COMPRESS ")?; algorithm.encode_ctx(ctx) } CommandBody::GetQuota { root } => { ctx.write_all(b"GETQUOTA ")?; root.encode_ctx(ctx) } CommandBody::GetQuotaRoot { mailbox } => { ctx.write_all(b"GETQUOTAROOT ")?; mailbox.encode_ctx(ctx) } CommandBody::SetQuota { root, quotas } => { ctx.write_all(b"SETQUOTA ")?; root.encode_ctx(ctx)?; ctx.write_all(b" (")?; join_serializable(quotas.as_ref(), b" ", ctx)?; ctx.write_all(b")") } CommandBody::Move { sequence_set, mailbox, uid, } => { if *uid { ctx.write_all(b"UID MOVE ")?; } else { ctx.write_all(b"MOVE ")?; } sequence_set.encode_ctx(ctx)?; ctx.write_all(b" ")?; mailbox.encode_ctx(ctx) } } } } impl<'a> EncodeIntoContext for AuthMechanism<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { write!(ctx, "{}", self) } } impl EncodeIntoContext for AuthenticateData { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { let encoded = base64.encode(self.0.declassify()); ctx.write_all(encoded.as_bytes())?; ctx.write_all(b"\r\n") } } impl<'a> EncodeIntoContext for AString<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { AString::Atom(atom) => atom.encode_ctx(ctx), AString::String(imap_str) => imap_str.encode_ctx(ctx), } } } impl<'a> EncodeIntoContext for Atom<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { ctx.write_all(self.inner().as_bytes()) } } impl<'a> EncodeIntoContext for AtomExt<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { ctx.write_all(self.inner().as_bytes()) } } impl<'a> EncodeIntoContext for IString<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Self::Literal(val) => val.encode_ctx(ctx), Self::Quoted(val) => val.encode_ctx(ctx), } } } impl<'a> EncodeIntoContext for Literal<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self.mode() { LiteralMode::Sync => write!(ctx, "{{{}}}\r\n", self.as_ref().len())?, LiteralMode::NonSync => write!(ctx, "{{{}+}}\r\n", self.as_ref().len())?, } ctx.push_line(); ctx.write_all(self.as_ref())?; ctx.push_literal(self.mode()); Ok(()) } } impl<'a> EncodeIntoContext for Quoted<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { write!(ctx, "\"{}\"", escape_quoted(self.inner())) } } impl<'a> EncodeIntoContext for Mailbox<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Mailbox::Inbox => ctx.write_all(b"INBOX"), Mailbox::Other(other) => other.encode_ctx(ctx), } } } impl<'a> EncodeIntoContext for MailboxOther<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { self.inner().encode_ctx(ctx) } } impl<'a> EncodeIntoContext for ListMailbox<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { ListMailbox::Token(lcs) => lcs.encode_ctx(ctx), ListMailbox::String(istr) => istr.encode_ctx(ctx), } } } impl<'a> EncodeIntoContext for ListCharString<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { ctx.write_all(self.as_ref()) } } impl EncodeIntoContext for StatusDataItemName { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Self::Messages => ctx.write_all(b"MESSAGES"), Self::Recent => ctx.write_all(b"RECENT"), Self::UidNext => ctx.write_all(b"UIDNEXT"), Self::UidValidity => ctx.write_all(b"UIDVALIDITY"), Self::Unseen => ctx.write_all(b"UNSEEN"), Self::Deleted => ctx.write_all(b"DELETED"), Self::DeletedStorage => ctx.write_all(b"DELETED-STORAGE"), #[cfg(feature = "ext_condstore_qresync")] Self::HighestModSeq => ctx.write_all(b"HIGHESTMODSEQ"), } } } impl<'a> EncodeIntoContext for Flag<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { write!(ctx, "{}", self) } } impl<'a> EncodeIntoContext for FlagFetch<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Self::Flag(flag) => flag.encode_ctx(ctx), Self::Recent => ctx.write_all(b"\\Recent"), } } } impl<'a> EncodeIntoContext for FlagPerm<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Self::Flag(flag) => flag.encode_ctx(ctx), Self::Asterisk => ctx.write_all(b"\\*"), } } } impl EncodeIntoContext for DateTime { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { self.as_ref().encode_ctx(ctx) } } impl<'a> EncodeIntoContext for Charset<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Charset::Atom(atom) => atom.encode_ctx(ctx), Charset::Quoted(quoted) => quoted.encode_ctx(ctx), } } } impl<'a> EncodeIntoContext for SearchKey<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { SearchKey::All => ctx.write_all(b"ALL"), SearchKey::Answered => ctx.write_all(b"ANSWERED"), SearchKey::Bcc(astring) => { ctx.write_all(b"BCC ")?; astring.encode_ctx(ctx) } SearchKey::Before(date) => { ctx.write_all(b"BEFORE ")?; date.encode_ctx(ctx) } SearchKey::Body(astring) => { ctx.write_all(b"BODY ")?; astring.encode_ctx(ctx) } SearchKey::Cc(astring) => { ctx.write_all(b"CC ")?; astring.encode_ctx(ctx) } SearchKey::Deleted => ctx.write_all(b"DELETED"), SearchKey::Flagged => ctx.write_all(b"FLAGGED"), SearchKey::From(astring) => { ctx.write_all(b"FROM ")?; astring.encode_ctx(ctx) } SearchKey::Keyword(flag_keyword) => { ctx.write_all(b"KEYWORD ")?; flag_keyword.encode_ctx(ctx) } SearchKey::New => ctx.write_all(b"NEW"), SearchKey::Old => ctx.write_all(b"OLD"), SearchKey::On(date) => { ctx.write_all(b"ON ")?; date.encode_ctx(ctx) } SearchKey::Recent => ctx.write_all(b"RECENT"), SearchKey::Seen => ctx.write_all(b"SEEN"), SearchKey::Since(date) => { ctx.write_all(b"SINCE ")?; date.encode_ctx(ctx) } SearchKey::Subject(astring) => { ctx.write_all(b"SUBJECT ")?; astring.encode_ctx(ctx) } SearchKey::Text(astring) => { ctx.write_all(b"TEXT ")?; astring.encode_ctx(ctx) } SearchKey::To(astring) => { ctx.write_all(b"TO ")?; astring.encode_ctx(ctx) } SearchKey::Unanswered => ctx.write_all(b"UNANSWERED"), SearchKey::Undeleted => ctx.write_all(b"UNDELETED"), SearchKey::Unflagged => ctx.write_all(b"UNFLAGGED"), SearchKey::Unkeyword(flag_keyword) => { ctx.write_all(b"UNKEYWORD ")?; flag_keyword.encode_ctx(ctx) } SearchKey::Unseen => ctx.write_all(b"UNSEEN"), SearchKey::Draft => ctx.write_all(b"DRAFT"), SearchKey::Header(header_fld_name, astring) => { ctx.write_all(b"HEADER ")?; header_fld_name.encode_ctx(ctx)?; ctx.write_all(b" ")?; astring.encode_ctx(ctx) } SearchKey::Larger(number) => write!(ctx, "LARGER {number}"), SearchKey::Not(search_key) => { ctx.write_all(b"NOT ")?; search_key.encode_ctx(ctx) } SearchKey::Or(search_key_a, search_key_b) => { ctx.write_all(b"OR ")?; search_key_a.encode_ctx(ctx)?; ctx.write_all(b" ")?; search_key_b.encode_ctx(ctx) } SearchKey::SentBefore(date) => { ctx.write_all(b"SENTBEFORE ")?; date.encode_ctx(ctx) } SearchKey::SentOn(date) => { ctx.write_all(b"SENTON ")?; date.encode_ctx(ctx) } SearchKey::SentSince(date) => { ctx.write_all(b"SENTSINCE ")?; date.encode_ctx(ctx) } SearchKey::Smaller(number) => write!(ctx, "SMALLER {number}"), SearchKey::Uid(sequence_set) => { ctx.write_all(b"UID ")?; sequence_set.encode_ctx(ctx) } SearchKey::Undraft => ctx.write_all(b"UNDRAFT"), SearchKey::SequenceSet(sequence_set) => sequence_set.encode_ctx(ctx), SearchKey::And(search_keys) => { ctx.write_all(b"(")?; join_serializable(search_keys.as_ref(), b" ", ctx)?; ctx.write_all(b")") } } } } impl EncodeIntoContext for SequenceSet { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { join_serializable(self.0.as_ref(), b",", ctx) } } impl EncodeIntoContext for Sequence { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Sequence::Single(seq_no) => seq_no.encode_ctx(ctx), Sequence::Range(from, to) => { from.encode_ctx(ctx)?; ctx.write_all(b":")?; to.encode_ctx(ctx) } } } } impl EncodeIntoContext for SeqOrUid { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { SeqOrUid::Value(number) => write!(ctx, "{number}"), SeqOrUid::Asterisk => ctx.write_all(b"*"), } } } impl EncodeIntoContext for NaiveDate { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { write!(ctx, "\"{}\"", self.as_ref().format("%d-%b-%Y")) } } impl<'a> EncodeIntoContext for MacroOrMessageDataItemNames<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Self::Macro(m) => m.encode_ctx(ctx), Self::MessageDataItemNames(item_names) => { if item_names.len() == 1 { item_names[0].encode_ctx(ctx) } else { ctx.write_all(b"(")?; join_serializable(item_names.as_slice(), b" ", ctx)?; ctx.write_all(b")") } } } } } impl EncodeIntoContext for Macro { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { write!(ctx, "{}", self) } } impl<'a> EncodeIntoContext for MessageDataItemName<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Self::Body => ctx.write_all(b"BODY"), Self::BodyExt { section, partial, peek, } => { if *peek { ctx.write_all(b"BODY.PEEK[")?; } else { ctx.write_all(b"BODY[")?; } if let Some(section) = section { section.encode_ctx(ctx)?; } ctx.write_all(b"]")?; if let Some((a, b)) = partial { write!(ctx, "<{a}.{b}>")?; } Ok(()) } Self::BodyStructure => ctx.write_all(b"BODYSTRUCTURE"), Self::Envelope => ctx.write_all(b"ENVELOPE"), Self::Flags => ctx.write_all(b"FLAGS"), Self::InternalDate => ctx.write_all(b"INTERNALDATE"), Self::Rfc822 => ctx.write_all(b"RFC822"), Self::Rfc822Header => ctx.write_all(b"RFC822.HEADER"), Self::Rfc822Size => ctx.write_all(b"RFC822.SIZE"), Self::Rfc822Text => ctx.write_all(b"RFC822.TEXT"), Self::Uid => ctx.write_all(b"UID"), } } } impl<'a> EncodeIntoContext for Section<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Section::Part(part) => part.encode_ctx(ctx), Section::Header(maybe_part) => match maybe_part { Some(part) => { part.encode_ctx(ctx)?; ctx.write_all(b".HEADER") } None => ctx.write_all(b"HEADER"), }, Section::HeaderFields(maybe_part, header_list) => { match maybe_part { Some(part) => { part.encode_ctx(ctx)?; ctx.write_all(b".HEADER.FIELDS (")?; } None => ctx.write_all(b"HEADER.FIELDS (")?, }; join_serializable(header_list.as_ref(), b" ", ctx)?; ctx.write_all(b")") } Section::HeaderFieldsNot(maybe_part, header_list) => { match maybe_part { Some(part) => { part.encode_ctx(ctx)?; ctx.write_all(b".HEADER.FIELDS.NOT (")?; } None => ctx.write_all(b"HEADER.FIELDS.NOT (")?, }; join_serializable(header_list.as_ref(), b" ", ctx)?; ctx.write_all(b")") } Section::Text(maybe_part) => match maybe_part { Some(part) => { part.encode_ctx(ctx)?; ctx.write_all(b".TEXT") } None => ctx.write_all(b"TEXT"), }, Section::Mime(part) => { part.encode_ctx(ctx)?; ctx.write_all(b".MIME") } } } } impl EncodeIntoContext for Part { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { join_serializable(self.0.as_ref(), b".", ctx) } } impl EncodeIntoContext for NonZeroU32 { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { write!(ctx, "{self}") } } impl<'a> EncodeIntoContext for Capability<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { write!(ctx, "{}", self) } } // ----- Responses --------------------------------------------------------------------------------- impl<'a> EncodeIntoContext for Response<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Response::Status(status) => status.encode_ctx(ctx), Response::Data(data) => data.encode_ctx(ctx), Response::CommandContinuationRequest(continue_request) => { continue_request.encode_ctx(ctx) } } } } impl<'a> EncodeIntoContext for Greeting<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { ctx.write_all(b"* ")?; self.kind.encode_ctx(ctx)?; ctx.write_all(b" ")?; if let Some(ref code) = self.code { ctx.write_all(b"[")?; code.encode_ctx(ctx)?; ctx.write_all(b"] ")?; } self.text.encode_ctx(ctx)?; ctx.write_all(b"\r\n") } } impl EncodeIntoContext for GreetingKind { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { GreetingKind::Ok => ctx.write_all(b"OK"), GreetingKind::PreAuth => ctx.write_all(b"PREAUTH"), GreetingKind::Bye => ctx.write_all(b"BYE"), } } } impl<'a> EncodeIntoContext for Status<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { fn format_status( tag: &Option, status: &str, code: &Option, comment: &Text, ctx: &mut EncodeContext, ) -> std::io::Result<()> { match tag { Some(tag) => tag.encode_ctx(ctx)?, None => ctx.write_all(b"*")?, } ctx.write_all(b" ")?; ctx.write_all(status.as_bytes())?; ctx.write_all(b" ")?; if let Some(code) = code { ctx.write_all(b"[")?; code.encode_ctx(ctx)?; ctx.write_all(b"] ")?; } comment.encode_ctx(ctx)?; ctx.write_all(b"\r\n") } match self { Status::Ok { tag, code, text } => format_status(tag, "OK", code, text, ctx), Status::No { tag, code, text } => format_status(tag, "NO", code, text, ctx), Status::Bad { tag, code, text } => format_status(tag, "BAD", code, text, ctx), Status::Bye { code, text } => format_status(&None, "BYE", code, text, ctx), } } } impl<'a> EncodeIntoContext for Code<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Code::Alert => ctx.write_all(b"ALERT"), Code::BadCharset { allowed } => { if allowed.is_empty() { ctx.write_all(b"BADCHARSET") } else { ctx.write_all(b"BADCHARSET (")?; join_serializable(allowed, b" ", ctx)?; ctx.write_all(b")") } } Code::Capability(caps) => { ctx.write_all(b"CAPABILITY ")?; join_serializable(caps.as_ref(), b" ", ctx) } Code::Parse => ctx.write_all(b"PARSE"), Code::PermanentFlags(flags) => { ctx.write_all(b"PERMANENTFLAGS (")?; join_serializable(flags, b" ", ctx)?; ctx.write_all(b")") } Code::ReadOnly => ctx.write_all(b"READ-ONLY"), Code::ReadWrite => ctx.write_all(b"READ-WRITE"), Code::TryCreate => ctx.write_all(b"TRYCREATE"), Code::UidNext(next) => { ctx.write_all(b"UIDNEXT ")?; next.encode_ctx(ctx) } Code::UidValidity(validity) => { ctx.write_all(b"UIDVALIDITY ")?; validity.encode_ctx(ctx) } Code::Unseen(seq) => { ctx.write_all(b"UNSEEN ")?; seq.encode_ctx(ctx) } // RFC 2221 #[cfg(any(feature = "ext_login_referrals", feature = "ext_mailbox_referrals"))] Code::Referral(url) => { ctx.write_all(b"REFERRAL ")?; ctx.write_all(url.as_bytes()) } Code::CompressionActive => ctx.write_all(b"COMPRESSIONACTIVE"), Code::OverQuota => ctx.write_all(b"OVERQUOTA"), Code::TooBig => ctx.write_all(b"TOOBIG"), Code::Other(unknown) => unknown.encode_ctx(ctx), } } } impl<'a> EncodeIntoContext for CodeOther<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { ctx.write_all(self.inner()) } } impl<'a> EncodeIntoContext for Text<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { ctx.write_all(self.inner().as_bytes()) } } impl<'a> EncodeIntoContext for Data<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Data::Capability(caps) => { ctx.write_all(b"* CAPABILITY ")?; join_serializable(caps.as_ref(), b" ", ctx)?; } Data::List { items, delimiter, mailbox, } => { ctx.write_all(b"* LIST (")?; join_serializable(items, b" ", ctx)?; ctx.write_all(b") ")?; if let Some(delimiter) = delimiter { ctx.write_all(b"\"")?; delimiter.encode_ctx(ctx)?; ctx.write_all(b"\"")?; } else { ctx.write_all(b"NIL")?; } ctx.write_all(b" ")?; mailbox.encode_ctx(ctx)?; } Data::Lsub { items, delimiter, mailbox, } => { ctx.write_all(b"* LSUB (")?; join_serializable(items, b" ", ctx)?; ctx.write_all(b") ")?; if let Some(delimiter) = delimiter { ctx.write_all(b"\"")?; delimiter.encode_ctx(ctx)?; ctx.write_all(b"\"")?; } else { ctx.write_all(b"NIL")?; } ctx.write_all(b" ")?; mailbox.encode_ctx(ctx)?; } Data::Status { mailbox, items } => { ctx.write_all(b"* STATUS ")?; mailbox.encode_ctx(ctx)?; ctx.write_all(b" (")?; join_serializable(items, b" ", ctx)?; ctx.write_all(b")")?; } Data::Search(seqs) => { if seqs.is_empty() { ctx.write_all(b"* SEARCH")?; } else { ctx.write_all(b"* SEARCH ")?; join_serializable(seqs, b" ", ctx)?; } } Data::Flags(flags) => { ctx.write_all(b"* FLAGS (")?; join_serializable(flags, b" ", ctx)?; ctx.write_all(b")")?; } Data::Exists(count) => write!(ctx, "* {count} EXISTS")?, Data::Recent(count) => write!(ctx, "* {count} RECENT")?, Data::Expunge(msg) => write!(ctx, "* {msg} EXPUNGE")?, Data::Fetch { seq, items } => { write!(ctx, "* {seq} FETCH (")?; join_serializable(items.as_ref(), b" ", ctx)?; ctx.write_all(b")")?; } Data::Enabled { capabilities } => { write!(ctx, "* ENABLED")?; for cap in capabilities { ctx.write_all(b" ")?; cap.encode_ctx(ctx)?; } } Data::Quota { root, quotas } => { ctx.write_all(b"* QUOTA ")?; root.encode_ctx(ctx)?; ctx.write_all(b" (")?; join_serializable(quotas.as_ref(), b" ", ctx)?; ctx.write_all(b")")?; } Data::QuotaRoot { mailbox, roots } => { ctx.write_all(b"* QUOTAROOT ")?; mailbox.encode_ctx(ctx)?; for root in roots { ctx.write_all(b" ")?; root.encode_ctx(ctx)?; } } } ctx.write_all(b"\r\n") } } impl<'a> EncodeIntoContext for FlagNameAttribute<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { write!(ctx, "{}", self) } } impl EncodeIntoContext for QuotedChar { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self.inner() { '\\' => ctx.write_all(b"\\\\"), '"' => ctx.write_all(b"\\\""), other => ctx.write_all(&[other as u8]), } } } impl EncodeIntoContext for StatusDataItem { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Self::Messages(count) => { ctx.write_all(b"MESSAGES ")?; count.encode_ctx(ctx) } Self::Recent(count) => { ctx.write_all(b"RECENT ")?; count.encode_ctx(ctx) } Self::UidNext(next) => { ctx.write_all(b"UIDNEXT ")?; next.encode_ctx(ctx) } Self::UidValidity(identifier) => { ctx.write_all(b"UIDVALIDITY ")?; identifier.encode_ctx(ctx) } Self::Unseen(count) => { ctx.write_all(b"UNSEEN ")?; count.encode_ctx(ctx) } Self::Deleted(count) => { ctx.write_all(b"DELETED ")?; count.encode_ctx(ctx) } Self::DeletedStorage(count) => { ctx.write_all(b"DELETED-STORAGE ")?; count.encode_ctx(ctx) } } } } impl<'a> EncodeIntoContext for MessageDataItem<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Self::BodyExt { section, origin, data, } => { ctx.write_all(b"BODY[")?; if let Some(section) = section { section.encode_ctx(ctx)?; } ctx.write_all(b"]")?; if let Some(origin) = origin { write!(ctx, "<{origin}>")?; } ctx.write_all(b" ")?; data.encode_ctx(ctx) } // FIXME: do not return body-ext-1part and body-ext-mpart here Self::Body(body) => { ctx.write_all(b"BODY ")?; body.encode_ctx(ctx) } Self::BodyStructure(body) => { ctx.write_all(b"BODYSTRUCTURE ")?; body.encode_ctx(ctx) } Self::Envelope(envelope) => { ctx.write_all(b"ENVELOPE ")?; envelope.encode_ctx(ctx) } Self::Flags(flags) => { ctx.write_all(b"FLAGS (")?; join_serializable(flags, b" ", ctx)?; ctx.write_all(b")") } Self::InternalDate(datetime) => { ctx.write_all(b"INTERNALDATE ")?; datetime.encode_ctx(ctx) } Self::Rfc822(nstring) => { ctx.write_all(b"RFC822 ")?; nstring.encode_ctx(ctx) } Self::Rfc822Header(nstring) => { ctx.write_all(b"RFC822.HEADER ")?; nstring.encode_ctx(ctx) } Self::Rfc822Size(size) => write!(ctx, "RFC822.SIZE {size}"), Self::Rfc822Text(nstring) => { ctx.write_all(b"RFC822.TEXT ")?; nstring.encode_ctx(ctx) } Self::Uid(uid) => write!(ctx, "UID {uid}"), } } } impl<'a> EncodeIntoContext for NString<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match &self.0 { Some(imap_str) => imap_str.encode_ctx(ctx), None => ctx.write_all(b"NIL"), } } } impl<'a> EncodeIntoContext for BodyStructure<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { ctx.write_all(b"(")?; match self { BodyStructure::Single { body, extension_data: extension, } => { body.encode_ctx(ctx)?; if let Some(extension) = extension { ctx.write_all(b" ")?; extension.encode_ctx(ctx)?; } } BodyStructure::Multi { bodies, subtype, extension_data, } => { for body in bodies.as_ref() { body.encode_ctx(ctx)?; } ctx.write_all(b" ")?; subtype.encode_ctx(ctx)?; if let Some(extension) = extension_data { ctx.write_all(b" ")?; extension.encode_ctx(ctx)?; } } } ctx.write_all(b")") } } impl<'a> EncodeIntoContext for Body<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self.specific { SpecificFields::Basic { r#type: ref type_, ref subtype, } => { type_.encode_ctx(ctx)?; ctx.write_all(b" ")?; subtype.encode_ctx(ctx)?; ctx.write_all(b" ")?; self.basic.encode_ctx(ctx) } SpecificFields::Message { ref envelope, ref body_structure, number_of_lines, } => { ctx.write_all(b"\"MESSAGE\" \"RFC822\" ")?; self.basic.encode_ctx(ctx)?; ctx.write_all(b" ")?; envelope.encode_ctx(ctx)?; ctx.write_all(b" ")?; body_structure.encode_ctx(ctx)?; ctx.write_all(b" ")?; write!(ctx, "{number_of_lines}") } SpecificFields::Text { ref subtype, number_of_lines, } => { ctx.write_all(b"\"TEXT\" ")?; subtype.encode_ctx(ctx)?; ctx.write_all(b" ")?; self.basic.encode_ctx(ctx)?; ctx.write_all(b" ")?; write!(ctx, "{number_of_lines}") } } } } impl<'a> EncodeIntoContext for BasicFields<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { List1AttributeValueOrNil(&self.parameter_list).encode_ctx(ctx)?; ctx.write_all(b" ")?; self.id.encode_ctx(ctx)?; ctx.write_all(b" ")?; self.description.encode_ctx(ctx)?; ctx.write_all(b" ")?; self.content_transfer_encoding.encode_ctx(ctx)?; ctx.write_all(b" ")?; write!(ctx, "{}", self.size) } } impl<'a> EncodeIntoContext for Envelope<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { ctx.write_all(b"(")?; self.date.encode_ctx(ctx)?; ctx.write_all(b" ")?; self.subject.encode_ctx(ctx)?; ctx.write_all(b" ")?; List1OrNil(&self.from, b"").encode_ctx(ctx)?; ctx.write_all(b" ")?; List1OrNil(&self.sender, b"").encode_ctx(ctx)?; ctx.write_all(b" ")?; List1OrNil(&self.reply_to, b"").encode_ctx(ctx)?; ctx.write_all(b" ")?; List1OrNil(&self.to, b"").encode_ctx(ctx)?; ctx.write_all(b" ")?; List1OrNil(&self.cc, b"").encode_ctx(ctx)?; ctx.write_all(b" ")?; List1OrNil(&self.bcc, b"").encode_ctx(ctx)?; ctx.write_all(b" ")?; self.in_reply_to.encode_ctx(ctx)?; ctx.write_all(b" ")?; self.message_id.encode_ctx(ctx)?; ctx.write_all(b")") } } impl<'a> EncodeIntoContext for Address<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { ctx.write_all(b"(")?; self.name.encode_ctx(ctx)?; ctx.write_all(b" ")?; self.adl.encode_ctx(ctx)?; ctx.write_all(b" ")?; self.mailbox.encode_ctx(ctx)?; ctx.write_all(b" ")?; self.host.encode_ctx(ctx)?; ctx.write_all(b")")?; Ok(()) } } impl<'a> EncodeIntoContext for SinglePartExtensionData<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { self.md5.encode_ctx(ctx)?; if let Some(disposition) = &self.tail { ctx.write_all(b" ")?; disposition.encode_ctx(ctx)?; } Ok(()) } } impl<'a> EncodeIntoContext for MultiPartExtensionData<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { List1AttributeValueOrNil(&self.parameter_list).encode_ctx(ctx)?; if let Some(disposition) = &self.tail { ctx.write_all(b" ")?; disposition.encode_ctx(ctx)?; } Ok(()) } } impl<'a> EncodeIntoContext for Disposition<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match &self.disposition { Some((s, param)) => { ctx.write_all(b"(")?; s.encode_ctx(ctx)?; ctx.write_all(b" ")?; List1AttributeValueOrNil(param).encode_ctx(ctx)?; ctx.write_all(b")")?; } None => ctx.write_all(b"NIL")?, } if let Some(language) = &self.tail { ctx.write_all(b" ")?; language.encode_ctx(ctx)?; } Ok(()) } } impl<'a> EncodeIntoContext for Language<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { List1OrNil(&self.language, b" ").encode_ctx(ctx)?; if let Some(location) = &self.tail { ctx.write_all(b" ")?; location.encode_ctx(ctx)?; } Ok(()) } } impl<'a> EncodeIntoContext for Location<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { self.location.encode_ctx(ctx)?; for body_extension in &self.extensions { ctx.write_all(b" ")?; body_extension.encode_ctx(ctx)?; } Ok(()) } } impl<'a> EncodeIntoContext for BodyExtension<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { BodyExtension::NString(nstring) => nstring.encode_ctx(ctx), BodyExtension::Number(number) => number.encode_ctx(ctx), BodyExtension::List(list) => { ctx.write_all(b"(")?; join_serializable(list.as_ref(), b" ", ctx)?; ctx.write_all(b")") } } } } impl EncodeIntoContext for ChronoDateTime { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { write!(ctx, "\"{}\"", self.format("%d-%b-%Y %H:%M:%S %z")) } } impl<'a> EncodeIntoContext for CommandContinuationRequest<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { match self { Self::Basic(continue_basic) => match continue_basic.code() { Some(code) => { ctx.write_all(b"+ [")?; code.encode_ctx(ctx)?; ctx.write_all(b"] ")?; continue_basic.text().encode_ctx(ctx)?; ctx.write_all(b"\r\n") } None => { ctx.write_all(b"+ ")?; continue_basic.text().encode_ctx(ctx)?; ctx.write_all(b"\r\n") } }, Self::Base64(data) => { ctx.write_all(b"+ ")?; ctx.write_all(base64.encode(data).as_bytes())?; ctx.write_all(b"\r\n") } } } } mod utils { use std::io::Write; use super::{EncodeContext, EncodeIntoContext}; pub struct List1OrNil<'a, T>(pub &'a Vec, pub &'a [u8]); pub struct List1AttributeValueOrNil<'a, T>(pub &'a Vec<(T, T)>); pub(crate) fn join_serializable( elements: &[I], sep: &[u8], ctx: &mut EncodeContext, ) -> std::io::Result<()> { if let Some((last, head)) = elements.split_last() { for item in head { item.encode_ctx(ctx)?; ctx.write_all(sep)?; } last.encode_ctx(ctx) } else { Ok(()) } } impl<'a, T> EncodeIntoContext for List1OrNil<'a, T> where T: EncodeIntoContext, { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { if let Some((last, head)) = self.0.split_last() { ctx.write_all(b"(")?; for item in head { item.encode_ctx(ctx)?; ctx.write_all(self.1)?; } last.encode_ctx(ctx)?; ctx.write_all(b")") } else { ctx.write_all(b"NIL") } } } impl<'a, T> EncodeIntoContext for List1AttributeValueOrNil<'a, T> where T: EncodeIntoContext, { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { if let Some((last, head)) = self.0.split_last() { ctx.write_all(b"(")?; for (attribute, value) in head { attribute.encode_ctx(ctx)?; ctx.write_all(b" ")?; value.encode_ctx(ctx)?; ctx.write_all(b" ")?; } let (attribute, value) = last; attribute.encode_ctx(ctx)?; ctx.write_all(b" ")?; value.encode_ctx(ctx)?; ctx.write_all(b")") } else { ctx.write_all(b"NIL") } } } } #[cfg(test)] mod tests { use std::num::NonZeroU32; use imap_types::{ auth::AuthMechanism, command::{Command, CommandBody}, core::{AString, Literal, NString, NonEmptyVec}, fetch::MessageDataItem, response::{Data, Response}, utils::escape_byte_string, }; use super::*; #[test] fn test_api_encoder_usage() { let cmd = Command::new( "A", CommandBody::login( AString::from(Literal::unvalidated_non_sync(b"alice".as_ref())), "password", ) .unwrap(), ) .unwrap(); // Dump. let got_encoded = CommandCodec::default().encode(&cmd).dump(); // Encoded. let encoded = CommandCodec::default().encode(&cmd); let mut out = Vec::new(); for x in encoded { match x { Fragment::Line { data } => { println!("C: {}", escape_byte_string(&data)); out.extend_from_slice(&data); } Fragment::Literal { data, mode } => { match mode { LiteralMode::Sync => println!("C: "), LiteralMode::NonSync => println!("C: "), } println!("C: {}", escape_byte_string(&data)); out.extend_from_slice(&data); } } } assert_eq!(got_encoded, out); } #[test] fn test_encode_command() { kat_encoder::, &[Fragment]>(&[ ( Command::new("A", CommandBody::login("alice", "pass").unwrap()).unwrap(), [Fragment::Line { data: b"A LOGIN alice pass\r\n".to_vec(), }] .as_ref(), ), ( Command::new( "A", CommandBody::login("alice", b"\xCA\xFE".as_ref()).unwrap(), ) .unwrap(), [ Fragment::Line { data: b"A LOGIN alice {2}\r\n".to_vec(), }, Fragment::Literal { data: b"\xCA\xFE".to_vec(), mode: LiteralMode::Sync, }, Fragment::Line { data: b"\r\n".to_vec(), }, ] .as_ref(), ), ( Command::new("A", CommandBody::authenticate(AuthMechanism::Login)).unwrap(), [Fragment::Line { data: b"A AUTHENTICATE LOGIN\r\n".to_vec(), }] .as_ref(), ), ( Command::new( "A", CommandBody::authenticate_with_ir(AuthMechanism::Login, b"alice".as_ref()), ) .unwrap(), [Fragment::Line { data: b"A AUTHENTICATE LOGIN YWxpY2U=\r\n".to_vec(), }] .as_ref(), ), ( Command::new("A", CommandBody::authenticate(AuthMechanism::Plain)).unwrap(), [Fragment::Line { data: b"A AUTHENTICATE PLAIN\r\n".to_vec(), }] .as_ref(), ), ( Command::new( "A", CommandBody::authenticate_with_ir( AuthMechanism::Plain, b"\x00alice\x00pass".as_ref(), ), ) .unwrap(), [Fragment::Line { data: b"A AUTHENTICATE PLAIN AGFsaWNlAHBhc3M=\r\n".to_vec(), }] .as_ref(), ), ]); } #[test] fn test_encode_response() { kat_encoder::, &[Fragment]>(&[ ( Response::Data(Data::Fetch { seq: NonZeroU32::new(12345).unwrap(), items: NonEmptyVec::from(MessageDataItem::BodyExt { section: None, origin: None, data: NString::from(Literal::unvalidated(b"ABCDE".as_ref())), }), }), [ Fragment::Line { data: b"* 12345 FETCH (BODY[] {5}\r\n".to_vec(), }, Fragment::Literal { data: b"ABCDE".to_vec(), mode: LiteralMode::Sync, }, Fragment::Line { data: b")\r\n".to_vec(), }, ] .as_ref(), ), ( Response::Data(Data::Fetch { seq: NonZeroU32::new(12345).unwrap(), items: NonEmptyVec::from(MessageDataItem::BodyExt { section: None, origin: None, data: NString::from(Literal::unvalidated_non_sync(b"ABCDE".as_ref())), }), }), [ Fragment::Line { data: b"* 12345 FETCH (BODY[] {5+}\r\n".to_vec(), }, Fragment::Literal { data: b"ABCDE".to_vec(), mode: LiteralMode::NonSync, }, Fragment::Line { data: b")\r\n".to_vec(), }, ] .as_ref(), ), ]) } fn kat_encoder<'a, E, M, F>(tests: &'a [(M, F)]) where E: Encoder = M> + Default, F: AsRef<[Fragment]>, { for (i, (obj, actions)) in tests.iter().enumerate() { println!("# Testing {i}"); let encoder = E::default().encode(obj); let actions = actions.as_ref(); assert_eq!(encoder.collect::>(), actions); } } } imap-codec-1.0.0/imap-codec/src/command.rs000066400000000000000000000432521447115025300202750ustar00rootroot00000000000000use std::borrow::Cow; #[cfg(not(feature = "quirk_crlf_relaxed"))] use abnf_core::streaming::crlf; #[cfg(feature = "quirk_crlf_relaxed")] use abnf_core::streaming::crlf_relaxed as crlf; use abnf_core::streaming::sp; use imap_types::{ auth::AuthMechanism, command::{Command, CommandBody}, core::AString, fetch::{Macro, MacroOrMessageDataItemNames}, flag::{Flag, StoreResponse, StoreType}, secret::Secret, }; use nom::{ branch::alt, bytes::streaming::{tag, tag_no_case}, combinator::{map, opt, value}, multi::{separated_list0, separated_list1}, sequence::{delimited, preceded, terminated, tuple}, }; use crate::{ auth::auth_type, core::{astring, base64, literal, tag_imap}, datetime::date_time, decode::{IMAPErrorKind, IMAPResult}, extensions::{ compress::compress, enable::enable, idle::idle, quota::{getquota, getquotaroot, setquota}, r#move::r#move, }, fetch::fetch_att, flag::{flag, flag_list}, mailbox::{list_mailbox, mailbox}, search::search, sequence::sequence_set, status::status_att, }; /// `command = tag SP ( /// command-any / /// command-auth / /// command-nonauth / /// command-select /// ) CRLF` pub(crate) fn command(input: &[u8]) -> IMAPResult<&[u8], Command> { let mut parser_tag = terminated(tag_imap, sp); let mut parser_body = terminated( alt((command_any, command_auth, command_nonauth, command_select)), crlf, ); let (remaining, obtained_tag) = parser_tag(input)?; match parser_body(remaining) { Ok((remaining, body)) => Ok(( remaining, Command { tag: obtained_tag, body, }, )), Err(mut error) => { // If we got an `IMAPErrorKind::Literal`, we fill in the missing `tag`. if let nom::Err::Error(ref mut err) | nom::Err::Failure(ref mut err) = error { if let IMAPErrorKind::Literal { ref mut tag, .. } = err.kind { *tag = Some(obtained_tag); } } Err(error) } } } // # Command Any /// `command-any = "CAPABILITY" / "LOGOUT" / "NOOP" / x-command` /// /// Note: Valid in all states pub(crate) fn command_any(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { alt(( value(CommandBody::Capability, tag_no_case(b"CAPABILITY")), value(CommandBody::Logout, tag_no_case(b"LOGOUT")), value(CommandBody::Noop, tag_no_case(b"NOOP")), // x-command = "X" atom ))(input) } // # Command Auth /// `command-auth = append / /// create / /// delete / /// examine / /// list / /// lsub / /// rename / /// select / /// status / /// subscribe / /// unsubscribe / /// idle ; RFC 2177 /// enable ; RFC 5161 /// compress ; RFC 4978` /// /// Note: Valid only in Authenticated or Selected state pub(crate) fn command_auth(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { alt(( append, create, delete, examine, list, lsub, rename, select, status, subscribe, unsubscribe, idle, enable, compress, getquota, getquotaroot, setquota, ))(input) } /// `append = "APPEND" SP mailbox [SP flag-list] [SP date-time] SP literal` pub(crate) fn append(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple(( tag_no_case(b"APPEND"), sp, mailbox, opt(preceded(sp, flag_list)), opt(preceded(sp, date_time)), sp, literal, )); let (remaining, (_, _, mailbox, flags, date, _, message)) = parser(input)?; Ok(( remaining, CommandBody::Append { mailbox, flags: flags.unwrap_or_default(), date, message, }, )) } /// `create = "CREATE" SP mailbox` /// /// Note: Use of INBOX gives a NO error pub(crate) fn create(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case(b"CREATE"), sp, mailbox)); let (remaining, (_, _, mailbox)) = parser(input)?; Ok((remaining, CommandBody::Create { mailbox })) } /// `delete = "DELETE" SP mailbox` /// /// Note: Use of INBOX gives a NO error pub(crate) fn delete(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case(b"DELETE"), sp, mailbox)); let (remaining, (_, _, mailbox)) = parser(input)?; Ok((remaining, CommandBody::Delete { mailbox })) } /// `examine = "EXAMINE" SP mailbox` pub(crate) fn examine(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case(b"EXAMINE"), sp, mailbox)); let (remaining, (_, _, mailbox)) = parser(input)?; Ok((remaining, CommandBody::Examine { mailbox })) } /// `list = "LIST" SP mailbox SP list-mailbox` pub(crate) fn list(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case(b"LIST"), sp, mailbox, sp, list_mailbox)); let (remaining, (_, _, reference, _, mailbox_wildcard)) = parser(input)?; Ok(( remaining, CommandBody::List { reference, mailbox_wildcard, }, )) } /// `lsub = "LSUB" SP mailbox SP list-mailbox` pub(crate) fn lsub(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case(b"LSUB"), sp, mailbox, sp, list_mailbox)); let (remaining, (_, _, reference, _, mailbox_wildcard)) = parser(input)?; Ok(( remaining, CommandBody::Lsub { reference, mailbox_wildcard, }, )) } /// `rename = "RENAME" SP mailbox SP mailbox` /// /// Note: Use of INBOX as a destination gives a NO error pub(crate) fn rename(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case(b"RENAME"), sp, mailbox, sp, mailbox)); let (remaining, (_, _, mailbox, _, new_mailbox)) = parser(input)?; Ok(( remaining, CommandBody::Rename { from: mailbox, to: new_mailbox, }, )) } /// `select = "SELECT" SP mailbox` pub(crate) fn select(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case(b"SELECT"), sp, mailbox)); let (remaining, (_, _, mailbox)) = parser(input)?; Ok((remaining, CommandBody::Select { mailbox })) } /// `status = "STATUS" SP mailbox SP "(" status-att *(SP status-att) ")"` pub(crate) fn status(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple(( tag_no_case(b"STATUS"), sp, mailbox, sp, delimited(tag(b"("), separated_list0(sp, status_att), tag(b")")), )); let (remaining, (_, _, mailbox, _, item_names)) = parser(input)?; Ok(( remaining, CommandBody::Status { mailbox, item_names: item_names.into(), }, )) } /// `subscribe = "SUBSCRIBE" SP mailbox` pub(crate) fn subscribe(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case(b"SUBSCRIBE"), sp, mailbox)); let (remaining, (_, _, mailbox)) = parser(input)?; Ok((remaining, CommandBody::Subscribe { mailbox })) } /// `unsubscribe = "UNSUBSCRIBE" SP mailbox` pub(crate) fn unsubscribe(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case(b"UNSUBSCRIBE"), sp, mailbox)); let (remaining, (_, _, mailbox)) = parser(input)?; Ok((remaining, CommandBody::Unsubscribe { mailbox })) } // # Command NonAuth /// `command-nonauth = login / authenticate / "STARTTLS"` /// /// Note: Valid only when in Not Authenticated state pub(crate) fn command_nonauth(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = alt(( login, map(authenticate, |(mechanism, initial_response)| { CommandBody::Authenticate { mechanism, initial_response, } }), #[cfg(feature = "starttls")] value(CommandBody::StartTLS, tag_no_case(b"STARTTLS")), )); let (remaining, parsed_command_nonauth) = parser(input)?; Ok((remaining, parsed_command_nonauth)) } /// `login = "LOGIN" SP userid SP password` pub(crate) fn login(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case(b"LOGIN"), sp, userid, sp, password)); let (remaining, (_, _, username, _, password)) = parser(input)?; Ok(( remaining, CommandBody::Login { username, password: Secret::new(password), }, )) } #[inline] /// `userid = astring` pub(crate) fn userid(input: &[u8]) -> IMAPResult<&[u8], AString> { astring(input) } #[inline] /// `password = astring` pub(crate) fn password(input: &[u8]) -> IMAPResult<&[u8], AString> { astring(input) } /// `authenticate = "AUTHENTICATE" SP auth-type *(CRLF base64)` (edited) /// /// ```text /// Added by SASL-IR /// | /// vvvvvvvvvvvvvvvvvvv /// authenticate = "AUTHENTICATE" SP auth-type [SP (base64 / "=")] *(CRLF base64) /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /// | /// This is parsed here. /// CRLF is parsed by upper command parser. /// ``` #[allow(clippy::type_complexity)] pub(crate) fn authenticate( input: &[u8], ) -> IMAPResult<&[u8], (AuthMechanism, Option>>)> { let mut parser = tuple(( tag_no_case(b"AUTHENTICATE "), auth_type, opt(preceded( sp, alt(( map(base64, |data| Secret::new(Cow::Owned(data))), value(Secret::new(Cow::Borrowed(&b""[..])), tag("=")), )), )), )); let (remaining, (_, auth_type, raw_data)) = parser(input)?; // Server must send continuation ("+ ") at this point... Ok((remaining, (auth_type, raw_data))) } // # Command Select /// `command-select = "CHECK" / /// "CLOSE" / /// "EXPUNGE" / /// copy / /// fetch / /// store / /// uid / /// search` /// /// Note: Valid only when in Selected state pub(crate) fn command_select(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { alt(( value(CommandBody::Check, tag_no_case(b"CHECK")), value(CommandBody::Close, tag_no_case(b"CLOSE")), value(CommandBody::Expunge, tag_no_case(b"EXPUNGE")), copy, fetch, store, uid, search, value(CommandBody::Unselect, tag_no_case(b"UNSELECT")), r#move, ))(input) } /// `copy = "COPY" SP sequence-set SP mailbox` pub(crate) fn copy(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case(b"COPY"), sp, sequence_set, sp, mailbox)); let (remaining, (_, _, sequence_set, _, mailbox)) = parser(input)?; Ok(( remaining, CommandBody::Copy { sequence_set, mailbox, uid: false, }, )) } /// `fetch = "FETCH" SP sequence-set SP ("ALL" / /// "FULL" / /// "FAST" / /// fetch-att / "(" fetch-att *(SP fetch-att) ")")` pub(crate) fn fetch(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple(( tag_no_case(b"FETCH"), sp, sequence_set, sp, alt(( value( MacroOrMessageDataItemNames::Macro(Macro::All), tag_no_case(b"ALL"), ), value( MacroOrMessageDataItemNames::Macro(Macro::Fast), tag_no_case(b"FAST"), ), value( MacroOrMessageDataItemNames::Macro(Macro::Full), tag_no_case(b"FULL"), ), map(fetch_att, |fetch_att| { MacroOrMessageDataItemNames::MessageDataItemNames(vec![fetch_att]) }), map( delimited(tag(b"("), separated_list0(sp, fetch_att), tag(b")")), MacroOrMessageDataItemNames::MessageDataItemNames, ), )), )); let (remaining, (_, _, sequence_set, _, macro_or_item_names)) = parser(input)?; Ok(( remaining, CommandBody::Fetch { sequence_set, macro_or_item_names, uid: false, }, )) } /// `store = "STORE" SP sequence-set SP store-att-flags` pub(crate) fn store(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case(b"STORE"), sp, sequence_set, sp, store_att_flags)); let (remaining, (_, _, sequence_set, _, (kind, response, flags))) = parser(input)?; Ok(( remaining, CommandBody::Store { sequence_set, kind, response, flags, uid: false, }, )) } /// `store-att-flags = (["+" / "-"] "FLAGS" [".SILENT"]) SP (flag-list / (flag *(SP flag)))` pub(crate) fn store_att_flags( input: &[u8], ) -> IMAPResult<&[u8], (StoreType, StoreResponse, Vec)> { let mut parser = tuple(( tuple(( map( opt(alt(( value(StoreType::Add, tag(b"+")), value(StoreType::Remove, tag(b"-")), ))), |type_| match type_ { Some(type_) => type_, None => StoreType::Replace, }, ), tag_no_case(b"FLAGS"), map(opt(tag_no_case(b".SILENT")), |x| match x { Some(_) => StoreResponse::Silent, None => StoreResponse::Answer, }), )), sp, alt((flag_list, separated_list1(sp, flag))), )); let (remaining, ((store_type, _, store_response), _, flag_list)) = parser(input)?; Ok((remaining, (store_type, store_response, flag_list))) } /// `uid = "UID" SP (copy / fetch / search / store)` /// /// Note: Unique identifiers used instead of message sequence numbers pub(crate) fn uid(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple(( tag_no_case(b"UID"), sp, alt((copy, fetch, search, store, r#move)), )); let (remaining, (_, _, mut cmd)) = parser(input)?; match cmd { CommandBody::Copy { ref mut uid, .. } | CommandBody::Fetch { ref mut uid, .. } | CommandBody::Search { ref mut uid, .. } | CommandBody::Store { ref mut uid, .. } | CommandBody::Move { ref mut uid, .. } => *uid = true, _ => unreachable!(), } Ok((remaining, cmd)) } #[cfg(test)] mod tests { use std::num::NonZeroU32; use imap_types::{ core::Tag, fetch::{MessageDataItemName, Section}, }; use super::*; use crate::{encode::Encoder, CommandCodec}; #[test] fn test_parse_fetch() { println!("{:#?}", fetch(b"fetch 1:1 (flags)???")); } #[test] fn test_parse_fetch_att() { let tests = [ (MessageDataItemName::Envelope, "ENVELOPE???"), (MessageDataItemName::Flags, "FLAGS???"), (MessageDataItemName::InternalDate, "INTERNALDATE???"), (MessageDataItemName::Rfc822, "RFC822???"), (MessageDataItemName::Rfc822Header, "RFC822.HEADER???"), (MessageDataItemName::Rfc822Size, "RFC822.SIZE???"), (MessageDataItemName::Rfc822Text, "RFC822.TEXT???"), (MessageDataItemName::Body, "BODY???"), (MessageDataItemName::BodyStructure, "BODYSTRUCTURE???"), (MessageDataItemName::Uid, "UID???"), ( MessageDataItemName::BodyExt { partial: None, peek: false, section: None, }, "BODY[]???", ), ( MessageDataItemName::BodyExt { partial: None, peek: true, section: None, }, "BODY.PEEK[]???", ), ( MessageDataItemName::BodyExt { partial: None, peek: true, section: Some(Section::Text(None)), }, "BODY.PEEK[TEXT]???", ), ( MessageDataItemName::BodyExt { partial: Some((42, NonZeroU32::try_from(1337).unwrap())), peek: true, section: Some(Section::Text(None)), }, "BODY.PEEK[TEXT]<42.1337>???", ), ]; let expected_remainder = "???".as_bytes(); for (expected, test) in tests { let (got_remainder, got) = fetch_att(test.as_bytes()).unwrap(); assert_eq!(expected, got); assert_eq!(expected_remainder, got_remainder); } } #[test] fn test_that_empty_ir_is_encoded_correctly() { let command = Command::new( Tag::try_from("A").unwrap(), CommandBody::Authenticate { mechanism: AuthMechanism::Plain, initial_response: Some(Secret::new(Cow::Borrowed(&b""[..]))), }, ) .unwrap(); let buffer = CommandCodec::default().encode(&command).dump(); assert_eq!(buffer, b"A AUTHENTICATE PLAIN =\r\n") } } imap-codec-1.0.0/imap-codec/src/core.rs000066400000000000000000000303441447115025300176050ustar00rootroot00000000000000use std::{borrow::Cow, num::NonZeroU32, str::from_utf8}; #[cfg(not(feature = "quirk_crlf_relaxed"))] use abnf_core::streaming::crlf; #[cfg(feature = "quirk_crlf_relaxed")] use abnf_core::streaming::crlf_relaxed as crlf; use abnf_core::{is_alpha, is_digit, streaming::dquote}; use base64::{engine::general_purpose::STANDARD as _base64, Engine}; use imap_types::{ core::{ AString, Atom, AtomExt, Charset, IString, Literal, LiteralMode, NString, Quoted, QuotedChar, Tag, Text, }, utils::{ indicators::{is_astring_char, is_atom_char, is_quoted_specials, is_text_char}, unescape_quoted, }, }; use nom::{ branch::alt, bytes::streaming::{escaped, tag, tag_no_case, take, take_while, take_while1, take_while_m_n}, character::streaming::{char, digit1, one_of}, combinator::{map, map_res, opt, recognize}, sequence::{delimited, terminated, tuple}, }; use crate::decode::{IMAPErrorKind, IMAPParseError, IMAPResult}; // ----- number ----- /// `number = 1*DIGIT` /// /// Unsigned 32-bit integer (0 <= n < 4,294,967,296) pub(crate) fn number(input: &[u8]) -> IMAPResult<&[u8], u32> { map_res( // # Safety // // `unwrap` is safe because `1*DIGIT` contains ASCII-only characters. map(digit1, |val| from_utf8(val).unwrap()), str::parse::, )(input) } /// ```abnf /// number64 = 1*DIGIT /// ``` /// /// Unsigned 63-bit integer (0 <= n <= 9,223,372,036,854,775,807) /// /// Defined in RFC 9051 pub(crate) fn number64(input: &[u8]) -> IMAPResult<&[u8], u64> { map_res( // # Safety // // `unwrap` is safe because `1*DIGIT` contains ASCII-only characters. map(digit1, |val| from_utf8(val).unwrap()), str::parse::, )(input) } /// `nz-number = digit-nz *DIGIT` /// /// Non-zero unsigned 32-bit integer (0 < n < 4,294,967,296) pub(crate) fn nz_number(input: &[u8]) -> IMAPResult<&[u8], NonZeroU32> { map_res(number, NonZeroU32::try_from)(input) } // ----- string ----- /// `string = quoted / literal` pub(crate) fn string(input: &[u8]) -> IMAPResult<&[u8], IString> { alt((map(quoted, IString::Quoted), map(literal, IString::Literal)))(input) } /// `quoted = DQUOTE *QUOTED-CHAR DQUOTE` /// /// This function only allocates a new String, when needed, i.e. when /// quoted chars need to be replaced. pub(crate) fn quoted(input: &[u8]) -> IMAPResult<&[u8], Quoted> { let mut parser = tuple(( dquote, map( escaped( take_while1(is_any_text_char_except_quoted_specials), '\\', one_of("\\\""), ), // # Saftey // // `unwrap` is safe because val contains ASCII-only characters. |val| from_utf8(val).unwrap(), ), dquote, )); let (remaining, (_, quoted, _)) = parser(input)?; Ok((remaining, Quoted::unvalidated(unescape_quoted(quoted)))) } /// `QUOTED-CHAR = / "\" quoted-specials` pub(crate) fn quoted_char(input: &[u8]) -> IMAPResult<&[u8], QuotedChar> { map( alt(( map( take_while_m_n(1, 1, is_any_text_char_except_quoted_specials), |bytes: &[u8]| { assert_eq!(bytes.len(), 1); bytes[0] as char }, ), map( tuple((tag("\\"), take_while_m_n(1, 1, is_quoted_specials))), |(_, bytes): (_, &[u8])| { assert_eq!(bytes.len(), 1); bytes[0] as char }, ), )), QuotedChar::unvalidated, )(input) } pub(crate) fn is_any_text_char_except_quoted_specials(byte: u8) -> bool { is_text_char(byte) && !is_quoted_specials(byte) } /// `literal = "{" number "}" CRLF *CHAR8` /// /// Number represents the number of CHAR8s /// /// # IMAP4 Non-synchronizing Literals /// /// ```abnf /// literal = "{" number ["+"] "}" CRLF *CHAR8 /// ; Number represents the number of CHAR8 octets /// /// CHAR8 = /// /// literal8 = /// ``` /// -- pub(crate) fn literal(input: &[u8]) -> IMAPResult<&[u8], Literal> { let (remaining, (length, mode)) = terminated( delimited( tag(b"{"), tuple(( number, map(opt(char('+')), |i| { i.map(|_| LiteralMode::NonSync).unwrap_or(LiteralMode::Sync) }), )), tag(b"}"), ), crlf, )(input)?; // Signal that an continuation request could be required. // Note: This doesn't trigger when there is data following the literal prefix. if remaining.is_empty() { return Err(nom::Err::Failure(IMAPParseError { input, kind: IMAPErrorKind::Literal { // We don't know the tag here and rely on an upper parser, e.g., `command` to fill this in. tag: None, length, mode, }, })); } let (remaining, data) = take(length)(remaining)?; match Literal::try_from(data) { Ok(mut literal) => { literal.set_mode(mode); Ok((remaining, literal)) } Err(_) => Err(nom::Err::Failure(IMAPParseError { input, kind: IMAPErrorKind::LiteralContainsNull, })), } } // ----- astring ----- atom (roughly) or string /// `astring = 1*ASTRING-CHAR / string` pub(crate) fn astring(input: &[u8]) -> IMAPResult<&[u8], AString> { alt(( map(take_while1(is_astring_char), |bytes: &[u8]| { // # Safety // // `unwrap` is safe, because `is_astring_char` enforces that the bytes ... // * contain ASCII-only characters, i.e., `from_utf8` will return `Ok`. // * are valid according to `AtomExt::verify(), i.e., `unvalidated` is safe. AString::Atom(AtomExt::unvalidated(Cow::Borrowed( std::str::from_utf8(bytes).unwrap(), ))) }), map(string, AString::String), ))(input) } /// `atom = 1*ATOM-CHAR` pub(crate) fn atom(input: &[u8]) -> IMAPResult<&[u8], Atom> { let parser = take_while1(is_atom_char); let (remaining, parsed_atom) = parser(input)?; // # Saftey // // `unwrap` is safe, because `is_atom_char` enforces ... // * that the string is always UTF8, and ... // * contains only the allowed characters. Ok(( remaining, Atom::unvalidated(from_utf8(parsed_atom).unwrap()), )) } // ----- nstring ----- nil or string /// `nstring = string / nil` pub(crate) fn nstring(input: &[u8]) -> IMAPResult<&[u8], NString> { alt(( map(string, |item| NString(Some(item))), map(nil, |_| NString(None)), ))(input) } #[inline] /// `nil = "NIL"` pub(crate) fn nil(input: &[u8]) -> IMAPResult<&[u8], &[u8]> { tag_no_case(b"NIL")(input) } // ----- text ----- /// `text = 1*TEXT-CHAR` pub(crate) fn text(input: &[u8]) -> IMAPResult<&[u8], Text> { map(take_while1(is_text_char), |bytes| // # Safety // // `is_text_char` makes sure that the sequence of bytes // is always valid ASCII. Thus, it is also valid UTF-8. Text::unvalidated(from_utf8(bytes).unwrap()))(input) } // ----- base64 ----- /// `base64 = *(4base64-char) [base64-terminal]` pub(crate) fn base64(input: &[u8]) -> IMAPResult<&[u8], Vec> { map_res( recognize(tuple(( take_while(is_base64_char), opt(alt((tag("=="), tag("=")))), ))), |input| _base64.decode(input), )(input) } /// `base64-char = ALPHA / DIGIT / "+" / "/" ; Case-sensitive` pub(crate) fn is_base64_char(i: u8) -> bool { is_alpha(i) || is_digit(i) || i == b'+' || i == b'/' } // base64-terminal = (2base64-char "==") / (3base64-char "=") // ----- charset ----- /// `charset = atom / quoted` /// /// Note: see errata id: 261 pub(crate) fn charset(input: &[u8]) -> IMAPResult<&[u8], Charset> { alt((map(atom, Charset::Atom), map(quoted, Charset::Quoted)))(input) } // ----- tag ----- /// `tag = 1*` pub(crate) fn tag_imap(input: &[u8]) -> IMAPResult<&[u8], Tag> { map(take_while1(|b| is_astring_char(b) && b != b'+'), |val| { // # Safety // // `is_astring_char` ensures that `val` is UTF-8. Tag::unvalidated(from_utf8(val).unwrap()) })(input) } #[cfg(test)] mod tests { use super::*; use crate::encode::{EncodeContext, EncodeIntoContext}; #[test] fn test_atom() { assert!(atom(b" ").is_err()); assert!(atom(b"").is_err()); let (rem, val) = atom(b"a(").unwrap(); assert_eq!(val, "a".try_into().unwrap()); assert_eq!(rem, b"("); let (rem, val) = atom(b"xxx yyy").unwrap(); assert_eq!(val, "xxx".try_into().unwrap()); assert_eq!(rem, b" yyy"); } #[test] fn test_quoted() { let (rem, val) = quoted(br#""Hello"???"#).unwrap(); assert_eq!(rem, b"???"); assert_eq!(val, Quoted::try_from("Hello").unwrap()); // Allowed escapes... assert!(quoted(br#""Hello \" "???"#).is_ok()); assert!(quoted(br#""Hello \\ "???"#).is_ok()); // Not allowed escapes... assert!(quoted(br#""Hello \a "???"#).is_err()); assert!(quoted(br#""Hello \z "???"#).is_err()); assert!(quoted(br#""Hello \? "???"#).is_err()); let (rem, val) = quoted(br#""Hello \"World\""???"#).unwrap(); assert_eq!(rem, br#"???"#); // Should it be this (Hello \"World\") ... //assert_eq!(val, r#"Hello \"World\""#); // ... or this (Hello "World")? assert_eq!(val, Quoted::try_from("Hello \"World\"").unwrap()); // Test Incomplete assert!(matches!(quoted(br#""#), Err(nom::Err::Incomplete(_)))); assert!(matches!(quoted(br#""\"#), Err(nom::Err::Incomplete(_)))); assert!(matches!( quoted(br#""Hello "#), Err(nom::Err::Incomplete(_)) )); // Test Error assert!(matches!(quoted(br#"\"#), Err(nom::Err::Error(_)))); } #[test] fn test_quoted_char() { let (rem, val) = quoted_char(b"\\\"xxx").unwrap(); assert_eq!(rem, b"xxx"); assert_eq!(val, QuotedChar::try_from('"').unwrap()); } #[test] fn test_number() { assert!(number(b"").is_err()); assert!(number(b"?").is_err()); assert!(number(b"0?").is_ok()); assert!(number(b"55?").is_ok()); assert!(number(b"999?").is_ok()); } #[test] fn test_nz_number() { assert!(number(b"").is_err()); assert!(number(b"?").is_err()); assert!(nz_number(b"0?").is_err()); assert!(nz_number(b"55?").is_ok()); assert!(nz_number(b"999?").is_ok()); } #[test] fn test_literal() { assert!(literal(b"{3}\r\n123").is_ok()); assert!(literal(b"{3}\r\n1\x003").is_err()); let (rem, val) = literal(b"{3}\r\n123xxx").unwrap(); assert_eq!(rem, b"xxx"); assert_eq!(val, Literal::try_from(b"123".as_slice()).unwrap()); } #[test] fn test_nil() { assert!(nil(b"nil").is_ok()); assert!(nil(b"nil ").is_ok()); assert!(nil(b" nil").is_err()); assert!(nil(b"null").is_err()); let (rem, _) = nil(b"nilxxx").unwrap(); assert_eq!(rem, b"xxx"); } #[test] fn test_encode_charset() { let tests = [ ("bengali", "bengali"), ("\"simple\" english", r#""\"simple\" english""#), ("", "\"\""), ("\"", "\"\\\"\""), ("\\", "\"\\\\\""), ]; for (from, expected) in tests.iter() { let cs = Charset::try_from(*from).unwrap(); println!("{:?}", cs); let mut ctx = EncodeContext::new(); cs.encode_ctx(&mut ctx).unwrap(); let out = ctx.dump(); assert_eq!(from_utf8(&out).unwrap(), *expected); } assert!(Charset::try_from("\r").is_err()); assert!(Charset::try_from("\n").is_err()); assert!(Charset::try_from("¹").is_err()); assert!(Charset::try_from("²").is_err()); assert!(Charset::try_from("\x00").is_err()); } } imap-codec-1.0.0/imap-codec/src/datetime.rs000066400000000000000000000306131447115025300204500ustar00rootroot00000000000000use abnf_core::{ is_digit, streaming::{dquote, sp}, }; use chrono::{ FixedOffset, LocalResult, NaiveDate as ChronoNaiveDate, NaiveDateTime, NaiveTime, TimeZone, }; use imap_types::datetime::{DateTime, NaiveDate}; use nom::{ branch::alt, bytes::streaming::{tag, tag_no_case, take_while_m_n}, character::streaming::char, combinator::{map, map_res, value}, sequence::{delimited, preceded, tuple}, }; use crate::decode::{IMAPErrorKind, IMAPParseError, IMAPResult}; /// ```abnf /// date = date-text / DQUOTE date-text DQUOTE /// ``` pub(crate) fn date(input: &[u8]) -> IMAPResult<&[u8], Option> { alt((date_text, delimited(dquote, date_text, dquote)))(input) } /// ```abnf /// date-text = date-day "-" date-month "-" date-year /// ``` pub(crate) fn date_text(input: &[u8]) -> IMAPResult<&[u8], Option> { let mut parser = tuple((date_day, tag(b"-"), date_month, tag(b"-"), date_year)); let (remaining, (d, _, m, _, y)) = parser(input)?; Ok(( remaining, ChronoNaiveDate::from_ymd_opt(y.into(), m.into(), d.into()).map(NaiveDate::unvalidated), )) } /// Day of month. /// /// ```abnf /// date-day = 1*2DIGIT /// ``` pub(crate) fn date_day(input: &[u8]) -> IMAPResult<&[u8], u8> { digit_1_2(input) } /// ```abnf /// date-month = "Jan" / "Feb" / "Mar" / "Apr" / /// "May" / "Jun" / "Jul" / "Aug" / /// "Sep" / "Oct" / "Nov" / "Dec" /// ``` pub(crate) fn date_month(input: &[u8]) -> IMAPResult<&[u8], u8> { alt(( value(1, tag_no_case(b"Jan")), value(2, tag_no_case(b"Feb")), value(3, tag_no_case(b"Mar")), value(4, tag_no_case(b"Apr")), value(5, tag_no_case(b"May")), value(6, tag_no_case(b"Jun")), value(7, tag_no_case(b"Jul")), value(8, tag_no_case(b"Aug")), value(9, tag_no_case(b"Sep")), value(10, tag_no_case(b"Oct")), value(11, tag_no_case(b"Nov")), value(12, tag_no_case(b"Dec")), ))(input) } /// ```abnf /// date-year = 4DIGIT /// ``` pub(crate) fn date_year(input: &[u8]) -> IMAPResult<&[u8], u16> { digit_4(input) } /// Hours minutes seconds. /// /// ```abnf /// time = 2DIGIT ":" 2DIGIT ":" 2DIGIT /// ``` pub(crate) fn time(input: &[u8]) -> IMAPResult<&[u8], Option> { let mut parser = tuple((digit_2, tag(b":"), digit_2, tag(b":"), digit_2)); let (remaining, (h, _, m, _, s)) = parser(input)?; Ok(( remaining, NaiveTime::from_hms_opt(h.into(), m.into(), s.into()), )) } /// ```abnf /// date-time = DQUOTE /// date-day-fixed "-" date-month "-" date-year SP /// time SP /// zone /// DQUOTE /// ``` pub(crate) fn date_time(input: &[u8]) -> IMAPResult<&[u8], DateTime> { let mut parser = delimited( dquote, tuple(( date_day_fixed, tag(b"-"), date_month, tag(b"-"), date_year, sp, time, sp, zone, )), dquote, ); let (remaining, (d, _, m, _, y, _, time, _, zone)) = parser(input)?; let date = ChronoNaiveDate::from_ymd_opt(y.into(), m.into(), d.into()); match (date, time, zone) { (Some(date), Some(time), Some(zone)) => { let local_datetime = NaiveDateTime::new(date, time); if let LocalResult::Single(datetime) = zone.from_local_datetime(&local_datetime) { Ok((remaining, DateTime::unvalidated(datetime))) } else { Err(nom::Err::Failure(IMAPParseError { input, kind: IMAPErrorKind::BadDateTime, })) } } _ => Err(nom::Err::Failure(IMAPParseError { input, kind: IMAPErrorKind::BadDateTime, })), } } /// Fixed-format version of date-day. /// /// ```abnf /// date-day-fixed = (SP DIGIT) / 2DIGIT /// ``` pub(crate) fn date_day_fixed(input: &[u8]) -> IMAPResult<&[u8], u8> { alt(( map( preceded(sp, take_while_m_n(1, 1, is_digit)), |bytes: &[u8]| bytes[0] - b'0', ), digit_2, ))(input) } /// Signed four-digit value of hhmm representing hours and minutes east of Greenwich (that is, the /// amount that the given time differs from Universal Time). /// /// Subtracting the timezone from the given time will give the UT form. The Universal Time zone is /// "+0000". /// /// ```abnf /// zone = ("+" / "-") 4DIGIT /// ``` pub(crate) fn zone(input: &[u8]) -> IMAPResult<&[u8], Option> { let mut parser = tuple((alt((char('+'), char('-'))), digit_2, digit_2)); let (remaining, (sign, hh, mm)) = parser(input)?; let offset = 3600 * (hh as i32) + 60 * (mm as i32); let zone = match sign { '+' => FixedOffset::east_opt(offset), '-' => FixedOffset::west_opt(offset), _ => unreachable!(), }; Ok((remaining, zone)) } fn digit_1_2(input: &[u8]) -> IMAPResult<&[u8], u8> { map_res( map(take_while_m_n(1, 2, is_digit), |bytes| { // # Safety // // `bytes` is always UTF-8. std::str::from_utf8(bytes).unwrap() }), str::parse::, )(input) } fn digit_2(input: &[u8]) -> IMAPResult<&[u8], u8> { map_res( map(take_while_m_n(2, 2, is_digit), |bytes| { // # Safety // // `bytes` is always UTF-8. std::str::from_utf8(bytes).unwrap() }), str::parse::, )(input) } fn digit_4(input: &[u8]) -> IMAPResult<&[u8], u16> { map_res( map(take_while_m_n(4, 4, is_digit), |bytes| { // # Safety // // `bytes` is always UTF-8. std::str::from_utf8(bytes).unwrap() }), str::parse::, )(input) } #[cfg(test)] mod tests { use std::str::from_utf8; use super::*; use crate::testing::known_answer_test_encode; #[test] fn test_encode_date_time() { let tests = [ ( DateTime::try_from( chrono::DateTime::parse_from_rfc2822("Mon, 7 Feb 1994 21:52:25 -0800 (PST)") .unwrap(), ) .unwrap(), b"\"07-Feb-1994 21:52:25 -0800\"".as_ref(), ), ( DateTime::try_from( chrono::DateTime::parse_from_rfc2822("Mon, 7 Feb 0000 21:52:25 -0800 (PST)") .unwrap(), ) .unwrap(), b"\"07-Feb-0000 21:52:25 -0800\"".as_ref(), ), ]; for test in tests { known_answer_test_encode(test); } } #[test] fn test_date() { let (rem, val) = date(b"1-Feb-2020xxx").unwrap(); assert_eq!(rem, b"xxx"); assert_eq!( val, ChronoNaiveDate::from_ymd_opt(2020, 2, 1).map(NaiveDate::unvalidated) ); let (rem, val) = date(b"\"1-Feb-2020\"xxx").unwrap(); assert_eq!(rem, b"xxx"); assert_eq!( val, ChronoNaiveDate::from_ymd_opt(2020, 2, 1).map(NaiveDate::unvalidated) ); let (rem, val) = date(b"\"01-Feb-2020\"xxx").unwrap(); assert_eq!(rem, b"xxx"); assert_eq!( val, ChronoNaiveDate::from_ymd_opt(2020, 2, 1).map(NaiveDate::unvalidated) ); } #[test] fn test_date_text() { let (rem, val) = date_text(b"1-Feb-2020").unwrap(); assert_eq!(rem, b""); assert_eq!( val, ChronoNaiveDate::from_ymd_opt(2020, 2, 1).map(NaiveDate::unvalidated) ); } #[test] fn test_date_day() { let (rem, val) = date_day(b"1xxx").unwrap(); assert_eq!(rem, b"xxx"); assert_eq!(val, 1); let (rem, val) = date_day(b"01xxx").unwrap(); assert_eq!(rem, b"xxx"); assert_eq!(val, 1); let (rem, val) = date_day(b"999xxx").unwrap(); assert_eq!(rem, b"9xxx"); assert_eq!(val, 99); } #[test] fn test_date_month() { let (rem, val) = date_month(b"jAn").unwrap(); assert_eq!(rem, b""); assert_eq!(val, 1); let (rem, val) = date_month(b"DeCxxx").unwrap(); assert_eq!(rem, b"xxx"); assert_eq!(val, 12); } #[test] fn test_date_year() { let (rem, val) = date_year(b"1985xxx").unwrap(); assert_eq!(rem, b"xxx"); assert_eq!(val, 1985); let (rem, val) = date_year(b"1991xxx").unwrap(); assert_eq!(rem, b"xxx"); assert_eq!(val, 1991); } #[test] fn test_date_day_fixed() { let (rem, val) = date_day_fixed(b"00").unwrap(); assert_eq!(rem, b""); assert_eq!(val, 0); let (rem, val) = date_day_fixed(b" 0").unwrap(); assert_eq!(rem, b""); assert_eq!(val, 0); let (rem, val) = date_day_fixed(b"99").unwrap(); assert_eq!(rem, b""); assert_eq!(val, 99); let (rem, val) = date_day_fixed(b" 9").unwrap(); assert_eq!(rem, b""); assert_eq!(val, 9); } #[test] fn test_time() { assert!(time(b"1:34:56xxx").is_err()); assert!(time(b"12:3:56xxx").is_err()); assert!(time(b"12:34:5xxx").is_err()); let (rem, val) = time(b"12:34:56xxx").unwrap(); assert_eq!(rem, b"xxx"); assert_eq!(val, NaiveTime::from_hms_opt(12, 34, 56)); let (rem, val) = time(b"99:99:99 ").unwrap(); assert_eq!(rem, b" "); assert_eq!(val, NaiveTime::from_hms_opt(99, 99, 99)); let (rem, val) = time(b"12:34:56").unwrap(); assert_eq!(rem, b""); assert_eq!(val, NaiveTime::from_hms_opt(12, 34, 56)); let (rem, val) = time(b"99:99:99").unwrap(); assert_eq!(rem, b""); assert_eq!(val, NaiveTime::from_hms_opt(99, 99, 99)); } #[test] fn test_date_time() { let (rem, val) = date_time(b"\" 1-Feb-1985 12:34:56 +0100\"xxx").unwrap(); assert_eq!(rem, b"xxx"); let local_datetime = NaiveDateTime::new( ChronoNaiveDate::from_ymd_opt(1985, 2, 1).unwrap(), NaiveTime::from_hms_opt(12, 34, 56).unwrap(), ); let datetime = DateTime::try_from( FixedOffset::east_opt(3600) .unwrap() .from_local_datetime(&local_datetime) .unwrap(), ) .unwrap(); println!("{:?} == \n{:?}", val, datetime); assert_eq!(val, datetime); } #[test] fn test_date_time_invalid() { let tests = [ b"\" 1-Feb-0000 12:34:56 +0000\"xxx".as_ref(), // ok b"\" 1-Feb-9999 12:34:56 +0000\"xxx", // ok b"\" 1-Feb-0000 12:34:56 -0000\"xxx", // ok b"\" 1-Feb-9999 12:34:56 -0000\"xxx", // ok b"\" 1-Feb-2020 00:00:00 +0100\"xxx", // ok b"\" 1-Feb-0000 12:34:56 +9999\"xxx", b"\" 1-Feb-9999 12:34:56 +9999\"xxx", b"\" 1-Feb-0000 12:34:56 -9999\"xxx", b"\" 1-Feb-9999 12:34:56 -9999\"xxx", b"\" 1-Feb-2020 99:99:99 +0100\"xxx", b"\"31-Feb-2020 00:00:00 +0100\"xxx", b"\"99-Feb-2020 99:99:99 +0100\"xxx", ]; for test in &tests[..5] { let (rem, datetime) = date_time(test).unwrap(); assert_eq!(rem, b"xxx"); println!("{} -> {:?}", from_utf8(test).unwrap(), datetime); } for test in &tests[5..] { assert!(date_time(test).is_err()); } } #[test] fn test_zone() { let (rem, val) = zone(b"+0000xxx").unwrap(); eprintln!("{:?}", val); assert_eq!(rem, b"xxx"); assert_eq!(val, FixedOffset::east_opt(0)); let (rem, val) = zone(b"+0000").unwrap(); eprintln!("{:?}", val); assert_eq!(rem, b""); assert_eq!(val, FixedOffset::east_opt(0)); let (rem, val) = zone(b"-0205xxx").unwrap(); eprintln!("{:?}", val); assert_eq!(rem, b"xxx"); assert_eq!(val, FixedOffset::west_opt(2 * 3600 + 5 * 60)); let (rem, val) = zone(b"-1159").unwrap(); eprintln!("{:?}", val); assert_eq!(rem, b""); assert_eq!(val, FixedOffset::west_opt(11 * 3600 + 59 * 60)); let (rem, val) = zone(b"-1159").unwrap(); eprintln!("{:?}", val); assert_eq!(rem, b""); assert_eq!(val, FixedOffset::west_opt(11 * 3600 + 59 * 60)); } } imap-codec-1.0.0/imap-codec/src/envelope.rs000066400000000000000000000137771447115025300205050ustar00rootroot00000000000000use abnf_core::streaming::sp; use imap_types::{ core::NString, envelope::{Address, Envelope}, }; use nom::{ branch::alt, bytes::streaming::tag, combinator::map, multi::many1, sequence::{delimited, tuple}, }; use crate::{ core::{nil, nstring}, decode::IMAPResult, }; /// ```abnf /// envelope = "(" /// env-date SP /// env-subject SP /// env-from SP /// env-sender SP /// env-reply-to SP /// env-to SP /// env-cc SP /// env-bcc SP /// env-in-reply-to SP /// env-message-id /// ")" /// ``` pub(crate) fn envelope(input: &[u8]) -> IMAPResult<&[u8], Envelope> { let mut parser = delimited( tag(b"("), tuple(( env_date, sp, env_subject, sp, env_from, sp, env_sender, sp, env_reply_to, sp, env_to, sp, env_cc, sp, env_bcc, sp, env_in_reply_to, sp, env_message_id, )), tag(b")"), ); let ( remaining, ( date, _, subject, _, from, _, sender, _, reply_to, _, to, _, cc, _, bcc, _, in_reply_to, _, message_id, ), ) = parser(input)?; Ok(( remaining, Envelope { date, subject, from, sender, reply_to, to, cc, bcc, in_reply_to, message_id, }, )) } #[inline] /// `env-date = nstring` pub(crate) fn env_date(input: &[u8]) -> IMAPResult<&[u8], NString> { nstring(input) } #[inline] /// `env-subject = nstring` pub(crate) fn env_subject(input: &[u8]) -> IMAPResult<&[u8], NString> { nstring(input) } /// `env-from = "(" 1*address ")" / nil` pub(crate) fn env_from(input: &[u8]) -> IMAPResult<&[u8], Vec
> { alt(( delimited(tag(b"("), many1(address), tag(b")")), map(nil, |_| Vec::new()), ))(input) } /// `env-sender = "(" 1*address ")" / nil` pub(crate) fn env_sender(input: &[u8]) -> IMAPResult<&[u8], Vec
> { alt(( delimited(tag(b"("), many1(address), tag(b")")), map(nil, |_| Vec::new()), ))(input) } /// `env-reply-to = "(" 1*address ")" / nil` pub(crate) fn env_reply_to(input: &[u8]) -> IMAPResult<&[u8], Vec
> { alt(( delimited(tag(b"("), many1(address), tag(b")")), map(nil, |_| Vec::new()), ))(input) } /// `env-to = "(" 1*address ")" / nil` pub(crate) fn env_to(input: &[u8]) -> IMAPResult<&[u8], Vec
> { alt(( delimited(tag(b"("), many1(address), tag(b")")), map(nil, |_| Vec::new()), ))(input) } /// `env-cc = "(" 1*address ")" / nil` pub(crate) fn env_cc(input: &[u8]) -> IMAPResult<&[u8], Vec
> { alt(( delimited(tag(b"("), many1(address), tag(b")")), map(nil, |_| Vec::new()), ))(input) } /// `env-bcc = "(" 1*address ")" / nil` pub(crate) fn env_bcc(input: &[u8]) -> IMAPResult<&[u8], Vec
> { alt(( delimited(tag(b"("), many1(address), tag(b")")), map(nil, |_| Vec::new()), ))(input) } #[inline] /// `env-in-reply-to = nstring` pub(crate) fn env_in_reply_to(input: &[u8]) -> IMAPResult<&[u8], NString> { nstring(input) } #[inline] /// `env-message-id = nstring` pub(crate) fn env_message_id(input: &[u8]) -> IMAPResult<&[u8], NString> { nstring(input) } /// `address = "(" /// addr-name SP /// addr-adl SP /// addr-mailbox SP /// addr-host /// ")"` pub(crate) fn address(input: &[u8]) -> IMAPResult<&[u8], Address> { let mut parser = delimited( tag(b"("), tuple((addr_name, sp, addr_adl, sp, addr_mailbox, sp, addr_host)), tag(b")"), ); let (remaining, (name, _, adl, _, mailbox, _, host)) = parser(input)?; Ok(( remaining, Address { name, adl, mailbox, host, }, )) } #[inline] /// `addr-name = nstring` /// /// If non-NIL, holds phrase from [RFC-2822] /// mailbox after removing [RFC-2822] quoting /// TODO(misuse): use `Phrase`? pub(crate) fn addr_name(input: &[u8]) -> IMAPResult<&[u8], NString> { nstring(input) } #[inline] /// `addr-adl = nstring` /// /// Holds route from [RFC-2822] route-addr if non-NIL /// TODO(misuse): use `Route`? pub(crate) fn addr_adl(input: &[u8]) -> IMAPResult<&[u8], NString> { nstring(input) } #[inline] /// `addr-mailbox = nstring` /// /// NIL indicates end of [RFC-2822] group; /// if non-NIL and addr-host is NIL, holds [RFC-2822] group name. /// Otherwise, holds [RFC-2822] local-part after removing [RFC-2822] quoting /// TODO(misuse): use `GroupName` or `LocalPart`? pub(crate) fn addr_mailbox(input: &[u8]) -> IMAPResult<&[u8], NString> { nstring(input) } #[inline] /// `addr-host = nstring` /// /// NIL indicates [RFC-2822] group syntax. /// Otherwise, holds [RFC-2822] domain name /// TODO(misuse): use `DomainName`? pub(crate) fn addr_host(input: &[u8]) -> IMAPResult<&[u8], NString> { nstring(input) } #[cfg(test)] mod tests { use imap_types::core::{IString, NString}; use super::*; #[test] fn test_parse_address() { let (rem, val) = address(b"(nil {3}\r\nxxx \"xxx\" nil)").unwrap(); assert_eq!( val, Address { name: NString(None), adl: NString(Some(IString::Literal( b"xxx".as_slice().try_into().unwrap() ))), mailbox: NString(Some(IString::Quoted("xxx".try_into().unwrap()))), host: NString(None), } ); assert_eq!(rem, b""); } } imap-codec-1.0.0/imap-codec/src/extensions.rs000066400000000000000000000001621447115025300210470ustar00rootroot00000000000000pub mod compress; pub mod enable; pub mod idle; pub mod literal; pub mod r#move; pub mod quota; pub mod unselect; imap-codec-1.0.0/imap-codec/src/extensions/000077500000000000000000000000001447115025300205025ustar00rootroot00000000000000imap-codec-1.0.0/imap-codec/src/extensions/compress.rs000066400000000000000000000051471447115025300227120ustar00rootroot00000000000000//! The IMAP COMPRESS Extension // Additional changes: // // command-auth =/ compress // capability =/ "COMPRESS=" algorithm // resp-text-code =/ "COMPRESSIONACTIVE" use std::io::Write; use imap_types::{command::CommandBody, extensions::compress::CompressionAlgorithm}; use nom::{ bytes::streaming::tag_no_case, combinator::{map, value}, sequence::preceded, }; use crate::{ decode::IMAPResult, encode::{EncodeContext, EncodeIntoContext}, }; /// `algorithm = "DEFLATE"` pub(crate) fn algorithm(input: &[u8]) -> IMAPResult<&[u8], CompressionAlgorithm> { value(CompressionAlgorithm::Deflate, tag_no_case("DEFLATE"))(input) } /// `compress = "COMPRESS" SP algorithm` pub(crate) fn compress(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { map(preceded(tag_no_case("COMPRESS "), algorithm), |algorithm| { CommandBody::Compress { algorithm } })(input) } impl EncodeIntoContext for CompressionAlgorithm { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { write!(ctx, "{}", self) } } #[cfg(test)] mod tests { use imap_types::command::{Command, CommandBody}; use super::*; use crate::testing::kat_inverse_command; #[test] fn test_parse_compress() { let tests = [ ( b"compress deflate ".as_ref(), Ok(( b" ".as_ref(), CommandBody::compress(CompressionAlgorithm::Deflate), )), ), (b"compress deflat ".as_ref(), Err(())), (b"compres deflate ".as_ref(), Err(())), (b"compress deflate ".as_ref(), Err(())), ]; for (test, expected) in tests { match expected { Ok((expected_rem, expected_object)) => { let (got_rem, got_object) = compress(test).unwrap(); assert_eq!(expected_object, got_object); assert_eq!(expected_rem, got_rem); } Err(_) => { assert!(compress(test).is_err()) } } } } #[test] fn test_kat_inverse_body_compress() { kat_inverse_command(&[ ( b"A COMPRESS DEFLATE\r\n".as_ref(), b"".as_ref(), Command::new("A", CommandBody::compress(CompressionAlgorithm::Deflate)).unwrap(), ), ( b"A COMPRESS DEFLATE\r\n?".as_ref(), b"?".as_ref(), Command::new("A", CommandBody::compress(CompressionAlgorithm::Deflate)).unwrap(), ), ]); } } imap-codec-1.0.0/imap-codec/src/extensions/enable.rs000066400000000000000000000064371447115025300223100ustar00rootroot00000000000000//! The IMAP ENABLE Extension // Additional changes: // // capability =/ "ENABLE" // command-any =/ "ENABLE" 1*(SP capability) // response-data =/ "*" SP enable-data CRLF use std::io::Write; use abnf_core::streaming::sp; use imap_types::{command::CommandBody, extensions::enable::CapabilityEnable, response::Data}; use nom::{ bytes::streaming::tag_no_case, combinator::map, multi::{many0, many1}, sequence::{preceded, tuple}, }; use crate::{ core::atom, decode::IMAPResult, encode::{EncodeContext, EncodeIntoContext}, }; /// `command-any =/ "ENABLE" 1*(SP capability)` /// /// Note: /// /// Introduced into imap-codec as ... /// /// ```text /// enable = "ENABLE" 1*(SP capability) /// /// command-any =/ enable /// ``` pub(crate) fn enable(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple(( tag_no_case("ENABLE"), many1(preceded(sp, capability_enable)), )); let (remaining, (_, capabilities)) = parser(input)?; Ok(( remaining, CommandBody::Enable { capabilities: capabilities.try_into().unwrap(), }, )) } pub(crate) fn capability_enable(input: &[u8]) -> IMAPResult<&[u8], CapabilityEnable> { map(atom, CapabilityEnable::from)(input) } /// `enable-data = "ENABLED" *(SP capability)` pub(crate) fn enable_data(input: &[u8]) -> IMAPResult<&[u8], Data> { let mut parser = tuple(( tag_no_case(b"ENABLED"), many0(preceded(sp, capability_enable)), )); let (remaining, (_, capabilities)) = parser(input)?; Ok((remaining, { Data::Enabled { capabilities } })) } impl<'a> EncodeIntoContext for CapabilityEnable<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { write!(ctx, "{}", self) } } #[cfg(test)] mod tests { use imap_types::{ command::Command, core::Atom, extensions::enable::{CapabilityEnable, Utf8Kind}, }; use super::*; use crate::testing::kat_inverse_command; #[test] fn test_parse_enable() { let got = enable(b"enable UTF8=ACCEPT\r\n").unwrap().1; assert_eq!( CommandBody::enable(vec![CapabilityEnable::Utf8(Utf8Kind::Accept)]).unwrap(), got ); } #[test] fn test_kat_inverse_command_enable() { kat_inverse_command(&[ ( b"A ENABLE UTF8=ONLY\r\n".as_ref(), b"".as_ref(), Command::new( "A", CommandBody::enable(vec![CapabilityEnable::Utf8(Utf8Kind::Only)]).unwrap(), ) .unwrap(), ), ( b"A ENABLE UTF8=ACCEPT\r\n?", b"?".as_ref(), Command::new( "A", CommandBody::enable(vec![CapabilityEnable::Utf8(Utf8Kind::Accept)]).unwrap(), ) .unwrap(), ), ( b"A ENABLE FOO\r\n??", b"??", Command::new( "A", CommandBody::enable(vec![CapabilityEnable::from( Atom::try_from("FOO").unwrap(), )]) .unwrap(), ) .unwrap(), ), ]); } } imap-codec-1.0.0/imap-codec/src/extensions/idle.rs000066400000000000000000000067471447115025300220030ustar00rootroot00000000000000//! IMAP4 IDLE command //! //! This extension enables the [`CommandBody::Idle`](crate::command::CommandBody#variant.Idle) variant. //! No additional types are used. // Additional changes: // // command_auth =/ idle use std::io::Write; #[cfg(not(feature = "quirk_crlf_relaxed"))] use abnf_core::streaming::crlf; #[cfg(feature = "quirk_crlf_relaxed")] use abnf_core::streaming::crlf_relaxed as crlf; use imap_types::{command::CommandBody, extensions::idle::IdleDone}; use nom::{bytes::streaming::tag_no_case, combinator::value, sequence::tuple}; use crate::{ decode::IMAPResult, encode::{EncodeContext, EncodeIntoContext}, }; /// `idle = "IDLE" CRLF "DONE"` (edited) /// /// ```text /// idle = "IDLE" CRLF "DONE" /// ^^^^^^^^^^^ /// | /// This is parsed here. /// CRLF is consumed in upper command parser. /// ``` /// /// Valid only in Authenticated or Selected state pub(crate) fn idle(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { value(CommandBody::Idle, tag_no_case("IDLE"))(input) } /// `idle = "IDLE" CRLF "DONE"` (edited) /// /// ```text /// idle = "IDLE" CRLF "DONE" CRLF /// ^^^^^^^^^^^ /// | /// This is parsed here. /// CRLF is additionally consumed in this parser. /// ``` /// /// Valid only in Authenticated or Selected state /// /// Note: This parser must be executed *instead* of the command parser /// when the server is in the IDLE state. pub(crate) fn idle_done(input: &[u8]) -> IMAPResult<&[u8], IdleDone> { value(IdleDone, tuple((tag_no_case("DONE"), crlf)))(input) } impl EncodeIntoContext for IdleDone { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { ctx.write_all(b"DONE\r\n") } } #[cfg(test)] mod tests { use imap_types::command::{Command, CommandBody}; use super::*; use crate::{ decode::{Decoder, IdleDoneDecodeError}, testing::kat_inverse_command, IdleDoneCodec, }; #[test] fn test_kat_inverse_command_idle() { kat_inverse_command(&[ ( b"A IDLE\r\n".as_ref(), b"".as_ref(), Command::new("A", CommandBody::Idle).unwrap(), ), ( b"A IDLE\r\n?", b"?", Command::new("A", CommandBody::Idle).unwrap(), ), ]); } #[test] fn test_decode_idle_done() { let tests = [ // Ok (b"done\r\n".as_ref(), Ok((b"".as_ref(), IdleDone))), (b"done\r\n?".as_ref(), Ok((b"?".as_ref(), IdleDone))), // Incomplete (b"d".as_ref(), Err(IdleDoneDecodeError::Incomplete)), (b"do".as_ref(), Err(IdleDoneDecodeError::Incomplete)), (b"don".as_ref(), Err(IdleDoneDecodeError::Incomplete)), (b"done".as_ref(), Err(IdleDoneDecodeError::Incomplete)), (b"done\r".as_ref(), Err(IdleDoneDecodeError::Incomplete)), // Failed (b"donee\r\n".as_ref(), Err(IdleDoneDecodeError::Failed)), (b" done\r\n".as_ref(), Err(IdleDoneDecodeError::Failed)), (b"done \r\n".as_ref(), Err(IdleDoneDecodeError::Failed)), (b" done \r\n".as_ref(), Err(IdleDoneDecodeError::Failed)), ]; for (test, expected) in tests { let got = IdleDoneCodec::default().decode(test); dbg!((std::str::from_utf8(test).unwrap(), &expected, &got)); assert_eq!(expected, got); } } } imap-codec-1.0.0/imap-codec/src/extensions/literal.rs000066400000000000000000000055641447115025300225160ustar00rootroot00000000000000#[cfg(test)] mod tests { use imap_types::{ command::{Command, CommandBody}, core::{Literal, NonEmptyVec}, response::{Capability, Code, Greeting}, }; use crate::testing::{kat_inverse_command, kat_inverse_greeting}; #[test] fn test_kat_inverse_command_login_literal_plus() { kat_inverse_command(&[ ( b"A LOGIN {0}\r\n {1}\r\nA\r\n".as_ref(), b"".as_ref(), Command::new( "A", CommandBody::login( Literal::try_from("").unwrap(), Literal::try_from("A").unwrap(), ) .unwrap(), ) .unwrap(), ), ( b"A LOGIN {1}\r\nA {2}\r\nAB\r\n?".as_ref(), b"?".as_ref(), Command::new( "A", CommandBody::login( Literal::try_from("A").unwrap(), Literal::try_from("AB").unwrap(), ) .unwrap(), ) .unwrap(), ), ( b"A LOGIN {0+}\r\n {1+}\r\nA\r\n??".as_ref(), b"??".as_ref(), Command::new( "A", CommandBody::login( Literal::try_from("").unwrap().into_non_sync(), Literal::try_from("A").unwrap().into_non_sync(), ) .unwrap(), ) .unwrap(), ), ( b"A LOGIN {1+}\r\nA {2+}\r\nAB\r\n???".as_ref(), b"???".as_ref(), Command::new( "A", CommandBody::login( Literal::try_from("A").unwrap().into_non_sync(), Literal::try_from("AB").unwrap().into_non_sync(), ) .unwrap(), ) .unwrap(), ), ]); } #[test] fn test_kat_inverse_greeting_capability_literal_plus() { kat_inverse_greeting(&[ ( b"* OK [CAPABILITY LITERAL+] ...\r\n".as_ref(), b"".as_ref(), Greeting::ok( Some(Code::Capability(NonEmptyVec::from(Capability::LiteralPlus))), "...", ) .unwrap(), ), ( b"* OK [CAPABILITY LITERAL-] ...\r\n?", b"?", Greeting::ok( Some(Code::Capability(NonEmptyVec::from( Capability::LiteralMinus, ))), "...", ) .unwrap(), ), ]); } } imap-codec-1.0.0/imap-codec/src/extensions/move.rs000066400000000000000000000027121447115025300220200ustar00rootroot00000000000000//! IMAP - MOVE Extension use abnf_core::streaming::sp; use imap_types::command::CommandBody; use nom::{bytes::streaming::tag_no_case, sequence::tuple}; use crate::{decode::IMAPResult, mailbox::mailbox, sequence::sequence_set}; /// ```abnf /// move = "MOVE" SP sequence-set SP mailbox /// ``` pub(crate) fn r#move(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case(b"MOVE"), sp, sequence_set, sp, mailbox)); let (remaining, (_, _, sequence_set, _, mailbox)) = parser(input)?; Ok(( remaining, CommandBody::Move { sequence_set, mailbox, uid: false, }, )) } #[cfg(test)] mod tests { use imap_types::command::{Command, CommandBody}; use crate::testing::kat_inverse_command; #[test] fn test_kat_inverse_command_move() { kat_inverse_command(&[ ( b"A MOVE 1 INBOX\r\n".as_ref(), b"".as_ref(), Command::new("A", CommandBody::r#move("1", "inBox", false).unwrap()).unwrap(), ), ( b"A UID MOVE 1 INBOX\r\n?", b"?", Command::new("A", CommandBody::r#move("1", "inBox", true).unwrap()).unwrap(), ), ( b"A MOVE 1:* test\r\n??", b"??", Command::new("A", CommandBody::r#move("1:*", "test", false).unwrap()).unwrap(), ), ]); } } imap-codec-1.0.0/imap-codec/src/extensions/quota.rs000066400000000000000000000464561447115025300222200ustar00rootroot00000000000000//! IMAP QUOTA Extension use std::io::Write; use abnf_core::streaming::sp; use imap_types::{ command::CommandBody, core::{AString, NonEmptyVec}, extensions::quota::{QuotaGet, QuotaSet, Resource}, response::Data, }; use nom::{ bytes::streaming::{tag, tag_no_case}, combinator::map, multi::{many0, separated_list0, separated_list1}, sequence::{delimited, preceded, tuple}, }; use crate::{ core::{astring, atom, number64}, decode::IMAPResult, encode::{EncodeContext, EncodeIntoContext}, mailbox::mailbox, }; /// ```abnf /// quota-root-name = astring /// ``` #[inline] pub(crate) fn quota_root_name(input: &[u8]) -> IMAPResult<&[u8], AString> { astring(input) } /// ```abnf /// getquota = "GETQUOTA" SP quota-root-name /// ``` #[inline] pub(crate) fn getquota(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case("GETQUOTA "), quota_root_name)); let (remaining, (_, root)) = parser(input)?; Ok((remaining, CommandBody::GetQuota { root })) } /// ```abnf /// getquotaroot = "GETQUOTAROOT" SP mailbox /// ``` pub(crate) fn getquotaroot(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple((tag_no_case("GETQUOTAROOT "), mailbox)); let (remaining, (_, mailbox)) = parser(input)?; Ok((remaining, CommandBody::GetQuotaRoot { mailbox })) } /// ```abnf /// quota-resource = resource-name SP /// resource-usage SP /// resource-limit /// /// resource-usage = number64 /// /// resource-limit = number64 /// ``` pub(crate) fn quota_resource(input: &[u8]) -> IMAPResult<&[u8], QuotaGet> { let mut parser = tuple((resource_name, sp, number64, sp, number64)); let (remaining, (resource, _, usage, _, limit)) = parser(input)?; Ok(( remaining, QuotaGet { resource, usage, limit, }, )) } /// ```abnf /// resource-name = "STORAGE" / /// "MESSAGE" / /// "MAILBOX" / /// "ANNOTATION-STORAGE" / /// resource-name-ext /// /// resource-name-ext = atom /// ``` pub(crate) fn resource_name(input: &[u8]) -> IMAPResult<&[u8], Resource> { map(atom, Resource::from)(input) } /// ```abnf /// quota-response = "QUOTA" SP quota-root-name SP quota-list /// /// quota-list = "(" quota-resource *(SP quota-resource) ")" /// ``` pub(crate) fn quota_response(input: &[u8]) -> IMAPResult<&[u8], Data> { let mut parser = tuple(( tag_no_case("QUOTA "), quota_root_name, sp, delimited(tag("("), separated_list1(sp, quota_resource), tag(")")), )); let (remaining, (_, root, _, quotas)) = parser(input)?; Ok(( remaining, Data::Quota { root, // Safety: Safe because we use `separated_list1` above. quotas: NonEmptyVec::try_from(quotas).unwrap(), }, )) } /// ```abnf /// quotaroot-response = "QUOTAROOT" SP mailbox *(SP quota-root-name) /// ``` pub(crate) fn quotaroot_response(input: &[u8]) -> IMAPResult<&[u8], Data> { let mut parser = tuple(( tag_no_case("QUOTAROOT "), mailbox, many0(preceded(sp, quota_root_name)), )); let (remaining, (_, mailbox, roots)) = parser(input)?; Ok((remaining, Data::QuotaRoot { mailbox, roots })) } /// ```abnf /// setquota = "SETQUOTA" SP quota-root-name SP setquota-list /// /// setquota-list = "(" [setquota-resource *(SP setquota-resource)] ")" /// ``` pub(crate) fn setquota(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple(( tag_no_case("SETQUOTA "), quota_root_name, sp, delimited(tag("("), separated_list0(sp, setquota_resource), tag(")")), )); let (remaining, (_, root, _, quotas)) = parser(input)?; Ok((remaining, CommandBody::SetQuota { root, quotas })) } /// ```abnf /// setquota-resource = resource-name SP resource-limit /// ``` pub(crate) fn setquota_resource(input: &[u8]) -> IMAPResult<&[u8], QuotaSet> { let mut parser = tuple((resource_name, sp, number64)); let (remaining, (resource, _, limit)) = parser(input)?; Ok((remaining, QuotaSet { resource, limit })) } // This had to be inlined into the `capability` parser because `CapabilityOther("QUOTAFOO")` would // be parsed as `Capability::Quota` plus an erroneous remainder. The `capability` parser eagerly consumes // an `atom` and tries to detect the variants later. // /// ```abnf // /// capability-quota = "QUOTASET" / capa-quota-res // /// ``` // /// // /// Note: Extended to ... // /// // /// ```abnf // /// capability-quota = "QUOTASET" / capa-quota-res / "QUOTA" // /// ``` // pub(crate) fn capability_quota(input: &[u8]) -> IMAPResult<&[u8], Capability> { // alt(( // value(Capability::QuotaSet, tag_no_case("QUOTASET")), // capa_quota_res, // value(Capability::Quota, tag_no_case("QUOTA")), // ))(input) // } // /// ```abnf // /// capa-quota-res = "QUOTA=RES-" resource-name // /// ``` // pub(crate) fn capa_quota_res(input: &[u8]) -> IMAPResult<&[u8], Capability> { // let mut parser = preceded(tag_no_case("QUOTA=RES-"), resource_name); // // let (remaining, resource) = parser(input)?; // // Ok((remaining, Capability::QuotaRes(resource))) // } impl<'a> EncodeIntoContext for Resource<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { ctx.write_all(self.to_string().as_bytes()) } } impl<'a> EncodeIntoContext for QuotaGet<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { self.resource.encode_ctx(ctx)?; write!(ctx, " {} {}", self.usage, self.limit) } } impl<'a> EncodeIntoContext for QuotaSet<'a> { fn encode_ctx(&self, ctx: &mut EncodeContext) -> std::io::Result<()> { self.resource.encode_ctx(ctx)?; write!(ctx, " {}", self.limit) } } #[cfg(test)] mod tests { use imap_types::{ command::{Command, CommandBody}, core::{IString, Tag}, extensions::quota::{QuotaGet, QuotaSet, Resource}, mailbox::Mailbox, response::{Capability, Code, Response, Status}, status::{StatusDataItem, StatusDataItemName}, }; use super::*; use crate::testing::{kat_inverse_command, kat_inverse_response}; #[test] fn test_parse_resource_name() { let tests = [ (b"stOragE ".as_ref(), Resource::Storage), (b"mesSaGe ".as_ref(), Resource::Message), (b"maIlbOx ".as_ref(), Resource::Mailbox), (b"anNotatIon-stoRage ".as_ref(), Resource::AnnotationStorage), ( b"anNotatIon-stoRageX ".as_ref(), Resource::try_from(b"anNotatIon-stoRageX".as_ref()).unwrap(), ), ( b"anNotatIon-stoRagee ".as_ref(), Resource::try_from(b"anNotatIon-stoRagee".as_ref()).unwrap(), ), ]; for (test, expected) in tests.iter() { let (rem, got) = resource_name(test).unwrap(); assert_eq!(*expected, got); assert_eq!(rem, b" "); } } #[test] fn test_kat_inverse_command_get_quota() { kat_inverse_command(&[ ( b"A GETQUOTA INBOX\r\n".as_ref(), b"".as_ref(), Command::new("A", CommandBody::get_quota("INBOX").unwrap()).unwrap(), ), ( b"A GETQUOTA \"\"\r\n?", b"?", Command::new("A", CommandBody::get_quota("").unwrap()).unwrap(), ), ( b"A003 GETQUOTA \"\"\r\n", b"", CommandBody::get_quota("").unwrap().tag("A003").unwrap(), ), ( b"G0001 GETQUOTA \"!partition/sda4\"\r\n", b"", CommandBody::get_quota(AString::String( IString::try_from("!partition/sda4").unwrap(), )) .unwrap() .tag("G0001") .unwrap(), ), ( b"S0000 GETQUOTA \"#user/alice\"\r\n", b"", CommandBody::get_quota(AString::String(IString::try_from("#user/alice").unwrap())) .unwrap() .tag("S0000") .unwrap(), ), ]); } #[test] fn test_kat_inverse_command_get_quota_root() { kat_inverse_command(&[ ( b"A003 GETQUOTAROOT INBOX\r\n".as_ref(), b"".as_ref(), CommandBody::get_quota_root(Mailbox::Inbox) .unwrap() .tag("A003") .unwrap(), ), ( b"A GETQUOTAROOT MAILBOX\r\n??", b"??", Command::new("A", CommandBody::get_quota_root("MAILBOX").unwrap()).unwrap(), ), ( b"G0002 GETQUOTAROOT INBOX\r\n", b"", CommandBody::get_quota_root("inbox") .unwrap() .tag("G0002") .unwrap(), ), ]); } #[test] fn test_kat_inverse_command_set_quota() { kat_inverse_command(&[ ( b"A SETQUOTA INBOX ()\r\n".as_ref(), b"".as_ref(), Command::new("A", CommandBody::set_quota("INBOX", vec![]).unwrap()).unwrap(), ), ( b"A SETQUOTA INBOX (STORAGE 256)\r\n", b"", Command::new("A", CommandBody::set_quota( "INBOX", vec![QuotaSet { resource: Resource::Storage, limit: 256, }], ) .unwrap()).unwrap(), ), ( b"A SETQUOTA INBOX (STORAGE 0 MESSAGE 512 MAILBOX 512 ANNOTATION-STORAGE 123 Foo 18446744073709551615)\r\n", b"", Command::new("A", CommandBody::set_quota( "INBOX", vec![ QuotaSet { resource: Resource::Storage, limit: 0, }, QuotaSet { resource: Resource::Message, limit: 512, }, QuotaSet { resource: Resource::Mailbox, limit: 512, }, QuotaSet { resource: Resource::AnnotationStorage, limit: 123, }, QuotaSet { resource: Resource::try_from("Foo").unwrap(), limit: u64::MAX, }, ], ) .unwrap()).unwrap(), ), ( b"S0001 SETQUOTA \"#user/alice\" (STORAGE 510)\r\n", b"", CommandBody::set_quota( AString::String(IString::try_from("#user/alice").unwrap()), vec![QuotaSet::new(Resource::Storage, 510)], ) .unwrap() .tag("S0001") .unwrap(), ), ( b"S0002 SETQUOTA \"!partition/sda4\" (STORAGE 99999999)\r\n", b"", CommandBody::set_quota( AString::String(IString::try_from("!partition/sda4").unwrap()), vec![QuotaSet::new(Resource::Storage, 99999999)], ) .unwrap() .tag("S0002") .unwrap(), ), ( b"A001 SETQUOTA \"\" (STORAGE 512)\r\n", b"", CommandBody::set_quota("", vec![QuotaSet::new(Resource::Storage, 512)]) .unwrap() .tag("A001") .unwrap(), ), ]); } #[test] fn test_kat_inverse_command_status_quota() { kat_inverse_command(&[( b"S0003 STATUS INBOX (MESSAGES DELETED DELETED-STORAGE)\r\n", b"", CommandBody::status( "inbox", vec![ StatusDataItemName::Messages, StatusDataItemName::Deleted, StatusDataItemName::DeletedStorage, ], ) .unwrap() .tag("S0003") .unwrap(), )]); } #[test] fn test_kat_inverse_response_data_quota() { kat_inverse_response(&[ ( b"* QUOTA INBOX (MESSAGE 1024 2048)\r\n".as_ref(), b"".as_ref(), Response::Data( Data::quota( "INBOX", vec![QuotaGet { resource: Resource::Message, usage: 1024, limit: 2048, }], ) .unwrap(), ), ), ( b"* QUOTAROOT INBOX\r\n", b"", Response::Data(Data::quota_root("INBOX", vec![]).unwrap()), ), ( b"* QUOTAROOT INBOX ROOT1 ROOT2\r\n", b"", Response::Data( Data::quota_root( "INBOX", vec!["ROOT1".try_into().unwrap(), "ROOT2".try_into().unwrap()], ) .unwrap(), ), ), ( b"* CAPABILITY QUOTA QUOTA=RES-STORAGE\r\n", b"", Response::Data( Data::capability(vec![ Capability::Quota, Capability::QuotaRes(Resource::Storage), ]) .unwrap(), ), ), ( b"* CAPABILITY QUOTA QUOTA=RES-STORAGE QUOTA=RES-MESSAGE\r\n", b"", Response::Data( Data::capability(vec![ Capability::Quota, Capability::QuotaRes(Resource::Storage), Capability::QuotaRes(Resource::Message), ]) .unwrap(), ), ), ( b"* CAPABILITY QUOTA QUOTASET QUOTA=RES-STORAGE QUOTA=RES-MESSAGE\r\n", b"", Response::Data( Data::capability(vec![ Capability::Quota, Capability::QuotaSet, Capability::QuotaRes(Resource::Storage), Capability::QuotaRes(Resource::Message), ]) .unwrap(), ), ), ( b"* QUOTA \"!partition/sda4\" (STORAGE 104 10923847)\r\n", b"", Response::Data( Data::quota( AString::String(IString::try_from("!partition/sda4").unwrap()), vec![QuotaGet::new(Resource::Storage, 104, 10923847)], ) .unwrap(), ), ), ( b"* QUOTA \"\" (STORAGE 10 512)\r\n", b"", Response::Data(Data::Quota { root: "".try_into().unwrap(), quotas: vec![QuotaGet::new(Resource::Storage, 10, 512)] .try_into() .unwrap(), }), ), ( b"* QUOTA \"#user/alice\" (MESSAGE 42 1000)\r\n", b"", Response::Data(Data::Quota { root: AString::String(IString::try_from("#user/alice").unwrap()), quotas: vec![QuotaGet::new(Resource::Message, 42, 1000)] .try_into() .unwrap(), }), ), ( b"* QUOTA \"#user/alice\" (STORAGE 54 111 MESSAGE 42 1000)\r\n", b"", Response::Data( Data::quota( AString::String(IString::try_from("#user/alice").unwrap()), vec![ QuotaGet::new(Resource::Storage, 54, 111), QuotaGet::new(Resource::Message, 42, 1000), ], ) .unwrap(), ), ), ( b"* QUOTA \"#user/alice\" (STORAGE 58 512)\r\n", b"", Response::Data( Data::quota( AString::String(IString::try_from("#user/alice").unwrap()), vec![QuotaGet::new(Resource::Storage, 58, 512)], ) .unwrap(), ), ), ( b"* QUOTAROOT INBOX \"\"\r\n", b"", Response::Data(Data::QuotaRoot { mailbox: Mailbox::Inbox, roots: vec!["".try_into().unwrap()], }), ), ( b"* QUOTAROOT comp.mail.mime\r\n", b"", Response::Data(Data::QuotaRoot { mailbox: Mailbox::try_from("comp.mail.mime").unwrap(), roots: vec![], }), ), ( b"* QUOTAROOT INBOX \"#user/alice\" \"!partition/sda4\"\r\n", b"", Response::Data(Data::QuotaRoot { mailbox: Mailbox::try_from("inbox").unwrap(), roots: vec![ AString::String(IString::try_from("#user/alice").unwrap()), AString::String(IString::try_from("!partition/sda4").unwrap()), ], }), ), ( b"* STATUS INBOX (MESSAGES 12 DELETED 4 DELETED-STORAGE 8)\r\n", b"", Response::Data(Data::Status { mailbox: Mailbox::Inbox, items: vec![ StatusDataItem::Messages(12), StatusDataItem::Deleted(4), StatusDataItem::DeletedStorage(8), ] .into(), }), ), ( b"* NO [OVERQUOTA] Soft quota has been exceeded\r\n", b"", Response::Status( Status::no(None, Some(Code::OverQuota), "Soft quota has been exceeded") .unwrap(), ), ), ( b"A003 NO [OVERQUOTA] APPEND Failed\r\n", b"".as_ref(), Response::Status( Status::no( Some(Tag::try_from("A003").unwrap()), Some(Code::OverQuota), "APPEND Failed", ) .unwrap(), ), ), ]); } } imap-codec-1.0.0/imap-codec/src/extensions/unselect.rs000066400000000000000000000005561447115025300227000ustar00rootroot00000000000000#[cfg(test)] mod tests { use imap_types::command::{Command, CommandBody}; use crate::testing::kat_inverse_command; #[test] fn test_kat_inverse_command_unselect() { kat_inverse_command(&[( b"A UNSELECT\r\n".as_ref(), b"".as_ref(), Command::new("A", CommandBody::unselect()).unwrap(), )]); } } imap-codec-1.0.0/imap-codec/src/fetch.rs000066400000000000000000000407201447115025300177450ustar00rootroot00000000000000use std::num::NonZeroU32; use abnf_core::streaming::sp; use imap_types::{ core::{AString, NonEmptyVec}, fetch::{MessageDataItem, MessageDataItemName, Part, PartSpecifier, Section}, }; use nom::{ branch::alt, bytes::streaming::{tag, tag_no_case}, combinator::{map, opt, value}, multi::separated_list1, sequence::{delimited, tuple}, }; use crate::{ body::body, core::{astring, nstring, number, nz_number}, datetime::date_time, decode::IMAPResult, envelope::envelope, flag::flag_fetch, }; /// `fetch-att = "ENVELOPE" / /// "FLAGS" / /// "INTERNALDATE" / /// "RFC822" [".HEADER" / ".SIZE" / ".TEXT"] / /// "BODY" ["STRUCTURE"] / /// "UID" / /// "BODY" section ["<" number "." nz-number ">"] / /// "BODY.PEEK" section ["<" number "." nz-number ">"]` pub(crate) fn fetch_att(input: &[u8]) -> IMAPResult<&[u8], MessageDataItemName> { alt(( value(MessageDataItemName::Envelope, tag_no_case(b"ENVELOPE")), value(MessageDataItemName::Flags, tag_no_case(b"FLAGS")), value( MessageDataItemName::InternalDate, tag_no_case(b"INTERNALDATE"), ), value( MessageDataItemName::BodyStructure, tag_no_case(b"BODYSTRUCTURE"), ), map( tuple(( tag_no_case(b"BODY.PEEK"), section, opt(delimited( tag(b"<"), tuple((number, tag(b"."), nz_number)), tag(b">"), )), )), |(_, section, byterange)| MessageDataItemName::BodyExt { section, partial: byterange.map(|(start, _, end)| (start, end)), peek: true, }, ), map( tuple(( tag_no_case(b"BODY"), section, opt(delimited( tag(b"<"), tuple((number, tag(b"."), nz_number)), tag(b">"), )), )), |(_, section, byterange)| MessageDataItemName::BodyExt { section, partial: byterange.map(|(start, _, end)| (start, end)), peek: false, }, ), value(MessageDataItemName::Body, tag_no_case(b"BODY")), value(MessageDataItemName::Uid, tag_no_case(b"UID")), value( MessageDataItemName::Rfc822Header, tag_no_case(b"RFC822.HEADER"), ), value(MessageDataItemName::Rfc822Size, tag_no_case(b"RFC822.SIZE")), value(MessageDataItemName::Rfc822Text, tag_no_case(b"RFC822.TEXT")), value(MessageDataItemName::Rfc822, tag_no_case(b"RFC822")), ))(input) } /// `msg-att = "(" /// (msg-att-dynamic / msg-att-static) *(SP (msg-att-dynamic / msg-att-static)) /// ")"` pub(crate) fn msg_att(input: &[u8]) -> IMAPResult<&[u8], NonEmptyVec> { delimited( tag(b"("), map( separated_list1(sp, alt((msg_att_dynamic, msg_att_static))), NonEmptyVec::unvalidated, ), tag(b")"), )(input) } /// `msg-att-dynamic = "FLAGS" SP "(" [flag-fetch *(SP flag-fetch)] ")"` /// /// Note: MAY change for a message pub(crate) fn msg_att_dynamic(input: &[u8]) -> IMAPResult<&[u8], MessageDataItem> { let mut parser = tuple(( tag_no_case(b"FLAGS"), sp, delimited(tag(b"("), opt(separated_list1(sp, flag_fetch)), tag(b")")), )); let (remaining, (_, _, flags)) = parser(input)?; Ok((remaining, MessageDataItem::Flags(flags.unwrap_or_default()))) } /// `msg-att-static = "ENVELOPE" SP envelope / /// "INTERNALDATE" SP date-time / /// "RFC822" [".HEADER" / ".TEXT"] SP nstring / /// "RFC822.SIZE" SP number / /// "BODY" ["STRUCTURE"] SP body / /// "BODY" section ["<" number ">"] SP nstring / /// "UID" SP uniqueid` /// /// Note: MUST NOT change for a message pub(crate) fn msg_att_static(input: &[u8]) -> IMAPResult<&[u8], MessageDataItem> { alt(( map( tuple((tag_no_case(b"ENVELOPE"), sp, envelope)), |(_, _, envelope)| MessageDataItem::Envelope(envelope), ), map( tuple((tag_no_case(b"INTERNALDATE"), sp, date_time)), |(_, _, date_time)| MessageDataItem::InternalDate(date_time), ), map( tuple((tag_no_case(b"RFC822.HEADER"), sp, nstring)), |(_, _, nstring)| MessageDataItem::Rfc822Header(nstring), ), map( tuple((tag_no_case(b"RFC822.TEXT"), sp, nstring)), |(_, _, nstring)| MessageDataItem::Rfc822Text(nstring), ), map( tuple((tag_no_case(b"RFC822.SIZE"), sp, number)), |(_, _, num)| MessageDataItem::Rfc822Size(num), ), map( tuple((tag_no_case(b"RFC822"), sp, nstring)), |(_, _, nstring)| MessageDataItem::Rfc822(nstring), ), map( tuple((tag_no_case(b"BODYSTRUCTURE"), sp, body(8))), |(_, _, body)| MessageDataItem::BodyStructure(body), ), map( tuple((tag_no_case(b"BODY"), sp, body(8))), |(_, _, body)| MessageDataItem::Body(body), ), map( tuple(( tag_no_case(b"BODY"), section, opt(delimited(tag(b"<"), number, tag(b">"))), sp, nstring, )), |(_, section, origin, _, data)| MessageDataItem::BodyExt { section, origin, data, }, ), map(tuple((tag_no_case(b"UID"), sp, uniqueid)), |(_, _, uid)| { MessageDataItem::Uid(uid) }), ))(input) } #[inline] /// `uniqueid = nz-number` /// /// Note: Strictly ascending pub(crate) fn uniqueid(input: &[u8]) -> IMAPResult<&[u8], NonZeroU32> { nz_number(input) } /// `section = "[" [section-spec] "]"` pub(crate) fn section(input: &[u8]) -> IMAPResult<&[u8], Option
> { delimited(tag(b"["), opt(section_spec), tag(b"]"))(input) } /// `section-spec = section-msgtext / (section-part ["." section-text])` pub(crate) fn section_spec(input: &[u8]) -> IMAPResult<&[u8], Section> { alt(( map(section_msgtext, |part_specifier| match part_specifier { PartSpecifier::PartNumber(_) => unreachable!(), PartSpecifier::Header => Section::Header(None), PartSpecifier::HeaderFields(fields) => Section::HeaderFields(None, fields), PartSpecifier::HeaderFieldsNot(fields) => Section::HeaderFieldsNot(None, fields), PartSpecifier::Text => Section::Text(None), PartSpecifier::Mime => unreachable!(), }), map( tuple((section_part, opt(tuple((tag(b"."), section_text))))), |(part_number, maybe_part_specifier)| { if let Some((_, part_specifier)) = maybe_part_specifier { match part_specifier { PartSpecifier::PartNumber(_) => unreachable!(), PartSpecifier::Header => Section::Header(Some(Part(part_number))), PartSpecifier::HeaderFields(fields) => { Section::HeaderFields(Some(Part(part_number)), fields) } PartSpecifier::HeaderFieldsNot(fields) => { Section::HeaderFieldsNot(Some(Part(part_number)), fields) } PartSpecifier::Text => Section::Text(Some(Part(part_number))), PartSpecifier::Mime => Section::Mime(Part(part_number)), } } else { Section::Part(Part(part_number)) } }, ), ))(input) } /// `section-msgtext = "HEADER" / "HEADER.FIELDS" [".NOT"] SP header-list / "TEXT"` /// /// Top-level or MESSAGE/RFC822 part pub(crate) fn section_msgtext(input: &[u8]) -> IMAPResult<&[u8], PartSpecifier> { alt(( map( tuple((tag_no_case(b"HEADER.FIELDS.NOT"), sp, header_list)), |(_, _, header_list)| PartSpecifier::HeaderFieldsNot(header_list), ), map( tuple((tag_no_case(b"HEADER.FIELDS"), sp, header_list)), |(_, _, header_list)| PartSpecifier::HeaderFields(header_list), ), value(PartSpecifier::Header, tag_no_case(b"HEADER")), value(PartSpecifier::Text, tag_no_case(b"TEXT")), ))(input) } #[inline] /// `section-part = nz-number *("." nz-number)` /// /// Body part nesting pub(crate) fn section_part(input: &[u8]) -> IMAPResult<&[u8], NonEmptyVec> { map( separated_list1(tag(b"."), nz_number), NonEmptyVec::unvalidated, )(input) } /// `section-text = section-msgtext / "MIME"` /// /// Text other than actual body part (headers, etc.) pub(crate) fn section_text(input: &[u8]) -> IMAPResult<&[u8], PartSpecifier> { alt(( section_msgtext, value(PartSpecifier::Mime, tag_no_case(b"MIME")), ))(input) } /// `header-list = "(" header-fld-name *(SP header-fld-name) ")"` pub(crate) fn header_list(input: &[u8]) -> IMAPResult<&[u8], NonEmptyVec> { map( delimited(tag(b"("), separated_list1(sp, header_fld_name), tag(b")")), NonEmptyVec::unvalidated, )(input) } #[inline] /// `header-fld-name = astring` pub(crate) fn header_fld_name(input: &[u8]) -> IMAPResult<&[u8], AString> { astring(input) } #[cfg(test)] mod tests { use imap_types::{ body::{BasicFields, Body, BodyStructure, SpecificFields}, core::{IString, NString}, datetime::DateTime, envelope::Envelope, }; use super::*; use crate::testing::known_answer_test_encode; #[test] fn test_encode_message_data_item_name() { let tests = [ (MessageDataItemName::Body, b"BODY".as_ref()), ( MessageDataItemName::BodyExt { section: None, partial: None, peek: false, }, b"BODY[]", ), (MessageDataItemName::BodyStructure, b"BODYSTRUCTURE"), (MessageDataItemName::Envelope, b"ENVELOPE"), (MessageDataItemName::Flags, b"FLAGS"), (MessageDataItemName::InternalDate, b"INTERNALDATE"), (MessageDataItemName::Rfc822, b"RFC822"), (MessageDataItemName::Rfc822Header, b"RFC822.HEADER"), (MessageDataItemName::Rfc822Size, b"RFC822.SIZE"), (MessageDataItemName::Rfc822Text, b"RFC822.TEXT"), (MessageDataItemName::Uid, b"UID"), ]; for test in tests { known_answer_test_encode(test); } } #[test] fn test_encode_message_data_item() { let tests = [ ( MessageDataItem::Body(BodyStructure::Single { body: Body { basic: BasicFields { parameter_list: vec![], id: NString(None), description: NString(None), content_transfer_encoding: IString::try_from("base64").unwrap(), size: 42, }, specific: SpecificFields::Text { subtype: IString::try_from("foo").unwrap(), number_of_lines: 1337, }, }, extension_data: None, }), b"BODY (\"TEXT\" \"foo\" NIL NIL NIL \"base64\" 42 1337)".as_ref(), ), ( MessageDataItem::BodyExt { section: None, origin: None, data: NString(None), }, b"BODY[] NIL", ), ( MessageDataItem::BodyExt { section: None, origin: Some(123), data: NString(None), }, b"BODY[]<123> NIL", ), ( MessageDataItem::BodyStructure(BodyStructure::Single { body: Body { basic: BasicFields { parameter_list: vec![], id: NString(None), description: NString(None), content_transfer_encoding: IString::try_from("base64").unwrap(), size: 213, }, specific: SpecificFields::Text { subtype: IString::try_from("").unwrap(), number_of_lines: 224, }, }, extension_data: None, }), b"BODYSTRUCTURE (\"TEXT\" \"\" NIL NIL NIL \"base64\" 213 224)", ), ( MessageDataItem::Envelope(Envelope { date: NString(None), subject: NString(None), from: vec![], sender: vec![], reply_to: vec![], to: vec![], cc: vec![], bcc: vec![], in_reply_to: NString(None), message_id: NString(None), }), b"ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)", ), (MessageDataItem::Flags(vec![]), b"FLAGS ()"), ( MessageDataItem::InternalDate( DateTime::try_from( chrono::DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 10:52:37 +0200") .unwrap(), ) .unwrap(), ), b"INTERNALDATE \"01-Jul-2003 10:52:37 +0200\"", ), (MessageDataItem::Rfc822(NString(None)), b"RFC822 NIL"), ( MessageDataItem::Rfc822Header(NString(None)), b"RFC822.HEADER NIL", ), (MessageDataItem::Rfc822Size(3456), b"RFC822.SIZE 3456"), ( MessageDataItem::Rfc822Text(NString(None)), b"RFC822.TEXT NIL", ), ( MessageDataItem::Uid(NonZeroU32::try_from(u32::MAX).unwrap()), b"UID 4294967295", ), ]; for test in tests { known_answer_test_encode(test); } } #[test] fn test_encode_section() { let tests = [ ( Section::Part(Part(NonEmptyVec::from(NonZeroU32::try_from(1).unwrap()))), b"1".as_ref(), ), (Section::Header(None), b"HEADER"), ( Section::Header(Some(Part(NonEmptyVec::from( NonZeroU32::try_from(1).unwrap(), )))), b"1.HEADER", ), ( Section::HeaderFields(None, NonEmptyVec::from(AString::try_from("").unwrap())), b"HEADER.FIELDS (\"\")", ), ( Section::HeaderFields( Some(Part(NonEmptyVec::from(NonZeroU32::try_from(1).unwrap()))), NonEmptyVec::from(AString::try_from("").unwrap()), ), b"1.HEADER.FIELDS (\"\")", ), ( Section::HeaderFieldsNot(None, NonEmptyVec::from(AString::try_from("").unwrap())), b"HEADER.FIELDS.NOT (\"\")", ), ( Section::HeaderFieldsNot( Some(Part(NonEmptyVec::from(NonZeroU32::try_from(1).unwrap()))), NonEmptyVec::from(AString::try_from("").unwrap()), ), b"1.HEADER.FIELDS.NOT (\"\")", ), (Section::Text(None), b"TEXT"), ( Section::Text(Some(Part(NonEmptyVec::from( NonZeroU32::try_from(1).unwrap(), )))), b"1.TEXT", ), ( Section::Mime(Part(NonEmptyVec::from(NonZeroU32::try_from(1).unwrap()))), b"1.MIME", ), ]; for test in tests { known_answer_test_encode(test) } } } imap-codec-1.0.0/imap-codec/src/flag.rs000066400000000000000000000133321447115025300175640ustar00rootroot00000000000000use abnf_core::streaming::sp; use imap_types::flag::{Flag, FlagFetch, FlagNameAttribute, FlagPerm}; use nom::{ branch::alt, bytes::streaming::tag, character::streaming::char, combinator::{map, recognize, value}, multi::{separated_list0, separated_list1}, sequence::{delimited, preceded, tuple}, }; use crate::{core::atom, decode::IMAPResult}; /// ```abnf /// flag = "\Answered" / /// "\Flagged" / /// "\Deleted" / /// "\Seen" / /// "\Draft" / /// flag-keyword / /// flag-extension /// ``` /// /// Note: Does not include "\Recent" pub(crate) fn flag(input: &[u8]) -> IMAPResult<&[u8], Flag> { alt(( map(preceded(char('\\'), atom), Flag::system), map(atom, Flag::Keyword), ))(input) } // Note(duesee): This was inlined into [`flag`]. // #[inline] // /// `flag-keyword = atom` // pub(crate) fn flag_keyword(input: &[u8]) -> IMAPResult<&[u8], Flag> { // map(atom, Flag::Keyword)(input) // } // Note: This was inlined into `mbx_list_flags`. // /// ```abnf // /// flag-extension = "\" atom // /// ``` // /// // /// Future expansion. // /// // /// Client implementations MUST accept flag-extension flags. // /// Server implementations MUST NOT generate flag-extension flags // /// except as defined by future standard or standards-track revisions of this specification. // pub(crate) fn flag_extension(input: &[u8]) -> IMAPResult<&[u8], Atom> { // preceded(tag(b"\\"), atom)(input) // } /// `flag-list = "(" [flag *(SP flag)] ")"` pub(crate) fn flag_list(input: &[u8]) -> IMAPResult<&[u8], Vec> { delimited(tag(b"("), separated_list0(sp, flag), tag(b")"))(input) } /// `flag-fetch = flag / "\Recent"` pub(crate) fn flag_fetch(input: &[u8]) -> IMAPResult<&[u8], FlagFetch> { if let Ok((rem, peek)) = recognize(tuple((char('\\'), atom)))(input) { if peek.to_ascii_lowercase() == b"\\recent" { return Ok((rem, FlagFetch::Recent)); } } map(flag, FlagFetch::Flag)(input) } /// `flag-perm = flag / "\*"` pub(crate) fn flag_perm(input: &[u8]) -> IMAPResult<&[u8], FlagPerm> { alt(( value(FlagPerm::Asterisk, tag("\\*")), map(flag, FlagPerm::Flag), ))(input) } /// ```abnf /// mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag *(SP mbx-list-oflag) / /// mbx-list-oflag *(SP mbx-list-oflag) /// ``` /// /// TODO(#155): ABNF enforces that sflag is not used more than once. /// We could parse any flag and check for multiple occurrences of sflag later. pub(crate) fn mbx_list_flags(input: &[u8]) -> IMAPResult<&[u8], Vec> { let (remaining, flags) = separated_list1(sp, map(preceded(char('\\'), atom), FlagNameAttribute::from))(input)?; // TODO(#155): Do we really want to enforce this? // let sflag_count = flags // .iter() // .filter(|&flag| FlagNameAttribute::is_selectability(flag)) // .count(); // // if sflag_count > 1 { // return Err(nom::Err::Failure(nom::error::make_error( // input, // nom::error::ErrorKind::Verify, // ))); // } Ok((remaining, flags)) } // Note: This was inlined into `mbx_list_flags`. // /// ```abnf // /// mbx-list-oflag = "\Noinferiors" / flag-extension // /// ``` // /// // /// Other flags; multiple possible per LIST response // pub(crate) fn mbx_list_oflag(input: &[u8]) -> IMAPResult<&[u8], FlagNameAttribute> { // alt(( // value( // FlagNameAttribute::Noinferiors, // tag_no_case(b"\\Noinferiors"), // ), // map(flag_extension, FlagNameAttribute::Extension), // ))(input) // } // Note: This was inlined into `mbx_list_flags`. // /// ```abnf // /// mbx-list-sflag = "\Noselect" / "\Marked" / "\Unmarked" // /// ``` // /// // /// Selectability flags; only one per LIST response // pub(crate) fn mbx_list_sflag(input: &[u8]) -> IMAPResult<&[u8], FlagNameAttribute> { // alt(( // value(FlagNameAttribute::Noselect, tag_no_case(b"\\Noselect")), // value(FlagNameAttribute::Marked, tag_no_case(b"\\Marked")), // value(FlagNameAttribute::Unmarked, tag_no_case(b"\\Unmarked")), // ))(input) // } #[cfg(test)] mod tests { use imap_types::{ core::Atom, flag::{Flag, FlagFetch, FlagNameAttribute, FlagPerm}, }; use super::*; #[test] fn test_parse_flag_fetch() { let tests = [( "iS)", FlagFetch::Flag(Flag::Keyword(Atom::try_from("iS").unwrap())), )]; for (test, expected) in tests { let (rem, got) = flag_fetch(test.as_bytes()).unwrap(); assert_eq!(rem.len(), 1); assert_eq!(expected, got); } } #[test] fn test_parse_flag_perm() { let tests = [ ("\\Deleted)", FlagPerm::Flag(Flag::Deleted)), ( "\\Deletedx)", FlagPerm::Flag(Flag::system(Atom::try_from("Deletedx").unwrap())), ), ("\\Seen ", FlagPerm::Flag(Flag::Seen)), ("\\*)", FlagPerm::Asterisk), ]; for (test, expected) in tests { let (rem, got) = flag_perm(test.as_bytes()).unwrap(); assert_eq!(rem.len(), 1); assert_eq!(expected, got); } } #[test] fn test_parse_mbx_list_flags() { let tests = [ ( "\\Markedm)", vec![FlagNameAttribute::from(Atom::try_from("Markedm").unwrap())], ), ("\\Marked)", vec![FlagNameAttribute::Marked]), ]; for (test, expected) in tests { let (rem, got) = mbx_list_flags(test.as_bytes()).unwrap(); assert_eq!(expected, got); assert_eq!(rem.len(), 1); } } } imap-codec-1.0.0/imap-codec/src/lib.rs000066400000000000000000000106401447115025300174200ustar00rootroot00000000000000//! # IMAP protocol library //! //! imap-codec provides complete and detailed parsing and construction of [IMAP4rev1] commands and responses. //! It is based on [imap-types] and extends it with parsing support using [nom]. //! //! The main codecs are //! [`GreetingCodec`](crate::GreetingCodec) (to parse the first message from a server), //! [`CommandCodec`](crate::CommandCodec) (to parse commands from a client), and //! [`ResponseCodec`](crate::ResponseCodec) (to parse responses or results from a server). //! //! Note that IMAP traces are not guaranteed to be UTF-8. //! Thus, be careful when using code like `from_utf8(...)`. //! //! ## Decoding //! //! Decoding is provided through the [`Decoder`](`crate::decode::Decoder`) trait. //! Every parser takes an input (`&[u8]`) and produces a remainder and a parsed value. //! //! **Note:** Decoding IMAP traces is more elaborate than it seems on a first glance. //! Please consult the [`decode`](`crate::decode`) module documentation to learn how to handle real-world decoding. //! //! ### Example //! //! ```rust //! # use imap_codec::{ //! # decode::Decoder, //! # imap_types::{ //! # core::Text, //! # response::{Code, Greeting, GreetingKind}, //! # }, //! # GreetingCodec, //! # }; //! let (remaining, greeting) = GreetingCodec::default() //! .decode(b"* OK [ALERT] Hello, World!\r\n") //! .unwrap(); //! //! assert_eq!( //! greeting, //! Greeting { //! kind: GreetingKind::Ok, //! code: Some(Code::Alert), //! text: Text::try_from("Hello, World!").unwrap(), //! } //! ); //! assert_eq!(remaining, &b""[..]) //! ``` //! //! ## Encoding //! //! Encoding is provided through the [`Encoder`](`crate::encode::Encoder`) trait. //! //! **Note:** Encoding IMAP traces is more elaborate than it seems on a first glance. //! Please consult the [`encode`](`crate::encode`) module documentation to learn how to handle real-world encoding. //! //! ### Example //! //! ```rust //! # use imap_codec::{ //! # encode::Encoder, //! # imap_types::{ //! # core::Text, //! # response::{Code, Greeting, GreetingKind}, //! # }, //! # GreetingCodec, //! # }; //! let greeting = Greeting { //! kind: GreetingKind::Ok, //! code: Some(Code::Alert), //! text: Text::try_from("Hello, World!").unwrap(), //! }; //! //! let bytes = GreetingCodec::default().encode(&greeting).dump(); //! //! assert_eq!(bytes, &b"* OK [ALERT] Hello, World!\r\n"[..]); //! ``` //! //! ## Features //! //! imap-codec forwards many features to imap-types. See [imap-types features] for a comprehensive list. //! //! In addition, imap-codec defines the following features: //! //! | Feature | Description | Enabled by default | //! |-----------------------|--------------------------------|--------------------| //! | quirk_crlf_relaxed | Make `\r` in `\r\n` optional. | No | //! | quirk_rectify_numbers | Rectify (invalid) numbers. | No | //! | quirk_missing_text | Rectify missing `text` element.| No | //! //! ## Quirks //! //! Features starting with `quirk_` are used to cope with existing interoperability issues. //! Unfortunately, we already observed some standard violations, such as, negative numbers, and missing syntax elements. //! Our policy is as follows: If we see an interoperability issue, we file an issue in the corresponding implementation. //! If, for any reason, the issue cannot be fixed, *and* the implementation is "important enough", e.g., because a user of //! imap-codec can't otherwise access their emails, we may add a `quirk_` feature to quickly resolve the problem. //! Of course, imap-codec should never violate the IMAP standard itself. So, we need to do this carefully. //! //! [imap-types]: https://docs.rs/imap-types/latest/imap_types //! [imap-types features]: https://docs.rs/imap-types/latest/imap_types/#features //! [IMAP4rev1]: https://tools.ietf.org/html/rfc3501 //! [parse_command]: https://github.com/duesee/imap-codec/blob/main/examples/parse_command.rs #![forbid(unsafe_code)] #![deny(missing_debug_implementations)] #![cfg_attr(docsrs, feature(doc_cfg))] mod auth; mod body; mod codec; mod command; mod core; mod datetime; mod envelope; mod extensions; mod fetch; mod flag; mod mailbox; mod response; mod search; mod sequence; mod status; #[cfg(test)] mod testing; pub use codec::*; // Re-export. pub use imap_types; imap-codec-1.0.0/imap-codec/src/mailbox.rs000066400000000000000000000113061447115025300203050ustar00rootroot00000000000000use abnf_core::streaming::{dquote, sp}; use imap_types::{ core::QuotedChar, flag::FlagNameAttribute, mailbox::{ListCharString, ListMailbox, Mailbox}, response::Data, utils::indicators::is_list_char, }; use nom::{ branch::alt, bytes::streaming::{tag, tag_no_case, take_while1}, combinator::{map, opt, value}, multi::many0, sequence::{delimited, preceded, tuple}, }; use crate::{ core::{astring, nil, number, nz_number, quoted_char, string}, decode::IMAPResult, extensions::quota::{quota_response, quotaroot_response}, flag::{flag_list, mbx_list_flags}, status::status_att_list, }; /// `list-mailbox = 1*list-char / string` pub(crate) fn list_mailbox(input: &[u8]) -> IMAPResult<&[u8], ListMailbox> { alt(( map(take_while1(is_list_char), |bytes: &[u8]| { // # Safety // // `unwrap` is safe here, because `is_list_char` enforces that the bytes ... // * contain ASCII-only characters, i.e., `from_utf8` will return `Ok`. // * are valid according to `ListCharString::verify()`, i.e., `unvalidated` is safe. ListMailbox::Token(ListCharString::unvalidated( std::str::from_utf8(bytes).unwrap(), )) }), map(string, ListMailbox::String), ))(input) } /// `mailbox = "INBOX" / astring` /// /// INBOX is case-insensitive. All case variants of INBOX (e.g., "iNbOx") /// MUST be interpreted as INBOX not as an astring. /// /// An astring which consists of the case-insensitive sequence /// "I" "N" "B" "O" "X" is considered to be INBOX and not an astring. /// /// Refer to section 5.1 for further semantic details of mailbox names. pub(crate) fn mailbox(input: &[u8]) -> IMAPResult<&[u8], Mailbox> { map(astring, Mailbox::from)(input) } /// `mailbox-data = "FLAGS" SP flag-list / /// "LIST" SP mailbox-list / /// "LSUB" SP mailbox-list / /// "SEARCH" *(SP nz-number) / /// "STATUS" SP mailbox SP "(" [status-att-list] ")" / /// number SP "EXISTS" / /// number SP "RECENT"` pub(crate) fn mailbox_data(input: &[u8]) -> IMAPResult<&[u8], Data> { alt(( map( tuple((tag_no_case(b"FLAGS"), sp, flag_list)), |(_, _, flags)| Data::Flags(flags), ), map( tuple((tag_no_case(b"LIST"), sp, mailbox_list)), |(_, _, (items, delimiter, mailbox))| Data::List { items: items.unwrap_or_default(), mailbox, delimiter, }, ), map( tuple((tag_no_case(b"LSUB"), sp, mailbox_list)), |(_, _, (items, delimiter, mailbox))| Data::Lsub { items: items.unwrap_or_default(), mailbox, delimiter, }, ), map( tuple((tag_no_case(b"SEARCH"), many0(preceded(sp, nz_number)))), |(_, nums)| Data::Search(nums), ), map( tuple(( tag_no_case(b"STATUS"), sp, mailbox, sp, delimited(tag(b"("), opt(status_att_list), tag(b")")), )), |(_, _, mailbox, _, items)| Data::Status { mailbox, items: items.unwrap_or_default().into(), }, ), map( tuple((number, sp, tag_no_case(b"EXISTS"))), |(num, _, _)| Data::Exists(num), ), map( tuple((number, sp, tag_no_case(b"RECENT"))), |(num, _, _)| Data::Recent(num), ), quotaroot_response, quota_response, ))(input) } /// `mailbox-list = "(" [mbx-list-flags] ")" SP /// (DQUOTE QUOTED-CHAR DQUOTE / nil) SP /// mailbox` #[allow(clippy::type_complexity)] pub(crate) fn mailbox_list( input: &[u8], ) -> IMAPResult<&[u8], (Option>, Option, Mailbox)> { let mut parser = tuple(( delimited(tag(b"("), opt(mbx_list_flags), tag(b")")), sp, alt(( map(delimited(dquote, quoted_char, dquote), Option::Some), value(None, nil), )), sp, mailbox, )); let (remaining, (mbx_list_flags, _, maybe_delimiter, _, mailbox)) = parser(input)?; Ok((remaining, (mbx_list_flags, maybe_delimiter, mailbox))) } #[cfg(test)] mod tests { use super::*; #[test] fn test_mailbox() { assert!(mailbox(b"\"iNbOx\"").is_ok()); assert!(mailbox(b"{3}\r\naaa\r\n").is_ok()); assert!(mailbox(b"inbox ").is_ok()); assert!(mailbox(b"inbox.sent ").is_ok()); assert!(mailbox(b"aaa").is_err()); } } imap-codec-1.0.0/imap-codec/src/response.rs000066400000000000000000000576661447115025300205330ustar00rootroot00000000000000use std::str::from_utf8; #[cfg(not(feature = "quirk_crlf_relaxed"))] use abnf_core::streaming::crlf; #[cfg(feature = "quirk_crlf_relaxed")] use abnf_core::streaming::crlf_relaxed as crlf; use abnf_core::streaming::sp; use base64::{engine::general_purpose::STANDARD as _base64, Engine}; use imap_types::{ core::{NonEmptyVec, Text}, response::{ Capability, Code, CodeOther, CommandContinuationRequest, Data, Greeting, GreetingKind, Response, Status, }, }; #[cfg(feature = "quirk_missing_text")] use nom::combinator::peek; use nom::{ branch::alt, bytes::streaming::{tag, tag_no_case, take_until, take_while}, combinator::{map, map_res, opt, value}, multi::separated_list1, sequence::{delimited, preceded, terminated, tuple}, }; use crate::{ core::{atom, charset, nz_number, tag_imap, text}, decode::IMAPResult, extensions::enable::enable_data, fetch::msg_att, flag::flag_perm, mailbox::mailbox_data, }; // ----- greeting ----- /// `greeting = "*" SP (resp-cond-auth / resp-cond-bye) CRLF` pub(crate) fn greeting(input: &[u8]) -> IMAPResult<&[u8], Greeting> { let mut parser = tuple(( tag(b"*"), sp, alt(( resp_cond_auth, map(resp_cond_bye, |resp_text| (GreetingKind::Bye, resp_text)), )), crlf, )); let (remaining, (_, _, (kind, (code, text)), _)) = parser(input)?; Ok((remaining, Greeting { kind, code, text })) } /// `resp-cond-auth = ("OK" / "PREAUTH") SP resp-text` /// /// Authentication condition #[allow(clippy::type_complexity)] pub(crate) fn resp_cond_auth( input: &[u8], ) -> IMAPResult<&[u8], (GreetingKind, (Option, Text))> { let mut parser = tuple(( alt(( value(GreetingKind::Ok, tag_no_case(b"OK")), value(GreetingKind::PreAuth, tag_no_case(b"PREAUTH")), )), sp, resp_text, )); let (remaining, (kind, _, resp_text)) = parser(input)?; Ok((remaining, (kind, resp_text))) } /// `resp-text = ["[" resp-text-code "]" SP] text` pub(crate) fn resp_text(input: &[u8]) -> IMAPResult<&[u8], (Option, Text)> { // When the text starts with "[", we insist to parse a code. // Otherwise, a broken code could be interpreted as text. let (_, start) = opt(tag(b"["))(input)?; if start.is_some() { tuple(( preceded( tag(b"["), map( alt(( terminated(resp_text_code, tag(b"]")), map( terminated( take_while(|b: u8| b != b']' && b != b'\r' && b != b'\n'), tag(b"]"), ), |bytes: &[u8]| Code::Other(CodeOther::unvalidated(bytes)), ), )), Some, ), ), #[cfg(not(feature = "quirk_missing_text"))] preceded(sp, text), #[cfg(feature = "quirk_missing_text")] alt(( preceded(sp, text), value( { log::warn!("Rectified missing `text` to \"...\""); Text::unvalidated("...") }, peek(crlf), ), )), ))(input) } else { map(text, |text| (None, text))(input) } } /// `resp-text-code = "ALERT" / /// "BADCHARSET" [SP "(" charset *(SP charset) ")" ] / /// capability-data / /// "PARSE" / /// "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")" / /// "READ-ONLY" / /// "READ-WRITE" / /// "TRYCREATE" / /// "UIDNEXT" SP nz-number / /// "UIDVALIDITY" SP nz-number / /// "UNSEEN" SP nz-number / /// "COMPRESSIONACTIVE" ; RFC 4978 /// atom [SP 1*]` /// /// Note: See errata id: 261 pub(crate) fn resp_text_code(input: &[u8]) -> IMAPResult<&[u8], Code> { alt(( value(Code::Alert, tag_no_case(b"ALERT")), map( tuple(( tag_no_case(b"BADCHARSET"), opt(preceded( sp, delimited(tag(b"("), separated_list1(sp, charset), tag(b")")), )), )), |(_, maybe_charsets)| Code::BadCharset { allowed: maybe_charsets.unwrap_or_default(), }, ), map(capability_data, Code::Capability), value(Code::Parse, tag_no_case(b"PARSE")), map( tuple(( tag_no_case(b"PERMANENTFLAGS"), sp, delimited( tag(b"("), map(opt(separated_list1(sp, flag_perm)), |maybe_flags| { maybe_flags.unwrap_or_default() }), tag(b")"), ), )), |(_, _, flags)| Code::PermanentFlags(flags), ), value(Code::ReadOnly, tag_no_case(b"READ-ONLY")), value(Code::ReadWrite, tag_no_case(b"READ-WRITE")), value(Code::TryCreate, tag_no_case(b"TRYCREATE")), map( tuple((tag_no_case(b"UIDNEXT"), sp, nz_number)), |(_, _, num)| Code::UidNext(num), ), map( tuple((tag_no_case(b"UIDVALIDITY"), sp, nz_number)), |(_, _, num)| Code::UidValidity(num), ), map( tuple((tag_no_case(b"UNSEEN"), sp, nz_number)), |(_, _, num)| Code::Unseen(num), ), value(Code::CompressionActive, tag_no_case(b"COMPRESSIONACTIVE")), value(Code::OverQuota, tag_no_case(b"OVERQUOTA")), value(Code::TooBig, tag_no_case(b"TOOBIG")), ))(input) } /// `capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev1" *(SP capability)` /// /// Servers MUST implement the STARTTLS, AUTH=PLAIN, and LOGINDISABLED capabilities /// Servers which offer RFC 1730 compatibility MUST list "IMAP4" as the first capability. pub(crate) fn capability_data(input: &[u8]) -> IMAPResult<&[u8], NonEmptyVec> { let mut parser = tuple(( tag_no_case("CAPABILITY"), sp, separated_list1(sp, capability), )); let (rem, (_, _, caps)) = parser(input)?; Ok((rem, NonEmptyVec::unvalidated(caps))) } /// `capability = ("AUTH=" auth-type) / /// "COMPRESS=" algorithm / ; RFC 4978 /// atom` pub(crate) fn capability(input: &[u8]) -> IMAPResult<&[u8], Capability> { map(atom, Capability::from)(input) } /// `resp-cond-bye = "BYE" SP resp-text` pub(crate) fn resp_cond_bye(input: &[u8]) -> IMAPResult<&[u8], (Option, Text)> { let mut parser = tuple((tag_no_case(b"BYE"), sp, resp_text)); let (remaining, (_, _, resp_text)) = parser(input)?; Ok((remaining, resp_text)) } // ----- response ----- /// `response = *(continue-req / response-data) response-done` pub(crate) fn response(input: &[u8]) -> IMAPResult<&[u8], Response> { // Divert from standard here for better usability. // response_data already contains the bye response, thus // response_done could also be response_tagged. // // However, I will keep it as it is for now. alt(( map(continue_req, Response::CommandContinuationRequest), response_data, map(response_done, Response::Status), ))(input) } /// `continue-req = "+" SP (resp-text / base64) CRLF` pub(crate) fn continue_req(input: &[u8]) -> IMAPResult<&[u8], CommandContinuationRequest> { // We can't map the output of `resp_text` directly to `Continue::basic()` because we might end // up with a subset of `Text` that is valid base64 and will panic on `unwrap()`. Thus, we first // let the parsing finish and only later map to `Continue`. // A helper struct to postpone the unification to `Continue` in the `alt` combinator below. enum Either { Base64(A), Basic(B), } let mut parser = tuple(( tag(b"+ "), alt(( #[cfg(not(feature = "quirk_crlf_relaxed"))] map( map_res(take_until("\r\n"), |input| _base64.decode(input)), Either::Base64, ), #[cfg(feature = "quirk_crlf_relaxed")] map( map_res(take_until("\n"), |input: &[u8]| { if !input.is_empty() && input[input.len().saturating_sub(1)] == b'\r' { _base64.decode(&input[..input.len().saturating_sub(1)]) } else { _base64.decode(input) } }), Either::Base64, ), map(resp_text, Either::Basic), )), crlf, )); let (remaining, (_, either, _)) = parser(input)?; let continue_request = match either { Either::Base64(data) => CommandContinuationRequest::base64(data), Either::Basic((code, text)) => CommandContinuationRequest::basic(code, text).unwrap(), }; Ok((remaining, continue_request)) } /// `response-data = "*" SP ( /// resp-cond-state / /// resp-cond-bye / /// mailbox-data / /// message-data / /// capability-data /// ) CRLF` pub(crate) fn response_data(input: &[u8]) -> IMAPResult<&[u8], Response> { let mut parser = tuple(( tag(b"*"), sp, alt(( map(resp_cond_state, |(raw_status, code, text)| { let status = match raw_status.to_ascii_lowercase().as_ref() { "ok" => Status::Ok { tag: None, code, text, }, "no" => Status::No { tag: None, code, text, }, "bad" => Status::Bad { tag: None, code, text, }, _ => unreachable!(), }; Response::Status(status) }), map(resp_cond_bye, |(code, text)| { Response::Status(Status::Bye { code, text }) }), map(mailbox_data, Response::Data), map(message_data, Response::Data), map(capability_data, |caps| { Response::Data(Data::Capability(caps)) }), map(enable_data, Response::Data), )), crlf, )); let (remaining, (_, _, response, _)) = parser(input)?; Ok((remaining, response)) } /// `resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text` /// /// Status condition pub(crate) fn resp_cond_state(input: &[u8]) -> IMAPResult<&[u8], (&str, Option, Text)> { let mut parser = tuple(( alt((tag_no_case("OK"), tag_no_case("NO"), tag_no_case("BAD"))), sp, resp_text, )); let (remaining, (raw_status, _, (maybe_code, text))) = parser(input)?; Ok(( remaining, // # Safety // // `raw_status` is always UTF-8. (from_utf8(raw_status).unwrap(), maybe_code, text), )) } /// `response-done = response-tagged / response-fatal` pub(crate) fn response_done(input: &[u8]) -> IMAPResult<&[u8], Status> { alt((response_tagged, response_fatal))(input) } /// `response-tagged = tag SP resp-cond-state CRLF` pub(crate) fn response_tagged(input: &[u8]) -> IMAPResult<&[u8], Status> { let mut parser = tuple((tag_imap, sp, resp_cond_state, crlf)); let (remaining, (tag, _, (raw_status, code, text), _)) = parser(input)?; let status = match raw_status.to_ascii_lowercase().as_ref() { "ok" => Status::Ok { tag: Some(tag), code, text, }, "no" => Status::No { tag: Some(tag), code, text, }, "bad" => Status::Bad { tag: Some(tag), code, text, }, _ => unreachable!(), }; Ok((remaining, status)) } /// `response-fatal = "*" SP resp-cond-bye CRLF` /// /// Server closes connection immediately pub(crate) fn response_fatal(input: &[u8]) -> IMAPResult<&[u8], Status> { let mut parser = tuple((tag(b"*"), sp, resp_cond_bye, crlf)); let (remaining, (_, _, (code, text), _)) = parser(input)?; Ok((remaining, { Status::Bye { code, text } })) } /// `message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))` pub(crate) fn message_data(input: &[u8]) -> IMAPResult<&[u8], Data> { let (remaining, seq) = terminated(nz_number, sp)(input)?; alt(( map(tag_no_case(b"EXPUNGE"), move |_| Data::Expunge(seq)), map( tuple((tag_no_case(b"FETCH"), sp, msg_att)), move |(_, _, items)| Data::Fetch { seq, items }, ), ))(remaining) } #[cfg(test)] mod tests { use std::num::NonZeroU32; use imap_types::{ body::{ BasicFields, Body, BodyExtension, BodyStructure, Disposition, Language, Location, SinglePartExtensionData, SpecificFields, }, core::{IString, NString, QuotedChar, Tag}, flag::FlagNameAttribute, }; use super::*; use crate::testing::{kat_inverse_greeting, kat_inverse_response, known_answer_test_encode}; #[test] fn test_kat_inverse_greeting() { kat_inverse_greeting(&[ ( b"* OK [badcharset] ...\r\n".as_slice(), b"".as_slice(), Greeting::ok(Some(Code::BadCharset { allowed: vec![] }), "...").unwrap(), ), ( b"* OK [UnSEEN 12345] ...\r\naaa".as_slice(), b"aaa".as_slice(), Greeting::ok( Some(Code::Unseen(NonZeroU32::try_from(12345).unwrap())), "...", ) .unwrap(), ), ( b"* OK [unseen 12345] \r\n ".as_slice(), b" ".as_slice(), Greeting::ok( Some(Code::Unseen(NonZeroU32::try_from(12345).unwrap())), " ", ) .unwrap(), ), ( b"* PREAUTH [ALERT] hello\r\n".as_ref(), b"".as_ref(), Greeting::new(GreetingKind::PreAuth, Some(Code::Alert), "hello").unwrap(), ), ]); } #[test] fn test_kat_inverse_response_data() { kat_inverse_response(&[ ( b"* CAPABILITY IMAP4REV1\r\n".as_ref(), b"".as_ref(), Response::Data(Data::Capability(NonEmptyVec::from(Capability::Imap4Rev1))), ), ( b"* LIST (\\Noselect) \"/\" bbb\r\n", b"", Response::Data(Data::List { items: vec![FlagNameAttribute::Noselect], delimiter: Some(QuotedChar::try_from('/').unwrap()), mailbox: "bbb".try_into().unwrap(), }), ), ( b"* SEARCH 1 2 3 42\r\n", b"", Response::Data(Data::Search(vec![ 1.try_into().unwrap(), 2.try_into().unwrap(), 3.try_into().unwrap(), 42.try_into().unwrap(), ])), ), (b"* 42 EXISTS\r\n", b"", Response::Data(Data::Exists(42))), ( b"* 12345 RECENT\r\n", b"", Response::Data(Data::Recent(12345)), ), ( b"* 123 EXPUNGE\r\n", b"", Response::Data(Data::Expunge(123.try_into().unwrap())), ), ]); } #[test] fn test_kat_inverse_response_status() { kat_inverse_response(&[ // tagged; Ok, No, Bad ( b"A1 OK [ALERT] hello\r\n".as_ref(), b"".as_ref(), Response::Status( Status::ok( Some(Tag::try_from("A1").unwrap()), Some(Code::Alert), "hello", ) .unwrap(), ), ), ( b"A1 NO [ALERT] hello\r\n", b"".as_ref(), Response::Status( Status::no( Some(Tag::try_from("A1").unwrap()), Some(Code::Alert), "hello", ) .unwrap(), ), ), ( b"A1 BAD [ALERT] hello\r\n", b"".as_ref(), Response::Status( Status::bad( Some(Tag::try_from("A1").unwrap()), Some(Code::Alert), "hello", ) .unwrap(), ), ), ( b"A1 OK hello\r\n", b"".as_ref(), Response::Status( Status::ok(Some(Tag::try_from("A1").unwrap()), None, "hello").unwrap(), ), ), ( b"A1 NO hello\r\n", b"".as_ref(), Response::Status( Status::no(Some(Tag::try_from("A1").unwrap()), None, "hello").unwrap(), ), ), ( b"A1 BAD hello\r\n", b"".as_ref(), Response::Status( Status::bad(Some(Tag::try_from("A1").unwrap()), None, "hello").unwrap(), ), ), // untagged; Ok, No, Bad ( b"* OK [ALERT] hello\r\n", b"".as_ref(), Response::Status(Status::ok(None, Some(Code::Alert), "hello").unwrap()), ), ( b"* NO [ALERT] hello\r\n", b"".as_ref(), Response::Status(Status::no(None, Some(Code::Alert), "hello").unwrap()), ), ( b"* BAD [ALERT] hello\r\n", b"".as_ref(), Response::Status(Status::bad(None, Some(Code::Alert), "hello").unwrap()), ), ( b"* OK hello\r\n", b"".as_ref(), Response::Status(Status::ok(None, None, "hello").unwrap()), ), ( b"* NO hello\r\n", b"".as_ref(), Response::Status(Status::no(None, None, "hello").unwrap()), ), ( b"* BAD hello\r\n", b"".as_ref(), Response::Status(Status::bad(None, None, "hello").unwrap()), ), // bye ( b"* BYE [ALERT] hello\r\n", b"".as_ref(), Response::Status(Status::bye(Some(Code::Alert), "hello").unwrap()), ), ]); } /* // TODO(#184) #[test] fn test_kat_inverse_continue() { kat_inverse_continue(&[ ( b"+ \x01\r\n".as_ref(), b"".as_ref(), Continue::basic(None, "\x01").unwrap(), ), ( b"+ hello\r\n".as_ref(), b"".as_ref(), Continue::basic(None, "hello").unwrap(), ), ( b"+ [READ-WRITE] hello\r\n", b"", Continue::basic(Some(Code::ReadWrite), "hello").unwrap(), ), ]); } */ #[test] fn test_encode_body_structure() { let tests = [ ( BodyStructure::Single { body: Body { basic: BasicFields { parameter_list: vec![], id: NString(None), description: NString::try_from("description").unwrap(), content_transfer_encoding: IString::try_from("cte").unwrap(), size: 123, }, specific: SpecificFields::Basic { r#type: IString::try_from("application").unwrap(), subtype: IString::try_from("voodoo").unwrap(), }, }, extension_data: None, }, b"(\"application\" \"voodoo\" NIL NIL \"description\" \"cte\" 123)".as_ref(), ), ( BodyStructure::Single { body: Body { basic: BasicFields { parameter_list: vec![], id: NString(None), description: NString::try_from("description").unwrap(), content_transfer_encoding: IString::try_from("cte").unwrap(), size: 123, }, specific: SpecificFields::Text { subtype: IString::try_from("plain").unwrap(), number_of_lines: 14, }, }, extension_data: None, }, b"(\"TEXT\" \"plain\" NIL NIL \"description\" \"cte\" 123 14)", ), ( BodyStructure::Single { body: Body { basic: BasicFields { parameter_list: vec![], id: NString(None), description: NString::try_from("description").unwrap(), content_transfer_encoding: IString::try_from("cte").unwrap(), size: 123, }, specific: SpecificFields::Text { subtype: IString::try_from("plain").unwrap(), number_of_lines: 14, }, }, extension_data: Some(SinglePartExtensionData { md5: NString::try_from("AABB").unwrap(), tail: Some(Disposition { disposition: None, tail: Some(Language { language: vec![], tail: Some(Location{ location: NString(None), extensions: vec![BodyExtension::List(NonEmptyVec::from(BodyExtension::Number(1337)))], }) }) }) }), }, b"(\"TEXT\" \"plain\" NIL NIL \"description\" \"cte\" 123 14 \"AABB\" NIL NIL NIL (1337))", ), ]; for test in tests { known_answer_test_encode(test); } } #[test] fn test_parse_response_negative() { let tests = [ // TODO(#301,#184) // b"+ Nose[CAY a\r\n".as_ref() ]; for test in tests { assert!(response(test).is_err()); } } #[test] fn test_parse_resp_text_quirk() { #[cfg(not(feature = "quirk_missing_text"))] { assert!(resp_text(b"[IMAP4rev1]\r\n").is_err()); assert!(resp_text(b"[IMAP4rev1]\r\n").is_err()); assert!(resp_text(b"[IMAP4rev1] \r\n").is_err()); assert!(resp_text(b"[IMAP4rev1] \r\n").is_ok()); } #[cfg(feature = "quirk_missing_text")] { assert!(resp_text(b"[IMAP4rev1]\r\n").is_ok()); assert!(resp_text(b"[IMAP4rev1] \r\n").is_err()); assert!(resp_text(b"[IMAP4rev1] \r\n").is_ok()); } } } imap-codec-1.0.0/imap-codec/src/search.rs000066400000000000000000000365631447115025300201330ustar00rootroot00000000000000use abnf_core::streaming::sp; use imap_types::{command::CommandBody, core::NonEmptyVec, search::SearchKey}; use nom::{ branch::alt, bytes::streaming::{tag, tag_no_case}, combinator::{map, map_opt, opt, value}, multi::{many1, separated_list1}, sequence::{delimited, preceded, tuple}, }; use crate::{ core::{astring, atom, charset, number}, datetime::date, decode::{IMAPErrorKind, IMAPParseError, IMAPResult}, fetch::header_fld_name, sequence::sequence_set, }; /// `search = "SEARCH" [SP "CHARSET" SP charset] 1*(SP search-key)` /// /// Note: CHARSET argument MUST be registered with IANA /// /// errata id: 261 pub(crate) fn search(input: &[u8]) -> IMAPResult<&[u8], CommandBody> { let mut parser = tuple(( tag_no_case(b"SEARCH"), opt(map( tuple((sp, tag_no_case(b"CHARSET"), sp, charset)), |(_, _, _, charset)| charset, )), many1(preceded(sp, search_key(9))), )); let (remaining, (_, charset, mut criteria)) = parser(input)?; let criteria = match criteria.len() { 0 => unreachable!(), 1 => criteria.pop().unwrap(), _ => SearchKey::And(NonEmptyVec::unvalidated(criteria)), }; Ok(( remaining, CommandBody::Search { charset, criteria, uid: false, }, )) } /// `search-key = "ALL" / /// "ANSWERED" / /// "BCC" SP astring / /// "BEFORE" SP date / /// "BODY" SP astring / /// "CC" SP astring / /// "DELETED" / /// "FLAGGED" / /// "FROM" SP astring / /// "KEYWORD" SP flag-keyword / /// "NEW" / /// "OLD" / /// "ON" SP date / /// "RECENT" / /// "SEEN" / /// "SINCE" SP date / /// "SUBJECT" SP astring / /// "TEXT" SP astring / /// "TO" SP astring / /// "UNANSWERED" / /// "UNDELETED" / /// "UNFLAGGED" / /// "UNKEYWORD" SP flag-keyword / /// "UNSEEN" / /// ; Above this line were in [IMAP2] /// "DRAFT" / /// "HEADER" SP header-fld-name SP astring / /// "LARGER" SP number / /// "NOT" SP search-key / /// "OR" SP search-key SP search-key / /// "SENTBEFORE" SP date / /// "SENTON" SP date / /// "SENTSINCE" SP date / /// "SMALLER" SP number / /// "UID" SP sequence-set / /// "UNDRAFT" / /// sequence-set / /// "(" search-key *(SP search-key) ")"` /// /// This parser is recursively defined. Thus, in order to not overflow the stack, /// it is needed to limit how may recursions are allowed. (8 should suffice). pub(crate) fn search_key( remaining_recursions: usize, ) -> impl Fn(&[u8]) -> IMAPResult<&[u8], SearchKey> { move |input: &[u8]| search_key_limited(input, remaining_recursions) } fn search_key_limited<'a>( input: &'a [u8], remaining_recursion: usize, ) -> IMAPResult<&'a [u8], SearchKey> { if remaining_recursion == 0 { return Err(nom::Err::Failure(IMAPParseError { input, kind: IMAPErrorKind::RecursionLimitExceeded, })); } let search_key = move |input: &'a [u8]| search_key_limited(input, remaining_recursion.saturating_sub(1)); alt(( alt(( value(SearchKey::All, tag_no_case(b"ALL")), value(SearchKey::Answered, tag_no_case(b"ANSWERED")), map(tuple((tag_no_case(b"BCC"), sp, astring)), |(_, _, val)| { SearchKey::Bcc(val) }), map( tuple((tag_no_case(b"BEFORE"), sp, map_opt(date, |date| date))), |(_, _, date)| SearchKey::Before(date), ), map(tuple((tag_no_case(b"BODY"), sp, astring)), |(_, _, val)| { SearchKey::Body(val) }), map(tuple((tag_no_case(b"CC"), sp, astring)), |(_, _, val)| { SearchKey::Cc(val) }), value(SearchKey::Deleted, tag_no_case(b"DELETED")), value(SearchKey::Flagged, tag_no_case(b"FLAGGED")), map(tuple((tag_no_case(b"FROM"), sp, astring)), |(_, _, val)| { SearchKey::From(val) }), map( // Note: `flag_keyword` parser returns `Flag`. Because Rust does not have first-class enum variants // it is not possible to fix SearchKey(Flag::Keyword), but only SearchKey(Flag). // Thus `SearchKey::Keyword(Atom)` is used instead. This is, why we use also `atom` parser here and not `flag_keyword` parser. tuple((tag_no_case(b"KEYWORD"), sp, atom)), |(_, _, val)| SearchKey::Keyword(val), ), value(SearchKey::New, tag_no_case(b"NEW")), value(SearchKey::Old, tag_no_case(b"OLD")), map( tuple((tag_no_case(b"ON"), sp, map_opt(date, |date| date))), |(_, _, date)| SearchKey::On(date), ), value(SearchKey::Recent, tag_no_case(b"RECENT")), value(SearchKey::Seen, tag_no_case(b"SEEN")), map( tuple((tag_no_case(b"SINCE"), sp, map_opt(date, |date| date))), |(_, _, date)| SearchKey::Since(date), ), map( tuple((tag_no_case(b"SUBJECT"), sp, astring)), |(_, _, val)| SearchKey::Subject(val), ), map(tuple((tag_no_case(b"TEXT"), sp, astring)), |(_, _, val)| { SearchKey::Text(val) }), map(tuple((tag_no_case(b"TO"), sp, astring)), |(_, _, val)| { SearchKey::To(val) }), )), alt(( value(SearchKey::Unanswered, tag_no_case(b"UNANSWERED")), value(SearchKey::Undeleted, tag_no_case(b"UNDELETED")), value(SearchKey::Unflagged, tag_no_case(b"UNFLAGGED")), map( // Note: `flag_keyword` parser returns `Flag`. Because Rust does not have first-class enum variants // it is not possible to fix SearchKey(Flag::Keyword), but only SearchKey(Flag). // Thus `SearchKey::Keyword(Atom)` is used instead. This is, why we use also `atom` parser here and not `flag_keyword` parser. tuple((tag_no_case(b"UNKEYWORD"), sp, atom)), |(_, _, val)| SearchKey::Unkeyword(val), ), value(SearchKey::Unseen, tag_no_case(b"UNSEEN")), value(SearchKey::Draft, tag_no_case(b"DRAFT")), map( tuple((tag_no_case(b"HEADER"), sp, header_fld_name, sp, astring)), |(_, _, key, _, val)| SearchKey::Header(key, val), ), map( tuple((tag_no_case(b"LARGER"), sp, number)), |(_, _, val)| SearchKey::Larger(val), ), map( tuple((tag_no_case(b"NOT"), sp, search_key)), |(_, _, val)| SearchKey::Not(Box::new(val)), ), map( tuple((tag_no_case(b"OR"), sp, search_key, sp, search_key)), |(_, _, alt1, _, alt2)| SearchKey::Or(Box::new(alt1), Box::new(alt2)), ), map( tuple((tag_no_case(b"SENTBEFORE"), sp, map_opt(date, |date| date))), |(_, _, date)| SearchKey::SentBefore(date), ), map( tuple((tag_no_case(b"SENTON"), sp, map_opt(date, |date| date))), |(_, _, date)| SearchKey::SentOn(date), ), map( tuple((tag_no_case(b"SENTSINCE"), sp, map_opt(date, |date| date))), |(_, _, date)| SearchKey::SentSince(date), ), map( tuple((tag_no_case(b"SMALLER"), sp, number)), |(_, _, val)| SearchKey::Smaller(val), ), map( tuple((tag_no_case(b"UID"), sp, sequence_set)), |(_, _, val)| SearchKey::Uid(val), ), value(SearchKey::Undraft, tag_no_case(b"UNDRAFT")), map(sequence_set, SearchKey::SequenceSet), map( delimited(tag(b"("), separated_list1(sp, search_key), tag(b")")), |val| SearchKey::And(NonEmptyVec::unvalidated(val)), ), )), ))(input) } #[cfg(test)] mod tests { use imap_types::{ core::{AString, Atom}, datetime::NaiveDate, sequence::{Sequence, SequenceSet}, }; use super::*; use crate::testing::known_answer_test_encode; #[test] fn test_parse_search() { use imap_types::{ search::SearchKey::*, sequence::{SeqOrUid::Value, Sequence::*, SequenceSet as SequenceSetData}, }; let (_rem, val) = search(b"search (uid 5)???").unwrap(); assert_eq!( val, CommandBody::Search { charset: None, criteria: And(NonEmptyVec::from(Uid(SequenceSetData( vec![Single(Value(5.try_into().unwrap()))] .try_into() .unwrap() )))), uid: false, } ); let (_rem, val) = search(b"search (uid 5 or uid 5 (uid 1 uid 2) not uid 5)???").unwrap(); let expected = CommandBody::Search { charset: None, criteria: And(vec![ Uid(SequenceSetData( vec![Single(Value(5.try_into().unwrap()))] .try_into() .unwrap(), )), Or( Box::new(Uid(SequenceSetData( vec![Single(Value(5.try_into().unwrap()))] .try_into() .unwrap(), ))), Box::new(And(vec![ Uid(SequenceSetData( vec![Single(Value(1.try_into().unwrap()))] .try_into() .unwrap(), )), Uid(SequenceSetData( vec![Single(Value(2.try_into().unwrap()))] .try_into() .unwrap(), )), ] .try_into() .unwrap())), ), Not(Box::new(Uid(SequenceSetData( vec![Single(Value(5.try_into().unwrap()))] .try_into() .unwrap(), )))), ] .try_into() .unwrap()), uid: false, }; assert_eq!(val, expected); } #[test] fn test_parse_search_key() { assert!(search_key(1)(b"1:5|").is_ok()); assert!(search_key(1)(b"(1:5)|").is_err()); assert!(search_key(2)(b"(1:5)|").is_ok()); assert!(search_key(2)(b"((1:5))|").is_err()); } #[test] fn test_encode_search_key() { let tests = [ ( SearchKey::And(NonEmptyVec::try_from(vec![SearchKey::Answered]).unwrap()), b"(ANSWERED)".as_ref(), ), ( SearchKey::And( NonEmptyVec::try_from(vec![SearchKey::Answered, SearchKey::Seen]).unwrap(), ), b"(ANSWERED SEEN)".as_ref(), ), ( SearchKey::SequenceSet(SequenceSet::try_from(1).unwrap()), b"1", ), (SearchKey::All, b"ALL"), (SearchKey::Answered, b"ANSWERED"), (SearchKey::Bcc(AString::try_from("A").unwrap()), b"BCC A"), ( SearchKey::Before( NaiveDate::try_from(chrono::NaiveDate::from_ymd_opt(2023, 4, 12).unwrap()) .unwrap(), ), b"BEFORE \"12-Apr-2023\"", ), (SearchKey::Body(AString::try_from("A").unwrap()), b"BODY A"), (SearchKey::Cc(AString::try_from("A").unwrap()), b"CC A"), (SearchKey::Deleted, b"DELETED"), (SearchKey::Draft, b"DRAFT"), (SearchKey::Flagged, b"FLAGGED"), (SearchKey::From(AString::try_from("A").unwrap()), b"FROM A"), ( SearchKey::Header( AString::try_from("A").unwrap(), AString::try_from("B").unwrap(), ), b"HEADER A B", ), ( SearchKey::Keyword(Atom::try_from("A").unwrap()), b"KEYWORD A", ), (SearchKey::Larger(42), b"LARGER 42"), (SearchKey::New, b"NEW"), (SearchKey::Not(Box::new(SearchKey::New)), b"NOT NEW"), (SearchKey::Old, b"OLD"), ( SearchKey::On( NaiveDate::try_from(chrono::NaiveDate::from_ymd_opt(2023, 4, 12).unwrap()) .unwrap(), ), b"ON \"12-Apr-2023\"", ), ( SearchKey::Or(Box::new(SearchKey::New), Box::new(SearchKey::Recent)), b"OR NEW RECENT", ), (SearchKey::Recent, b"RECENT"), (SearchKey::Seen, b"SEEN"), ( SearchKey::SentBefore( NaiveDate::try_from(chrono::NaiveDate::from_ymd_opt(2023, 4, 12).unwrap()) .unwrap(), ), b"SENTBEFORE \"12-Apr-2023\"", ), ( SearchKey::SentOn( NaiveDate::try_from(chrono::NaiveDate::from_ymd_opt(2023, 4, 12).unwrap()) .unwrap(), ), b"SENTON \"12-Apr-2023\"", ), ( SearchKey::SentSince( NaiveDate::try_from(chrono::NaiveDate::from_ymd_opt(2023, 4, 12).unwrap()) .unwrap(), ), b"SENTSINCE \"12-Apr-2023\"", ), ( SearchKey::Since( NaiveDate::try_from(chrono::NaiveDate::from_ymd_opt(2023, 4, 12).unwrap()) .unwrap(), ), b"SINCE \"12-Apr-2023\"", ), (SearchKey::Smaller(1337), b"SMALLER 1337"), ( SearchKey::Subject(AString::try_from("A").unwrap()), b"SUBJECT A", ), (SearchKey::Text(AString::try_from("A").unwrap()), b"TEXT A"), (SearchKey::To(AString::try_from("A").unwrap()), b"TO A"), ( SearchKey::Uid(SequenceSet::try_from(Sequence::try_from(1..).unwrap()).unwrap()), b"UID 1:*", ), (SearchKey::Unanswered, b"UNANSWERED"), (SearchKey::Undeleted, b"UNDELETED"), (SearchKey::Undraft, b"UNDRAFT"), (SearchKey::Unflagged, b"UNFLAGGED"), ( SearchKey::Unkeyword(Atom::try_from("A").unwrap()), b"UNKEYWORD A", ), (SearchKey::Unseen, b"UNSEEN"), ]; for test in tests { known_answer_test_encode(test); } } } imap-codec-1.0.0/imap-codec/src/sequence.rs000066400000000000000000000114331447115025300204630ustar00rootroot00000000000000use imap_types::{ core::NonEmptyVec, sequence::{SeqOrUid, Sequence, SequenceSet}, }; use nom::{ branch::alt, bytes::streaming::tag, combinator::{map, value}, multi::separated_list1, sequence::tuple, }; use crate::{core::nz_number, decode::IMAPResult}; /// `sequence-set = (seq-number / seq-range) ["," sequence-set]` /// /// Note: See errata id: 261 TODO: Why the errata? /// /// Set of seq-number values, regardless of order. /// Servers MAY coalesce overlaps and/or execute the sequence in any order. /// /// Example: a message sequence number set of /// 2,4:7,9,12:* for a mailbox with 15 messages is /// equivalent to 2,4,5,6,7,9,12,13,14,15 /// /// Example: a message sequence number set of *:4,5:7 /// for a mailbox with 10 messages is equivalent to /// 10,9,8,7,6,5,4,5,6,7 and MAY be reordered and /// overlap coalesced to be 4,5,6,7,8,9,10. /// /// Simplified: /// /// `sequence-set = (seq-number / seq-range) *("," (seq-number / seq-range))` pub(crate) fn sequence_set(input: &[u8]) -> IMAPResult<&[u8], SequenceSet> { map( separated_list1( tag(b","), alt(( // Ordering is important! map(seq_range, |(from, to)| Sequence::Range(from, to)), map(seq_number, Sequence::Single), )), ), |set| SequenceSet(NonEmptyVec::unvalidated(set)), )(input) } /// `seq-range = seq-number ":" seq-number` /// /// Two seq-number values and all values between these two regardless of order. /// /// Example: 2:4 and 4:2 are equivalent and indicate values 2, 3, and 4. /// /// Example: a unique identifier sequence range of 3291:* includes the UID /// of the last message in the mailbox, even if that value is less than 3291. pub(crate) fn seq_range(input: &[u8]) -> IMAPResult<&[u8], (SeqOrUid, SeqOrUid)> { let mut parser = tuple((seq_number, tag(b":"), seq_number)); let (remaining, (from, _, to)) = parser(input)?; Ok((remaining, (from, to))) } /// `seq-number = nz-number / "*"` /// /// Message sequence number (COPY, FETCH, STORE commands) or unique /// identifier (UID COPY, UID FETCH, UID STORE commands). /// /// * represents the largest number in use. /// In the case of message sequence numbers, it is the number of messages in a non-empty mailbox. /// In the case of unique identifiers, it is the unique identifier of the last message in the mailbox or, /// if the mailbox is empty, the mailbox's current UIDNEXT value. /// /// The server should respond with a tagged BAD response to a command that uses a message /// sequence number greater than the number of messages in the selected mailbox. /// This includes "*" if the selected mailbox is empty. pub(crate) fn seq_number(input: &[u8]) -> IMAPResult<&[u8], SeqOrUid> { alt(( map(nz_number, SeqOrUid::Value), value(SeqOrUid::Asterisk, tag(b"*")), ))(input) } #[cfg(test)] mod tests { use super::*; use crate::encode::{EncodeContext, EncodeIntoContext}; #[test] fn test_encode_of_some_sequence_sets() { let tests = [ ( Sequence::Single(SeqOrUid::Value(1.try_into().unwrap())), b"1".as_ref(), ), (Sequence::Single(SeqOrUid::Asterisk), b"*".as_ref()), ( Sequence::Range(SeqOrUid::Value(1.try_into().unwrap()), SeqOrUid::Asterisk), b"1:*".as_ref(), ), ]; for (test, expected) in tests { let mut ctx = EncodeContext::new(); test.encode_ctx(&mut ctx).unwrap(); let out = ctx.dump(); assert_eq!(*expected, out); } } #[test] fn test_parse_sequence_set() { let (rem, val) = sequence_set(b"1:*?").unwrap(); println!("{:?}, {:?}", rem, val); let (rem, val) = sequence_set(b"1:*,5?").unwrap(); println!("{:?}, {:?}", rem, val); } #[test] fn test_parse_seq_number() { // Must not be 0. assert!(seq_number(b"0?").is_err()); let (rem, val) = seq_number(b"1?").unwrap(); println!("{:?}, {:?}", rem, val); let (rem, val) = seq_number(b"*?").unwrap(); println!("{:?}, {:?}", rem, val); } #[test] fn test_parse_seq_range() { // Must not be 0. assert!(seq_range(b"0:1?").is_err()); assert_eq!( ( SeqOrUid::Value(1.try_into().unwrap()), SeqOrUid::Value(2.try_into().unwrap()) ), seq_range(b"1:2?").unwrap().1 ); assert_eq!( (SeqOrUid::Value(1.try_into().unwrap()), SeqOrUid::Asterisk), seq_range(b"1:*?").unwrap().1 ); assert_eq!( (SeqOrUid::Asterisk, SeqOrUid::Value(10.try_into().unwrap())), seq_range(b"*:10?").unwrap().1 ); } } imap-codec-1.0.0/imap-codec/src/status.rs000066400000000000000000000105511447115025300201760ustar00rootroot00000000000000use abnf_core::streaming::sp; use imap_types::status::{StatusDataItem, StatusDataItemName}; use nom::{ branch::alt, bytes::streaming::tag_no_case, combinator::{map, value}, multi::separated_list1, sequence::tuple, }; use crate::{ core::{number, number64, nz_number}, decode::IMAPResult, }; /// `status-att = "MESSAGES" / /// "RECENT" / /// "UIDNEXT" / /// "UIDVALIDITY" / /// "UNSEEN"` pub(crate) fn status_att(input: &[u8]) -> IMAPResult<&[u8], StatusDataItemName> { alt(( value(StatusDataItemName::Messages, tag_no_case(b"MESSAGES")), value(StatusDataItemName::Recent, tag_no_case(b"RECENT")), value(StatusDataItemName::UidNext, tag_no_case(b"UIDNEXT")), value(StatusDataItemName::UidValidity, tag_no_case(b"UIDVALIDITY")), value(StatusDataItemName::Unseen, tag_no_case(b"UNSEEN")), value( StatusDataItemName::DeletedStorage, tag_no_case(b"DELETED-STORAGE"), ), value(StatusDataItemName::Deleted, tag_no_case(b"DELETED")), #[cfg(feature = "ext_condstore_qresync")] value( StatusDataItemName::HighestModSeq, tag_no_case(b"HIGHESTMODSEQ"), ), ))(input) } /// `status-att-list = status-att-val *(SP status-att-val)` /// /// Note: See errata id: 261 pub(crate) fn status_att_list(input: &[u8]) -> IMAPResult<&[u8], Vec> { separated_list1(sp, status_att_val)(input) } /// `status-att-val = ("MESSAGES" SP number) / /// ("RECENT" SP number) / /// ("UIDNEXT" SP nz-number) / /// ("UIDVALIDITY" SP nz-number) / /// ("UNSEEN" SP number)` /// /// Note: See errata id: 261 fn status_att_val(input: &[u8]) -> IMAPResult<&[u8], StatusDataItem> { alt(( map( tuple((tag_no_case(b"MESSAGES"), sp, number)), |(_, _, num)| StatusDataItem::Messages(num), ), map( tuple((tag_no_case(b"RECENT"), sp, number)), |(_, _, num)| StatusDataItem::Recent(num), ), map( tuple((tag_no_case(b"UIDNEXT"), sp, nz_number)), |(_, _, next)| StatusDataItem::UidNext(next), ), map( tuple((tag_no_case(b"UIDVALIDITY"), sp, nz_number)), |(_, _, val)| StatusDataItem::UidValidity(val), ), map( tuple((tag_no_case(b"UNSEEN"), sp, number)), |(_, _, num)| StatusDataItem::Unseen(num), ), map( tuple((tag_no_case(b"DELETED-STORAGE"), sp, number64)), |(_, _, num)| StatusDataItem::DeletedStorage(num), ), map( tuple((tag_no_case(b"DELETED"), sp, number)), |(_, _, num)| StatusDataItem::Deleted(num), ), ))(input) } #[cfg(test)] mod tests { use std::num::NonZeroU32; use super::*; use crate::testing::known_answer_test_encode; #[test] fn test_encode_status_data_item_name() { let tests = [ (StatusDataItemName::Messages, b"MESSAGES".as_ref()), (StatusDataItemName::Recent, b"RECENT"), (StatusDataItemName::UidNext, b"UIDNEXT"), (StatusDataItemName::UidValidity, b"UIDVALIDITY"), (StatusDataItemName::Unseen, b"UNSEEN"), (StatusDataItemName::Deleted, b"DELETED"), (StatusDataItemName::DeletedStorage, b"DELETED-STORAGE"), ]; for test in tests { known_answer_test_encode(test); } } #[test] fn test_encode_status_data_item() { let tests = [ (StatusDataItem::Messages(0), b"MESSAGES 0".as_ref()), (StatusDataItem::Recent(u32::MAX), b"RECENT 4294967295"), ( StatusDataItem::UidNext(NonZeroU32::new(1).unwrap()), b"UIDNEXT 1", ), ( StatusDataItem::UidValidity(NonZeroU32::new(u32::MAX).unwrap()), b"UIDVALIDITY 4294967295", ), (StatusDataItem::Unseen(0), b"UNSEEN 0"), (StatusDataItem::Deleted(1), b"DELETED 1"), ( StatusDataItem::DeletedStorage(u64::MAX), b"DELETED-STORAGE 18446744073709551615", ), ]; for test in tests { known_answer_test_encode(test); } } } imap-codec-1.0.0/imap-codec/src/testing.rs000066400000000000000000000063731447115025300203370ustar00rootroot00000000000000use std::fmt::Debug; use imap_types::{ auth::AuthenticateData, command::Command, response::{Greeting, Response}, utils::escape_byte_string, }; use crate::{ decode::{Decoder, IMAPResult}, encode::{EncodeContext, EncodeIntoContext}, AuthenticateDataCodec, CommandCodec, GreetingCodec, ResponseCodec, }; pub(crate) fn known_answer_test_encode( (test_object, expected_bytes): (impl EncodeIntoContext, impl AsRef<[u8]>), ) { let expected_bytes = expected_bytes.as_ref(); let mut ctx = EncodeContext::new(); test_object.encode_ctx(&mut ctx).unwrap(); let got_bytes = ctx.dump(); let got_bytes = got_bytes.as_slice(); if expected_bytes != got_bytes { println!("# Debug (`escape_byte_string`, encapsulated by `<<<` and `>>>`)"); println!( "Left: <<<{}>>>\nRight: <<<{}>>>", escape_byte_string(expected_bytes), escape_byte_string(got_bytes), ); println!("# Debug"); panic!("Left: {:02x?}\nRight: {:02x?}", expected_bytes, got_bytes); } } pub(crate) fn known_answer_test_parse<'a, O, P>( (test, expected_remainder, expected_object): (&'a [u8], &[u8], O), parser: P, ) where O: Debug + Eq + 'a, P: Fn(&'a [u8]) -> IMAPResult<&'a [u8], O>, { let (got_remainder, got_object) = parser(test).unwrap(); assert_eq!(expected_remainder, got_remainder); assert_eq!(expected_object, got_object); } // Note: Maybe there is a cleaner way to write this using generic bounds. However, // we tried it and failed to provide a cleaner solution. Thus, it's a macro for now. macro_rules! impl_kat_inverse { ($fn_name:ident, $decoder:ident, $item:ty) => { pub(crate) fn $fn_name(tests: &[(&[u8], &[u8], $item)]) { for (no, (test_input, expected_remainder, expected_object)) in tests.iter().enumerate() { println!("# {no}"); let (got_remainder, got_object) = $decoder::default().decode(test_input).unwrap(); assert_eq!(*expected_object, got_object); assert_eq!(*expected_remainder, got_remainder); let mut ctx = EncodeContext::new(); got_object.encode_ctx(&mut ctx).unwrap(); let got_output = ctx.dump(); // This second `decode` makes using generic bounds more complicated due to the // different lifetime. let (got_remainder, got_object_again) = $decoder::default().decode(&got_output).unwrap(); assert_eq!(got_object, got_object_again); assert!(got_remainder.is_empty()); } } }; } impl_kat_inverse! {kat_inverse_greeting, GreetingCodec, Greeting} impl_kat_inverse! {kat_inverse_command, CommandCodec, Command} impl_kat_inverse! {kat_inverse_response, ResponseCodec, Response} //impl_kat_inverse! {kat_inverse_continue, ContinueCodec, Continue} impl_kat_inverse! {kat_inverse_authenticate_data, AuthenticateDataCodec, AuthenticateData} #[cfg(test)] mod tests { use imap_types::command::{Command, CommandBody}; use super::*; #[test] #[should_panic] fn test_known_answer_test_encode() { known_answer_test_encode((Command::new("A", CommandBody::Noop).unwrap(), b"")); } } imap-codec-1.0.0/imap-codec/tests/000077500000000000000000000000001447115025300166565ustar00rootroot00000000000000imap-codec-1.0.0/imap-codec/tests/readme.rs000066400000000000000000000010011447115025300204510ustar00rootroot00000000000000use imap_codec::{decode::Decoder, encode::Encoder, CommandCodec}; #[test] fn test_from_readme() { let input = b"ABCD UID FETCH 1,2:* (BODY.PEEK[1.2.3.4.MIME]<42.1337>)\r\n"; let (_remainder, parsed) = CommandCodec::default().decode(input).unwrap(); println!("# Parsed\n\n{:#?}\n\n", parsed); let buffer = CommandCodec::default().encode(&parsed).dump(); // Note: IMAP4rev1 may produce messages that are not valid UTF-8. println!("# Serialized\n\n{:?}", std::str::from_utf8(&buffer)); } imap-codec-1.0.0/imap-codec/tests/trace.rs000066400000000000000000001321551447115025300203310ustar00rootroot00000000000000use imap_codec::{ decode::Decoder, encode::Encoder, imap_types::{ auth::AuthMechanism, body::{BasicFields, Body, BodyStructure, SpecificFields}, command::{Command, CommandBody}, core::{AString, IString, Literal, NString, Quoted, Tag}, datetime::DateTime, envelope::{Address, Envelope}, fetch::{Macro, MessageDataItem, MessageDataItemName, Section}, flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType}, response::{Capability, Code, Data, Response, Status}, secret::Secret, }, CommandCodec, GreetingCodec, ResponseCodec, }; enum Who { Client, Server, } enum Message<'a> { Command(Command<'a>), Response(Response<'a>), } struct TraceLines<'a> { trace: &'a [u8], offset: usize, } impl<'a> Iterator for TraceLines<'a> { type Item = (Who, &'a [u8]); fn next(&mut self) -> Option { let input = &self.trace[self.offset..]; if let Some(pos) = input.iter().position(|b| *b == b'\n') { let who = match &input[..3] { b"C: " => Who::Client, b"S: " => Who::Server, _ => panic!("Line must begin with \"C: \" or \"S: \"."), }; self.offset += pos + 1; Some((who, &input[3..pos + 1])) } else { None } } } fn split_trace(trace: &[u8]) -> impl Iterator { TraceLines { trace, offset: 0 } } fn test_lines_of_trace(trace: &[u8]) { for (who, line) in split_trace(trace) { // Replace last "\n" with "\r\n". let line = { let mut line = line[..line.len().saturating_sub(1)].to_vec(); line.extend_from_slice(b"\r\n"); line }; match who { Who::Client => { println!("C: {}", String::from_utf8_lossy(&line).trim()); let (rem, parsed) = CommandCodec::default().decode(&line).unwrap(); assert!(rem.is_empty()); println!("Parsed {:?}", parsed); let serialized = CommandCodec::default().encode(&parsed).dump(); println!( "Serialized: {}", String::from_utf8_lossy(&serialized).trim() ); let (rem, parsed2) = CommandCodec::default().decode(&serialized).unwrap(); assert!(rem.is_empty()); assert_eq!(parsed, parsed2); println!() } Who::Server => { println!("S: {}", String::from_utf8_lossy(&line).trim()); let (rem, parsed) = ResponseCodec::default().decode(&line).unwrap(); println!("Parsed: {:?}", parsed); assert!(rem.is_empty()); let serialized = ResponseCodec::default().encode(&parsed).dump(); println!( "Serialized: {}", String::from_utf8_lossy(&serialized).trim() ); let (rem, parsed2) = ResponseCodec::default().decode(&serialized).unwrap(); assert!(rem.is_empty()); assert_eq!(parsed, parsed2); println!() } } } } fn test_trace_known_positive(tests: Vec<(&[u8], Message)>) { for (test, expected) in tests.into_iter() { println!("// {}", std::str::from_utf8(test).unwrap().trim()); match expected { Message::Command(expected) => { let (rem, got) = CommandCodec::default().decode(test).unwrap(); assert!(rem.is_empty()); assert_eq!(expected, got); println!("{:?}", got); let encoded = CommandCodec::default().encode(&got).dump(); println!("// {}", String::from_utf8(encoded.clone()).unwrap().trim()); let (rem2, got2) = CommandCodec::default().decode(&encoded).unwrap(); assert!(rem2.is_empty()); assert_eq!(expected, got2); } Message::Response(expected) => { let (rem, got) = ResponseCodec::default().decode(test).unwrap(); assert!(rem.is_empty()); assert_eq!(expected, got); println!("{:?}", got); let encoded = ResponseCodec::default().encode(&got).dump(); println!("// {}", String::from_utf8(encoded.clone()).unwrap().trim()); let (rem2, got2) = ResponseCodec::default().decode(&encoded).unwrap(); assert!(rem2.is_empty()); assert_eq!(expected, got2); } }; println!(); } } #[test] fn test_from_capability() { let tests = { vec![ ( b"abcd CAPABILITY\r\n".as_ref(), Message::Command(Command::new("abcd", CommandBody::Capability).unwrap()), ), ( b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n", Message::Response(Response::Data( // FIXME(API): accept &[...] Data::capability(vec![ Capability::Imap4Rev1, #[cfg(feature = "starttls")] Capability::StartTls, #[cfg(not(feature = "starttls"))] Capability::try_from("STARTTLS").unwrap(), Capability::Auth(AuthMechanism::try_from("GSSAPI").unwrap()), #[cfg(feature = "starttls")] Capability::LoginDisabled, #[cfg(not(feature = "starttls"))] Capability::try_from("LOGINDISABLED").unwrap(), ]) .unwrap(), )), ), ( b"abcd OK CAPABILITY completed\r\n", // FIXME(API): Option no TryInto ... Message::Response(Response::Status( Status::ok( Some(Tag::try_from("abcd").unwrap()), None, "CAPABILITY completed", ) .unwrap(), )), ), #[cfg(feature = "starttls")] ( b"efgh STARTTLS\r\n", Message::Command(Command::new("efgh", CommandBody::StartTLS).unwrap()), ), ( b"efgh OK STARTLS completed\r\n", // FIXME(API): Option no TryInto ... Message::Response(Response::Status( Status::ok( Some(Tag::try_from("efgh").unwrap()), None, "STARTLS completed", ) .unwrap(), )), ), ( b"ijkl CAPABILITY\r\n", Message::Command(Command::new("ijkl", CommandBody::Capability).unwrap()), ), ( b"* CAPABILITY IMAP4rev1 AUTH=GSSAPI AUTH=PLAIN\r\n", Message::Response(Response::Data( Data::capability(vec![ Capability::Imap4Rev1, Capability::Auth(AuthMechanism::try_from("GSSAPI").unwrap()), Capability::Auth(AuthMechanism::Plain), ]) .unwrap(), )), ), ( b"ijkl OK CAPABILITY completed\r\n", // FIXME(API): Option no TryInto ... Message::Response(Response::Status( Status::ok( Some(Tag::try_from("ijkl").unwrap()), None, "CAPABILITY completed", ) .unwrap(), )), ), ] }; test_trace_known_positive(tests); } #[test] fn test_from_noop() { let tests = { vec![ ( b"a002 NOOP\r\n".as_ref(), Message::Command(Command::new("a002", CommandBody::Noop).unwrap()), ), ( b"a002 OK NOOP completed\r\n", // FIXME(API) Message::Response(Response::Status( Status::ok(Some(Tag::try_from("a002").unwrap()), None, "NOOP completed") .unwrap(), )), ), ( b"a047 NOOP\r\n", Message::Command(Command::new("a047", CommandBody::Noop).unwrap()), ), ( b"* 22 EXPUNGE\r\n", Message::Response(Response::Data(Data::expunge(22).unwrap())), ), ( b"* 23 EXISTS\r\n", Message::Response(Response::Data(Data::Exists(23))), ), ( b"* 3 RECENT\r\n", Message::Response(Response::Data(Data::Recent(3))), ), ( b"* 14 FETCH (FLAGS (\\Seen \\Deleted))\r\n", // FIXME(API) Message::Response(Response::Data( Data::fetch( 14, vec![MessageDataItem::Flags(vec![ FlagFetch::Flag(Flag::Seen), FlagFetch::Flag(Flag::Deleted), ])], ) .unwrap(), )), ), ( b"a047 OK NOOP completed\r\n", // FIXME(API) Message::Response(Response::Status( Status::ok(Some(Tag::try_from("a047").unwrap()), None, "NOOP completed") .unwrap(), )), ), ] }; test_trace_known_positive(tests); } #[test] fn test_from_logout() { let tests = { vec![ ( b"A023 LOGOUT\r\n".as_ref(), Message::Command(Command::new("A023", CommandBody::Logout).unwrap()), ), ( b"* BYE IMAP4rev1 Server logging out\r\n", Message::Response(Response::Status( Status::bye(None, "IMAP4rev1 Server logging out").unwrap(), )), ), ( b"A023 OK LOGOUT completed\r\n", // FIXME(API) Message::Response(Response::Status( Status::ok( Some(Tag::try_from("A023").unwrap()), None, "LOGOUT completed", ) .unwrap(), )), ), ] }; test_trace_known_positive(tests); } #[cfg(feature = "starttls")] #[test] fn test_from_starttls() { let trace = br#"C: a001 CAPABILITY S: * CAPABILITY IMAP4rev1 STARTTLS LOGINDISABLED S: a001 OK CAPABILITY completed C: a002 STARTTLS S: a002 OK Begin TLS negotiation now C: a003 CAPABILITY S: * CAPABILITY IMAP4rev1 AUTH=PLAIN S: a003 OK CAPABILITY completed C: a004 LOGIN joe password S: a004 OK LOGIN completed "#; test_lines_of_trace(trace); } #[test] fn test_from_authenticate() { // S: * OK IMAP4rev1 Server // C: A001 AUTHENTICATE GSSAPI // S: + // C: YIIB+wYJKoZIhvcSAQICAQBuggHqMIIB5qADAgEFoQMCAQ6iBw // MFACAAAACjggEmYYIBIjCCAR6gAwIBBaESGxB1Lndhc2hpbmd0 // b24uZWR1oi0wK6ADAgEDoSQwIhsEaW1hcBsac2hpdmFtcy5jYW // Mud2FzaGluZ3Rvbi5lZHWjgdMwgdCgAwIBAaEDAgEDooHDBIHA // cS1GSa5b+fXnPZNmXB9SjL8Ollj2SKyb+3S0iXMljen/jNkpJX // AleKTz6BQPzj8duz8EtoOuNfKgweViyn/9B9bccy1uuAE2HI0y // C/PHXNNU9ZrBziJ8Lm0tTNc98kUpjXnHZhsMcz5Mx2GR6dGknb // I0iaGcRerMUsWOuBmKKKRmVMMdR9T3EZdpqsBd7jZCNMWotjhi // vd5zovQlFqQ2Wjc2+y46vKP/iXxWIuQJuDiisyXF0Y8+5GTpAL // pHDc1/pIGmMIGjoAMCAQGigZsEgZg2on5mSuxoDHEA1w9bcW9n // FdFxDKpdrQhVGVRDIzcCMCTzvUboqb5KjY1NJKJsfjRQiBYBdE // NKfzK+g5DlV8nrw81uOcP8NOQCLR5XkoMHC0Dr/80ziQzbNqhx // O6652Npft0LQwJvenwDI13YxpwOdMXzkWZN/XrEqOWp6GCgXTB // vCyLWLlWnbaUkZdEYbKHBPjd8t/1x5Yg== // S: + YGgGCSqGSIb3EgECAgIAb1kwV6ADAgEFoQMCAQ+iSzBJoAMC // AQGiQgRAtHTEuOP2BXb9sBYFR4SJlDZxmg39IxmRBOhXRKdDA0 // uHTCOT9Bq3OsUTXUlk0CsFLoa8j+gvGDlgHuqzWHPSQg== // C: // S: + YDMGCSqGSIb3EgECAgIBAAD/////6jcyG4GE3KkTzBeBiVHe // ceP2CWY0SR0fAQAgAAQEBAQ= // C: YDMGCSqGSIb3EgECAgIBAAD/////3LQBHXTpFfZgrejpLlLImP // wkhbfa2QteAQAgAG1yYwE= // S: A001 OK GSSAPI authentication successful } #[test] fn test_from_login() { let tests = { vec![ ( b"a001 LOGIN SMITH SESAME\r\n".as_ref(), // We know that `CommandBody::login()` will create two atoms. Message::Command( Command::new("a001", CommandBody::login("SMITH", "SESAME").unwrap()).unwrap(), ), ), ( // Addition: We change the previous command here to test a quoted string ... b"a001 LOGIN \"SMITH\" SESAME\r\n".as_ref(), Message::Command( Command::new( "a001", // ... and construct the command manually ... CommandBody::Login { // ... using a quoted string ... username: AString::String(IString::Quoted( Quoted::try_from("SMITH").unwrap(), )), // ... and an atom (knowing that `AString::try_from(...)` will create it. password: Secret::new(AString::try_from("SESAME").unwrap()), }, ) .unwrap(), ), ), ( b"a001 OK LOGIN completed\r\n", Message::Response(Response::Status( Status::ok( Some(Tag::try_from("a001").unwrap()), None, "LOGIN completed", ) .unwrap(), )), ), ] }; test_trace_known_positive(tests); } #[test] fn test_from_select() { let tests = { vec![ ( b"A142 SELECT INBOX\r\n".as_ref(), Message::Command( Command::new("A142", CommandBody::select("inbox").unwrap()).unwrap(), ), ), ( b"* 172 EXISTS\r\n", Message::Response(Response::Data(Data::Exists(172))), ), ( b"* 1 RECENT\r\n", Message::Response(Response::Data(Data::Recent(1))), ), ( b"* OK [UNSEEN 12] Message 12 is first unseen\r\n", Message::Response(Response::Status( Status::ok( None, Some(Code::unseen(12).unwrap()), "Message 12 is first unseen", ) .unwrap(), )), ), ( b"* OK [UIDVALIDITY 3857529045] UIDs valid\r\n", Message::Response(Response::Status( Status::ok( None, Some(Code::uidvalidity(3857529045).unwrap()), "UIDs valid", ) .unwrap(), )), ), ( b"* OK [UIDNEXT 4392] Predicted next UID\r\n", Message::Response(Response::Status( Status::ok( None, Some(Code::uidnext(4392).unwrap()), "Predicted next UID", ) .unwrap(), )), ), ( b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n", Message::Response(Response::Data(Data::Flags(vec![ Flag::Answered, Flag::Flagged, Flag::Deleted, Flag::Seen, Flag::Draft, ]))), ), ( b"* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n", Message::Response(Response::Status( Status::ok( None, Some(Code::PermanentFlags(vec![ FlagPerm::Flag(Flag::Deleted), FlagPerm::Flag(Flag::Seen), FlagPerm::Asterisk, ])), "Limited", ) .unwrap(), )), ), ( b"A142 OK [READ-WRITE] SELECT completed\r\n", // FIXME(API) Message::Response(Response::Status( Status::ok( Some(Tag::try_from("A142").unwrap()), Some(Code::ReadWrite), "SELECT completed", ) .unwrap(), )), ), ] }; test_trace_known_positive(tests); } #[test] fn test_from_examine() { let tests = { vec![ ( b"A932 EXAMINE blurdybloop\r\n".as_ref(), Message::Command( Command::new("A932", CommandBody::examine("blurdybloop").unwrap()).unwrap(), ), ), ( b"* 17 EXISTS\r\n", Message::Response(Response::Data(Data::Exists(17))), ), ( b"* 2 RECENT\r\n", Message::Response(Response::Data(Data::Recent(2))), ), ( b"* OK [UNSEEN 8] Message 8 is first unseen\r\n", Message::Response(Response::Status( Status::ok( None, Some(Code::unseen(8).unwrap()), "Message 8 is first unseen", ) .unwrap(), )), ), ( b"* OK [UIDVALIDITY 3857529045] UIDs valid\r\n", Message::Response(Response::Status( Status::ok( None, Some(Code::uidvalidity(3857529045).unwrap()), "UIDs valid", ) .unwrap(), )), ), ( b"* OK [UIDNEXT 4392] Predicted next UID\r\n", Message::Response(Response::Status( Status::ok( None, Some(Code::uidnext(4392).unwrap()), "Predicted next UID", ) .unwrap(), )), ), ( b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n", Message::Response(Response::Data(Data::Flags(vec![ Flag::Answered, Flag::Flagged, Flag::Deleted, Flag::Seen, Flag::Draft, ]))), ), ( b"* OK [PERMANENTFLAGS ()] No permanent flags permitted\r\n", Message::Response(Response::Status( Status::ok( None, Some(Code::PermanentFlags(vec![])), "No permanent flags permitted", ) .unwrap(), )), ), ( b"A932 OK [READ-ONLY] EXAMINE completed\r\n", // FIXME(API) Message::Response(Response::Status( Status::ok( Some(Tag::try_from("A932").unwrap()), Some(Code::ReadOnly), "EXAMINE completed", ) .unwrap(), )), ), ] }; test_trace_known_positive(tests); } #[test] fn test_from_create() { let trace = br#"C: A003 CREATE owatagusiam/ S: A003 OK CREATE completed C: A004 CREATE owatagusiam/blurdybloop S: A004 OK CREATE completed "#; test_lines_of_trace(trace); } #[test] fn test_from_delete() { let trace = br#"C: A682 LIST "" * S: * LIST () "/" blurdybloop S: * LIST (\Noselect) "/" foo S: * LIST () "/" foo/bar S: A682 OK LIST completed C: A683 DELETE blurdybloop S: A683 OK DELETE completed C: A684 DELETE foo S: A684 NO Name "foo" has inferior hierarchical names C: A685 DELETE foo/bar S: A685 OK DELETE Completed C: A686 LIST "" * S: * LIST (\Noselect) "/" foo S: A686 OK LIST completed C: A687 DELETE foo S: A687 OK DELETE Completed C: A82 LIST "" * S: * LIST () "." blurdybloop S: * LIST () "." foo S: * LIST () "." foo.bar S: A82 OK LIST completed C: A83 DELETE blurdybloop S: A83 OK DELETE completed C: A84 DELETE foo S: A84 OK DELETE Completed C: A85 LIST "" * S: * LIST () "." foo.bar S: A85 OK LIST completed C: A86 LIST "" % S: * LIST (\Noselect) "." foo S: A86 OK LIST completed "#; test_lines_of_trace(trace); } #[test] fn test_from_rename() { let trace = br#"C: A682 LIST "" * S: * LIST () "/" blurdybloop S: * LIST (\Noselect) "/" foo S: * LIST () "/" foo/bar S: A682 OK LIST completed C: A683 RENAME blurdybloop sarasoop S: A683 OK RENAME completed C: A684 RENAME foo zowie S: A684 OK RENAME Completed C: A685 LIST "" * S: * LIST () "/" sarasoop S: * LIST (\Noselect) "/" zowie S: * LIST () "/" zowie/bar S: A685 OK LIST completed C: Z432 LIST "" * S: * LIST () "." INBOX S: * LIST () "." INBOX.bar S: Z432 OK LIST completed C: Z433 RENAME INBOX old-mail S: Z433 OK RENAME completed C: Z434 LIST "" * S: * LIST () "." INBOX S: * LIST () "." INBOX.bar S: * LIST () "." old-mail S: Z434 OK LIST completed "#; test_lines_of_trace(trace); } #[test] fn test_from_subscribe() { let trace = br#"C: A002 SUBSCRIBE #news.comp.mail.mime S: A002 OK SUBSCRIBE completed "#; test_lines_of_trace(trace); } #[test] fn test_from_unsubscribe() { let trace = br#"C: A002 UNSUBSCRIBE #news.comp.mail.mime S: A002 OK UNSUBSCRIBE completed "#; test_lines_of_trace(trace); } #[test] fn test_from_list() { let trace = br#"C: A101 LIST "" "" S: * LIST (\Noselect) "/" "" S: A101 OK LIST Completed C: A102 LIST #news.comp.mail.misc "" S: * LIST (\Noselect) "." #news. S: A102 OK LIST Completed C: A103 LIST /usr/staff/jones "" S: * LIST (\Noselect) "/" / S: A103 OK LIST Completed C: A202 LIST ~/Mail/ % S: * LIST (\Noselect) "/" ~/Mail/foo S: * LIST () "/" ~/Mail/meetings S: A202 OK LIST completed "#; test_lines_of_trace(trace); } #[test] fn test_from_lsub() { let trace = br#"C: A002 LSUB "news." "comp.mail.*" S: * LSUB () "." #news.comp.mail.mime S: * LSUB () "." #news.comp.mail.misc S: A002 OK LSUB completed C: A003 LSUB "news." "comp.%" S: * LSUB (\NoSelect) "." #news.comp.mail S: A003 OK LSUB completed "#; test_lines_of_trace(trace); } #[test] fn test_from_status() { let trace = br#"C: A042 STATUS blurdybloop (UIDNEXT MESSAGES) S: * STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292) S: A042 OK STATUS completed "#; test_lines_of_trace(trace); } #[test] fn test_from_append() { // C: A003 APPEND saved-messages (\Seen) {310} // S: + Ready for literal data // C: Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST) // C: From: Fred Foobar // C: Subject: afternoon meeting // C: To: mooch@owatagu.siam.edu // C: Message-Id: // C: MIME-Version: 1.0 // C: Content-Type: TEXT/PLAIN; CHARSET=US-ASCII // C: // C: Hello Joe, do you think we can meet at 3:30 tomorrow? // C: // S: A003 OK APPEND completed } #[test] fn test_from_check() { let trace = br#"C: FXXZ CHECK S: FXXZ OK CHECK Completed "#; test_lines_of_trace(trace); } #[test] fn test_from_close() { let trace = br#"C: A341 CLOSE S: A341 OK CLOSE completed "#; test_lines_of_trace(trace); } #[test] fn test_from_expunge() { let trace = br#"C: A202 EXPUNGE S: * 3 EXPUNGE S: * 3 EXPUNGE S: * 5 EXPUNGE S: * 8 EXPUNGE S: A202 OK EXPUNGE completed "#; test_lines_of_trace(trace); } #[test] fn test_from_search() { // C: A284 SEARCH CHARSET UTF-8 TEXT {6} // C: XXXXXX let trace = br#"C: A282 SEARCH FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith" S: * SEARCH 2 84 882 S: A282 OK SEARCH completed C: A283 SEARCH TEXT "string not in mailbox" S: * SEARCH S: A283 OK SEARCH completed S: * SEARCH 43 S: A284 OK SEARCH completed "#; test_lines_of_trace(trace); } #[test] fn test_from_fetch() { // S: * 2 FETCH .... // S: * 3 FETCH .... // S: * 4 FETCH .... let trace = br#"C: A654 FETCH 2:4 (FLAGS BODY[HEADER.FIELDS (DATE FROM)]) S: A654 OK FETCH completed "#; test_lines_of_trace(trace); } #[test] fn test_from_store() { let trace = br#"C: A003 STORE 2:4 +FLAGS (\Deleted) S: * 2 FETCH (FLAGS (\Deleted \Seen)) S: * 3 FETCH (FLAGS (\Deleted)) S: * 4 FETCH (FLAGS (\Deleted \Flagged \Seen)) S: A003 OK STORE completed "#; test_lines_of_trace(trace); } #[test] fn test_from_copy() { let trace = br#"C: A003 COPY 2:4 MEETING S: A003 OK COPY completed "#; test_lines_of_trace(trace); } #[test] fn test_from_uid() { let trace = br#"C: A999 UID FETCH 4827313:4828442 FLAGS S: * 23 FETCH (FLAGS (\Seen) UID 4827313) S: * 24 FETCH (FLAGS (\Seen) UID 4827943) S: * 25 FETCH (FLAGS (\Seen) UID 4828442) S: A999 OK UID FETCH completed "#; test_lines_of_trace(trace); } //#[test] //fn test_from_X() { // let trace = br#"C: a441 CAPABILITY //S: * CAPABILITY IMAP4rev1 XPIG-LATIN //S: a441 OK CAPABILITY completed //C: A442 XPIG-LATIN //S: * XPIG-LATIN ow-nay eaking-spay ig-pay atin-lay //S: A442 OK XPIG-LATIN ompleted-cay"#; // // test_lines_of_trace(trace); //} #[test] fn test_transcript_from_rfc() { let tests = { vec![ ( b"* OK IMAP4rev1 Service Ready\r\n".as_ref(), Message::Response(Response::Status( Status::ok(None, None, "IMAP4rev1 Service Ready").unwrap(), )), ), ( b"a001 login mrc secret\r\n", Message::Command( Command::new("a001", CommandBody::login("mrc", "secret").unwrap()).unwrap(), ), ), ( b"a001 OK LOGIN completed\r\n", Message::Response(Response::Status( Status::ok( Some(Tag::try_from("a001").unwrap()), None, "LOGIN completed", ) .unwrap(), )), ), ( b"a002 select inbox\r\n", Message::Command( Command::new("a002", CommandBody::select("inbox").unwrap()).unwrap(), ), ), ( b"* 18 EXISTS\r\n", Message::Response(Response::Data(Data::Exists(18))), ), ( b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n", Message::Response(Response::Data(Data::Flags(vec![ Flag::Answered, Flag::Flagged, Flag::Deleted, Flag::Seen, Flag::Draft, ]))), ), ( b"* 2 RECENT\r\n", Message::Response(Response::Data(Data::Recent(2))), ), ( b"* OK [UNSEEN 17] Message 17 is the first unseen message\r\n", Message::Response(Response::Status( Status::ok( None, Some(Code::unseen(17).unwrap()), "Message 17 is the first unseen message", ) .unwrap(), )), ), ( b"* OK [UIDVALIDITY 3857529045] UIDs valid\r\n", Message::Response(Response::Status( Status::ok( None, Some(Code::uidvalidity(3857529045).unwrap()), "UIDs valid", ) .unwrap(), )), ), ( b"a002 OK [READ-WRITE] SELECT completed\r\n", Message::Response(Response::Status( Status::ok( Some(Tag::try_from("a002").unwrap()), Some(Code::ReadWrite), "SELECT completed", ) .unwrap(), )), ), ( b"a003 fetch 12 full\r\n", Message::Command( Command::new( "a003", CommandBody::fetch("12", Macro::Full, false).unwrap(), ) .unwrap(), ), ), ( b"* 12 FETCH (FLAGS (\\Seen) INTERNALDATE \"17-Jul-1996 02:44:25 -0700\" RFC822.SIZE 4286 ENVELOPE (\"Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\" \"IMAP4rev1 WG mtg summary and minutes\" ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) ((NIL NIL \"imap\" \"cac.washington.edu\")) ((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\")(\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) NIL NIL \"\") BODY (\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL \"7BIT\" 3028 92))\r\n", Message::Response(Response::Data( Data::fetch( 12, vec![ MessageDataItem::Flags(vec![FlagFetch::Flag(Flag::Seen)]), MessageDataItem::InternalDate(DateTime::try_from( chrono::DateTime::parse_from_rfc3339("1996-07-17T02:44:25-07:00") .unwrap(), ).unwrap()), MessageDataItem::Rfc822Size(4286), MessageDataItem::Envelope(Envelope { date: NString::from( Quoted::try_from("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)") .unwrap(), ), subject: NString::from( Quoted::try_from("IMAP4rev1 WG mtg summary and minutes") .unwrap(), ), from: vec![Address { name: NString::from(Quoted::try_from("Terry Gray").unwrap()), adl: NString(None), mailbox: NString::from(Quoted::try_from("gray").unwrap()), host: NString::from( Quoted::try_from("cac.washington.edu").unwrap(), ), }], sender: vec![Address { name: NString::from(Quoted::try_from("Terry Gray").unwrap()), adl: NString(None), mailbox: NString::from(Quoted::try_from("gray").unwrap()), host: NString::from( Quoted::try_from("cac.washington.edu").unwrap(), ), }], reply_to: vec![Address { name: NString::from(Quoted::try_from("Terry Gray").unwrap()), adl: NString(None), mailbox: NString::from(Quoted::try_from("gray").unwrap()), host: NString::from( Quoted::try_from("cac.washington.edu").unwrap(), ), }], to: vec![Address { name: NString(None), adl: NString(None), mailbox: NString::from(Quoted::try_from("imap").unwrap()), host: NString::from( Quoted::try_from("cac.washington.edu").unwrap(), ), }], cc: vec![ Address { name: NString(None), adl: NString(None), mailbox: NString::from( Quoted::try_from("minutes").unwrap(), ), host: NString::from( Quoted::try_from("CNRI.Reston.VA.US").unwrap(), ), }, Address { name: NString::from( Quoted::try_from("John Klensin").unwrap(), ), adl: NString(None), mailbox: NString::from( Quoted::try_from("KLENSIN").unwrap(), ), host: NString::from(Quoted::try_from("MIT.EDU").unwrap()), }, ], bcc: vec![], in_reply_to: NString(None), message_id: NString::from( Quoted::try_from("") .unwrap(), ), }), MessageDataItem::Body(BodyStructure::Single { body: Body { basic: BasicFields { parameter_list: vec![( IString::from(Quoted::try_from("CHARSET").unwrap()), IString::from(Quoted::try_from("US-ASCII").unwrap()), )], id: NString(None), description: NString(None), content_transfer_encoding: IString::from( Quoted::try_from("7BIT").unwrap(), ), size: 3028, }, specific: SpecificFields::Text { subtype: IString::from(Quoted::try_from("PLAIN").unwrap()), number_of_lines: 92, }, }, extension_data: None, }), ], ) .unwrap(), )), ), ( b"a003 OK FETCH completed\r\n", Message::Response(Response::Status( Status::ok( Some(Tag::try_from("a003").unwrap()), None, "FETCH completed", ) .unwrap(), )), ), ( b"a004 fetch 12 body[header]\r\n", Message::Command( Command::new( "a004", CommandBody::fetch( "12", vec![MessageDataItemName::BodyExt { section: Some(Section::Header(None)), peek: false, partial: None, }], false, ) .unwrap(), ) .unwrap(), ), ), ( b"* 12 FETCH (BODY[HEADER] {342}\r Date: Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\r From: Terry Gray \r Subject: IMAP4rev1 WG mtg summary and minutes\r To: imap@cac.washington.edu\r cc: minutes@CNRI.Reston.VA.US, John Klensin \r Message-Id: \r MIME-Version: 1.0\r Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r \r )\r\n", Message::Response(Response::Data( Data::fetch( 12, vec![MessageDataItem::BodyExt { section: Some(Section::Header(None)), origin: None, data: NString::from( Literal::try_from( b"Date: Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\r From: Terry Gray \r Subject: IMAP4rev1 WG mtg summary and minutes\r To: imap@cac.washington.edu\r cc: minutes@CNRI.Reston.VA.US, John Klensin \r Message-Id: \r MIME-Version: 1.0\r Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r \r " .as_ref(), ) .unwrap(), ), }], ) .unwrap(), )), ), ( b"a004 OK FETCH completed\r\n", Message::Response(Response::Status( Status::ok( Some(Tag::try_from("a004").unwrap()), None, "FETCH completed", ) .unwrap(), )), ), ( b"a005 store 12 +flags \\deleted\r\n", Message::Command( Command::new( "a005", CommandBody::store( "12", StoreType::Add, StoreResponse::Answer, vec![Flag::Deleted], false, ) .unwrap(), ) .unwrap(), ), ), ( b"* 12 FETCH (FLAGS (\\Seen \\Deleted))\r\n", Message::Response(Response::Data( Data::fetch( 12, vec![MessageDataItem::Flags(vec![ FlagFetch::Flag(Flag::Seen), FlagFetch::Flag(Flag::Deleted), ])], ) .unwrap(), )), ), ( b"a005 OK +FLAGS completed\r\n", Message::Response(Response::Status( Status::ok( Some(Tag::try_from("a005").unwrap()), None, "+FLAGS completed", ) .unwrap(), )), ), ( b"a006 logout\r\n", Message::Command(Command::new("a006", CommandBody::Logout).unwrap()), ), ( b"* BYE IMAP4rev1 server terminating connection\r\n", Message::Response(Response::Status( Status::bye(None, "IMAP4rev1 server terminating connection").unwrap(), )), ), ( b"a006 OK LOGOUT completed\r\n", Message::Response(Response::Status( Status::ok( Some(Tag::try_from("a006").unwrap()), None, "LOGOUT completed", ) .unwrap(), )), ), ] }; test_trace_known_positive(tests); } #[test] fn test_transcript_from_rfc5161() { let trace = br#"C: t1 CAPABILITY S: * CAPABILITY IMAP4rev1 ID LITERAL+ ENABLE X-GOOD-IDEA S: t1 OK foo C: t2 ENABLE CONDSTORE X-GOOD-IDEA S: * ENABLED X-GOOD-IDEA S: t2 OK foo C: t3 CAPABILITY S: * CAPABILITY IMAP4rev1 ID LITERAL+ ENABLE X-GOOD-IDEA S: t3 OK foo again C: a1 ENABLE CONDSTORE S: * ENABLED CONDSTORE S: a1 OK Conditional Store enabled"#; test_lines_of_trace(trace); } #[test] fn test_response_status_ok() { let trace = br#"S: * OK IMAP4rev1 server ready C: A001 LOGIN fred blurdybloop S: * OK [ALERT] System shutdown in 10 minutes S: A001 OK LOGIN Completed "#; test_lines_of_trace(trace); } #[test] fn test_response_status_no() { let trace = br#"C: A222 COPY 1:2 owatagusiam S: * NO Disk is 98% full, please delete unnecessary data S: A222 OK COPY completed C: A223 COPY 3:200 blurdybloop S: * NO Disk is 98% full, please delete unnecessary data S: * NO Disk is 99% full, please delete unnecessary data S: A223 NO COPY failed: disk is full "#; test_lines_of_trace(trace); } #[test] fn test_response_status_bad() { let trace = br#"S: * BAD Command line too long S: * BAD Empty command line C: A443 EXPUNGE S: * BAD Disk crash, attempting salvage to a new disk! S: * OK Salvage successful, no data lost S: A443 OK Expunge completed "#; test_lines_of_trace(trace); } #[test] fn test_response_status_preauth() { let line = b"* PREAUTH IMAP4rev1 server logged in as Smith\r\n"; println!("S: {}", String::from_utf8_lossy(line).trim()); let (rem, parsed) = GreetingCodec::default().decode(line).unwrap(); println!("Parsed: {:?}", parsed); assert!(rem.is_empty()); let serialized = GreetingCodec::default().encode(&parsed).dump(); println!( "Serialized: {}", String::from_utf8_lossy(&serialized).trim() ); let (rem, parsed2) = GreetingCodec::default().decode(&serialized).unwrap(); assert!(rem.is_empty()); assert_eq!(parsed, parsed2); println!() } #[test] fn test_response_status_bye() { let trace = br#"S: * BYE Autologout; idle for too long "#; test_lines_of_trace(trace); } #[test] fn test_response_data_capability() { let trace = br#"S: * CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI XPIG-LATIN "#; test_lines_of_trace(trace); } #[test] fn test_response_data_list() { let trace = br#"S: * LIST (\Noselect) "/" ~/Mail/foo "#; test_lines_of_trace(trace); } #[test] fn test_response_data_lsub() { let trace = br#"S: * LSUB () "." #news.comp.mail.misc "#; test_lines_of_trace(trace); } #[test] fn test_response_data_status() { let trace = br#"S: * STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292) "#; test_lines_of_trace(trace); } #[test] fn test_response_data_search() { let trace = br#"S: * SEARCH 2 3 6 "#; test_lines_of_trace(trace); } #[test] fn test_response_data_flags() { let trace = br#"S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) "#; test_lines_of_trace(trace); } #[test] fn test_response_data_exists() { let trace = br#"S: * 23 EXISTS "#; test_lines_of_trace(trace); } #[test] fn test_response_data_recent() { let trace = br#"S: * 5 RECENT "#; test_lines_of_trace(trace); } #[test] fn test_response_data_expunge() { let trace = br#"S: * 44 EXPUNGE "#; test_lines_of_trace(trace); } #[test] fn test_response_data_fetch() { let trace = br#"S: * 23 FETCH (FLAGS (\Seen) RFC822.SIZE 44827) "#; test_lines_of_trace(trace); } #[test] fn test_response_data_continue() { // C: A001 LOGIN {11} // C: FRED FOOBAR {7} // C: fat man // C: A044 BLURDYBLOOP {102856} let trace = br#"S: + Ready for additional command text S: A001 OK LOGIN completed S: A044 BAD No such command as "BLURDYBLOOP" "#; test_lines_of_trace(trace); } #[test] fn test_trace_rfc2088() { let test = b"A001 LOGIN {11+}\r\nFRED FOOBAR {7+}\r\nfat man\r\n".as_ref(); let (rem, got) = CommandCodec::default().decode(test).unwrap(); assert!(rem.is_empty()); assert_eq!(got, { let username = Literal::try_from("FRED FOOBAR").unwrap().into_non_sync(); let password = Literal::try_from("fat man").unwrap().into_non_sync(); Command::new( Tag::try_from("A001").unwrap(), CommandBody::login(username, password).unwrap(), ) .unwrap() }) } imap-codec-1.0.0/imap-types/000077500000000000000000000000001447115025300156035ustar00rootroot00000000000000imap-codec-1.0.0/imap-types/Cargo.toml000066400000000000000000000025521447115025300175370ustar00rootroot00000000000000[package] name = "imap-types" description = "Misuse-resistant data structures for IMAP" keywords = ["email", "imap", "types"] categories = ["email", "data-structures", "network-programming"] version = "1.0.0" authors = ["Damian Poddebniak "] repository = "https://github.com/duesee/imap-codec" license = "MIT OR Apache-2.0" edition = "2021" [features] arbitrary = ["dep:arbitrary", "unvalidated", "chrono/arbitrary", "chrono/std"] bounded-static = ["dep:bounded-static", "bounded-static/derive"] serde = ["dep:serde", "chrono/serde"] # IMAP starttls = [] # IMAP Extensions ext_condstore_qresync = [] ext_login_referrals = [] ext_mailbox_referrals = [] # Unlock `unvalidated` constructors. unvalidated = [] [dependencies] arbitrary = { version = "1.0.1", optional = true, features = ["derive"] } base64 = "0.21" bounded-static = { version = "0.5.0", optional = true } chrono = { version = "0.4", default-features = false, features = ["alloc"] } serde = { version = "1.0.103", features = ["derive"], optional = true } thiserror = "1.0.29" [dev-dependencies] criterion = "0.5.1" rand = { version = "0.8", default-features = false, features = ["small_rng"] } serde_json = "1.0.100" [[example]] name = "serde_json" path = "examples/serde_json.rs" required-features = ["serde"] [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] imap-codec-1.0.0/imap-types/README.md000066400000000000000000000061631447115025300170700ustar00rootroot00000000000000# imap-types This crate provides a complete set of well-designed, misuse-resistant types for the [IMAP4rev1] protocol and various [extensions]. Notably, it does *not* provide parsers, nor serializers, but tries to become the "standard library" for IMAP in Rust that is useful for a broad range of crates. If you are looking for a complete codec implementation, i.e., parsers, serializers, and network support, head over to [imap-codec]. ## Features * Rust's type system is used to enforce correctness and to make the library misuse-resistant. It's not possible to construct a message that violates the IMAP specification. * Fuzzing (via [cargo fuzz]) and property-based tests are used to uncover bugs. The library is fuzz-tested never to produce an invalid message. ## Working with imap-types To ensure correctness, imap-types makes use of types such as [`AString`](core::AString), [`Atom`](core::Atom), [`IString`](core::IString), [`Quoted`](core::Quoted), and [`Literal`](core::Literal). When constructing messages, imap-types can automatically choose the best representation. However, it's always possible to manually choose a specific representation. ### Examples
Automatic Construction This ... ```rust Command::new( "A1", CommandBody::login("alice", "password").unwrap(), ).unwrap(); ``` ... will produce ... ```imap A1 LOGIN alice password ``` However, ... ```rust Command::new( "A1", CommandBody::login("alice\"", b"\xCA\xFE".as_ref()).unwrap(), ) .unwrap(); ``` ... will produce ... ```imap A1 LOGIN "alice\"" {2} \xCA\xFE ``` Also, the construction ... ```rust Command::new( "A1", CommandBody::login("alice\x00", "password").unwrap(), ).unwrap(); ``` ... will fail because IMAP doesn't allow NULL bytes in the username (nor password).
Manual Construction You can also use ... ```rust Command::new( "A1", CommandBody::login(Literal::try_from("alice").unwrap(), "password").unwrap(), ) .unwrap(); ``` ... to produce ... ```imap A1 LOGIN {5} alice password ``` ... even though "alice" could be encoded more simply with an atom or quoted string. Also, you can use Rust literals and resort to `unvalidated` constructors when you are certain that your input is correct: ```rust // This could be provided by the email application. let tag = TagGenerator::random(); Command { tag, body: CommandBody::Login { // Note that the "unvalidated" feature must be activated. username: AString::from(Atom::unvalidated("alice")), password: Secret::new(AString::from(Atom::unvalidated("password"))), }, }; ``` In this case, imap-codec won't stand in your way. However, it won't guarantee that you produce correct messages, either.
# License This crate is dual-licensed under Apache 2.0 and MIT terms. [IMAP4rev1]: https://datatracker.ietf.org/doc/html/rfc3501 [extensions]: https://docs.rs/imap-codec/latest/imap_codec/#features [imap-codec]: https://docs.rs/imap-types/latest/imap_codec/ [cargo fuzz]: https://github.com/rust-fuzz/cargo-fuzz [core]: https://docs.rs/imap-types/latest/imap_types/core/index.html imap-codec-1.0.0/imap-types/examples/000077500000000000000000000000001447115025300174215ustar00rootroot00000000000000imap-codec-1.0.0/imap-types/examples/serde_json.rs000066400000000000000000000010421447115025300221170ustar00rootroot00000000000000use imap_types::{ command::{Command, CommandBody}, core::{Tag, Text}, response::{Response, Status}, }; fn main() { let cmd = Command::new("A1", CommandBody::login("Alice", "Pa²²word").unwrap()).unwrap(); println!("{:?}\n{}", cmd, serde_json::to_string_pretty(&cmd).unwrap()); let rsp = Response::Status(Status::Ok { tag: Some(Tag::try_from("A1").unwrap()), code: None, text: Text::try_from("...").unwrap(), }); println!("{:?}\n{}", rsp, serde_json::to_string_pretty(&rsp).unwrap()); } imap-codec-1.0.0/imap-types/fuzz/000077500000000000000000000000001447115025300166015ustar00rootroot00000000000000imap-codec-1.0.0/imap-types/fuzz/.gitignore000066400000000000000000000000311447115025300205630ustar00rootroot00000000000000 target corpus artifacts imap-codec-1.0.0/imap-types/fuzz/Cargo.toml000066400000000000000000000020451447115025300205320ustar00rootroot00000000000000[package] name = "imap-types-fuzz" version = "0.0.0" authors = ["Automatically generated"] publish = false edition = "2021" license = "MIT OR Apache-2.0" [package.metadata] cargo-fuzz = true [features] # # IMAP starttls = ["imap-types/starttls"] # IMAP Extensions ext_condstore_qresync = ["imap-types/ext_condstore_qresync"] ext_login_referrals = ["imap-types/ext_login_referrals"] ext_mailbox_referrals = ["imap-types/ext_mailbox_referrals"] # # Use (most) IMAP extensions. ext = [ "starttls", "ext_condstore_qresync", #"ext_login_referrals", #"ext_mailbox_referrals", ] # Enable `Debug`-printing during parsing. This is useful to analyze crashes. debug = [] [dependencies] libfuzzer-sys = "0.4" imap-types = { path = "..", default-features = false, features = ["arbitrary", "bounded-static", "unvalidated"] } [[bin]] name = "into_static" path = "fuzz_targets/into_static.rs" test = false doc = false [[bin]] name = "to_static" path = "fuzz_targets/to_static.rs" test = false doc = false imap-codec-1.0.0/imap-types/fuzz/fuzz_targets/000077500000000000000000000000001447115025300213305ustar00rootroot00000000000000imap-codec-1.0.0/imap-types/fuzz/fuzz_targets/into_static.rs000066400000000000000000000007131447115025300242170ustar00rootroot00000000000000#![no_main] use imap_types::{ bounded_static::IntoBoundedStatic, command::Command, response::{Greeting, Response}, }; use libfuzzer_sys::fuzz_target; fuzz_target!(|tuple: (Greeting, Command, Response)| { let (grt, cmd, rsp) = tuple; let got = grt.clone().into_static(); assert_eq!(grt, got); let got = cmd.clone().into_static(); assert_eq!(cmd, got); let got = rsp.clone().into_static(); assert_eq!(rsp, got); }); imap-codec-1.0.0/imap-types/fuzz/fuzz_targets/to_static.rs000066400000000000000000000006531447115025300236730ustar00rootroot00000000000000#![no_main] use imap_types::{ bounded_static::ToBoundedStatic, command::Command, response::{Greeting, Response}, }; use libfuzzer_sys::fuzz_target; fuzz_target!(|tuple: (Greeting, Command, Response)| { let (grt, cmd, rsp) = tuple; let got = grt.to_static(); assert_eq!(grt, got); let got = cmd.to_static(); assert_eq!(cmd, got); let got = rsp.to_static(); assert_eq!(rsp, got); }); imap-codec-1.0.0/imap-types/src/000077500000000000000000000000001447115025300163725ustar00rootroot00000000000000imap-codec-1.0.0/imap-types/src/arbitrary.rs000066400000000000000000000454661447115025300207560ustar00rootroot00000000000000use arbitrary::{Arbitrary, Unstructured}; use chrono::{FixedOffset, TimeZone}; use crate::{ auth::AuthMechanism, body::{ BasicFields, Body, BodyExtension, BodyStructure, MultiPartExtensionData, SinglePartExtensionData, SpecificFields, }, core::{ AString, Atom, AtomExt, IString, Literal, LiteralMode, NString, NonEmptyVec, Quoted, QuotedChar, Tag, Text, }, datetime::{DateTime, NaiveDate}, envelope::Envelope, extensions::{enable::CapabilityEnable, quota::Resource}, flag::{Flag, FlagNameAttribute}, mailbox::{ListCharString, Mailbox, MailboxOther}, response::{ Capability, Code, CodeOther, CommandContinuationRequestBasic, Greeting, GreetingKind, Status, }, search::SearchKey, sequence::SequenceSet, }; macro_rules! implement_tryfrom { ($target:ty, $from:ty) => { impl<'a> Arbitrary<'a> for $target { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { match <$target>::try_from(<$from>::arbitrary(u)?) { Ok(passed) => Ok(passed), Err(_) => Err(arbitrary::Error::IncorrectFormat), } } } }; } macro_rules! implement_tryfrom_t { ($target:ty, $from:ty) => { impl<'a, T> Arbitrary<'a> for $target where T: Arbitrary<'a>, { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { match <$target>::try_from(<$from>::arbitrary(u)?) { Ok(passed) => Ok(passed), Err(_) => Err(arbitrary::Error::IncorrectFormat), } } } }; } implement_tryfrom! { Atom<'a>, &str } implement_tryfrom! { AtomExt<'a>, &str } implement_tryfrom! { Quoted<'a>, &str } implement_tryfrom! { Tag<'a>, &str } implement_tryfrom! { Text<'a>, &str } implement_tryfrom! { ListCharString<'a>, &str } implement_tryfrom! { QuotedChar, char } implement_tryfrom! { Mailbox<'a>, &str } implement_tryfrom! { Capability<'a>, Atom<'a> } implement_tryfrom! { Flag<'a>, &str } implement_tryfrom! { FlagNameAttribute<'a>, Atom<'a> } implement_tryfrom! { MailboxOther<'a>, AString<'a> } implement_tryfrom! { CapabilityEnable<'a>, &str } implement_tryfrom! { Resource<'a>, &str } implement_tryfrom! { AuthMechanism<'a>, &str } implement_tryfrom_t! { NonEmptyVec, Vec } impl<'a> Arbitrary<'a> for CommandContinuationRequestBasic<'a> { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { Self::new(Option::::arbitrary(u)?, Text::arbitrary(u)?) .map_err(|_| arbitrary::Error::IncorrectFormat) } } // TODO(#301): This is due to the `Code`/`Text` ambiguity. impl<'a> Arbitrary<'a> for Greeting<'a> { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { Ok(Greeting { kind: GreetingKind::arbitrary(u)?, code: Option::::arbitrary(u)?, text: { let text = Text::arbitrary(u)?; if text.as_ref().starts_with('[') { Text::unvalidated("...") } else { text } }, }) } } // TODO(#301): This is due to the `Code`/`Text` ambiguity. impl<'a> Arbitrary<'a> for Status<'a> { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { let code = Option::::arbitrary(u)?; let text = if code.is_some() { Arbitrary::arbitrary(u)? } else { let text = Text::arbitrary(u)?; if text.as_ref().starts_with('[') { Text::unvalidated("...") } else { text } }; Ok(match u.int_in_range(0u8..=3)? { 0 => Status::Ok { tag: Arbitrary::arbitrary(u)?, code, text, }, 1 => Status::No { tag: Arbitrary::arbitrary(u)?, code, text, }, 2 => Status::Bad { tag: Arbitrary::arbitrary(u)?, code, text, }, 3 => Status::Bye { code, text }, _ => unreachable!(), }) } } impl<'a> Arbitrary<'a> for Literal<'a> { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { match Literal::try_from(<&[u8]>::arbitrary(u)?) { Ok(mut passed) => { passed.mode = LiteralMode::arbitrary(u)?; Ok(passed) } Err(_) => Err(arbitrary::Error::IncorrectFormat), } } } impl<'a> Arbitrary<'a> for CodeOther<'a> { fn arbitrary(_: &mut Unstructured<'a>) -> arbitrary::Result { // `CodeOther` is a fallback and should usually not be created. Ok(CodeOther::unvalidated(b"IMAP-CODEC-CODE-OTHER>".as_ref())) } } impl<'a> Arbitrary<'a> for SearchKey<'a> { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { fn make_search_key<'a>(u: &mut Unstructured<'a>) -> arbitrary::Result> { Ok(match u.int_in_range(0u8..=33)? { 0 => SearchKey::SequenceSet(SequenceSet::arbitrary(u)?), 1 => SearchKey::All, 2 => SearchKey::Answered, 3 => SearchKey::Bcc(AString::arbitrary(u)?), 4 => SearchKey::Before(NaiveDate::arbitrary(u)?), 5 => SearchKey::Body(AString::arbitrary(u)?), 6 => SearchKey::Cc(AString::arbitrary(u)?), 7 => SearchKey::Deleted, 8 => SearchKey::Draft, 9 => SearchKey::Flagged, 10 => SearchKey::From(AString::arbitrary(u)?), 11 => SearchKey::Header(AString::arbitrary(u)?, AString::arbitrary(u)?), 12 => SearchKey::Keyword(Atom::arbitrary(u)?), 13 => SearchKey::Larger(u32::arbitrary(u)?), 14 => SearchKey::New, 15 => SearchKey::Old, 16 => SearchKey::On(NaiveDate::arbitrary(u)?), 17 => SearchKey::Recent, 18 => SearchKey::Seen, 19 => SearchKey::SentBefore(NaiveDate::arbitrary(u)?), 20 => SearchKey::SentOn(NaiveDate::arbitrary(u)?), 21 => SearchKey::SentSince(NaiveDate::arbitrary(u)?), 22 => SearchKey::Since(NaiveDate::arbitrary(u)?), 23 => SearchKey::Smaller(u32::arbitrary(u)?), 24 => SearchKey::Subject(AString::arbitrary(u)?), 25 => SearchKey::Text(AString::arbitrary(u)?), 26 => SearchKey::To(AString::arbitrary(u)?), 27 => SearchKey::Uid(SequenceSet::arbitrary(u)?), 28 => SearchKey::Unanswered, 29 => SearchKey::Undeleted, 30 => SearchKey::Undraft, 31 => SearchKey::Unflagged, 32 => SearchKey::Unkeyword(Atom::arbitrary(u)?), 33 => SearchKey::Unseen, _ => unreachable!(), }) } fn make_search_key_rec<'a>( u: &mut Unstructured<'a>, depth: u8, ) -> arbitrary::Result> { if depth == 0 { return make_search_key(u); } Ok(match u.int_in_range(0u8..=36)? { 0 => SearchKey::And({ let keys = { let len = u.arbitrary_len::()?; let mut tmp = Vec::with_capacity(len); for _ in 0..len { tmp.push(make_search_key_rec(u, depth - 1)?); } tmp }; if !keys.is_empty() { NonEmptyVec::try_from(keys).unwrap() } else { NonEmptyVec::from(make_search_key(u)?) } }), 1 => SearchKey::SequenceSet(SequenceSet::arbitrary(u)?), 2 => SearchKey::All, 3 => SearchKey::Answered, 4 => SearchKey::Bcc(AString::arbitrary(u)?), 5 => SearchKey::Before(NaiveDate::arbitrary(u)?), 6 => SearchKey::Body(AString::arbitrary(u)?), 7 => SearchKey::Cc(AString::arbitrary(u)?), 8 => SearchKey::Deleted, 9 => SearchKey::Draft, 10 => SearchKey::Flagged, 11 => SearchKey::From(AString::arbitrary(u)?), 12 => SearchKey::Header(AString::arbitrary(u)?, AString::arbitrary(u)?), 13 => SearchKey::Keyword(Atom::arbitrary(u)?), 14 => SearchKey::Larger(u32::arbitrary(u)?), 15 => SearchKey::New, 16 => SearchKey::Not(Box::new(make_search_key_rec(u, depth - 1)?)), 17 => SearchKey::Old, 18 => SearchKey::On(NaiveDate::arbitrary(u)?), 19 => SearchKey::Or( Box::new(make_search_key_rec(u, depth - 1)?), Box::new(make_search_key_rec(u, depth - 1)?), ), 20 => SearchKey::Recent, 21 => SearchKey::Seen, 22 => SearchKey::SentBefore(NaiveDate::arbitrary(u)?), 23 => SearchKey::SentOn(NaiveDate::arbitrary(u)?), 24 => SearchKey::SentSince(NaiveDate::arbitrary(u)?), 25 => SearchKey::Since(NaiveDate::arbitrary(u)?), 26 => SearchKey::Smaller(u32::arbitrary(u)?), 27 => SearchKey::Subject(AString::arbitrary(u)?), 28 => SearchKey::Text(AString::arbitrary(u)?), 29 => SearchKey::To(AString::arbitrary(u)?), 30 => SearchKey::Uid(SequenceSet::arbitrary(u)?), 31 => SearchKey::Unanswered, 32 => SearchKey::Undeleted, 33 => SearchKey::Undraft, 34 => SearchKey::Unflagged, 35 => SearchKey::Unkeyword(Atom::arbitrary(u)?), 36 => SearchKey::Unseen, _ => unreachable!(), }) } make_search_key_rec(u, 7) } } impl<'a> Arbitrary<'a> for BodyStructure<'a> { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { fn make_body_structure_terminator<'a>( u: &mut Unstructured<'a>, ) -> arbitrary::Result> { Ok(BodyStructure::Single { body: Body { basic: BasicFields::arbitrary(u)?, specific: match u.int_in_range(1..=2)? { 1 => SpecificFields::Basic { r#type: IString::arbitrary(u)?, subtype: IString::arbitrary(u)?, }, // No SpecificFields::Message because it would recurse. 2 => SpecificFields::Text { subtype: IString::arbitrary(u)?, number_of_lines: u32::arbitrary(u)?, }, _ => unreachable!(), }, }, extension_data: Option::::arbitrary(u)?, }) } fn make_body_structure_rec<'a>( u: &mut Unstructured<'a>, depth: u8, ) -> arbitrary::Result> { if depth == 0 { return make_body_structure_terminator(u); } Ok(match u.int_in_range(1..=2)? { 1 => BodyStructure::Single { body: Body { basic: BasicFields::arbitrary(u)?, specific: match u.int_in_range(1..=3)? { 1 => SpecificFields::Basic { r#type: IString::arbitrary(u)?, subtype: IString::arbitrary(u)?, }, 2 => SpecificFields::Message { envelope: Box::::arbitrary(u)?, body_structure: Box::new(make_body_structure_rec(u, depth - 1)?), number_of_lines: u32::arbitrary(u)?, }, 3 => SpecificFields::Text { subtype: IString::arbitrary(u)?, number_of_lines: u32::arbitrary(u)?, }, _ => unreachable!(), }, }, extension_data: Option::::arbitrary(u)?, }, 2 => BodyStructure::Multi { bodies: { let bodies = { let len = u.arbitrary_len::()?; let mut tmp = Vec::with_capacity(len); for _ in 0..len { tmp.push(make_body_structure_rec(u, depth - 1)?); } tmp }; if !bodies.is_empty() { NonEmptyVec::try_from(bodies).unwrap() } else { NonEmptyVec::from(make_body_structure_terminator(u)?) } }, subtype: IString::arbitrary(u)?, extension_data: Option::::arbitrary(u)?, }, _ => unreachable!(), }) } make_body_structure_rec(u, 3) } } impl<'a> Arbitrary<'a> for BodyExtension<'a> { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { fn make_body_extension_terminator<'a>( u: &mut Unstructured<'a>, ) -> arbitrary::Result> { Ok(match u.int_in_range(1..=2)? { 1 => BodyExtension::NString(NString::arbitrary(u)?), 2 => BodyExtension::Number(u32::arbitrary(u)?), // No `BodyExtension::List` because it could recurse. _ => unreachable!(), }) } fn make_body_extension_rec<'a>( u: &mut Unstructured<'a>, depth: u8, ) -> arbitrary::Result> { if depth == 0 { return make_body_extension_terminator(u); } Ok(match u.int_in_range(1..=2)? { 1 => BodyExtension::NString(NString::arbitrary(u)?), 2 => BodyExtension::Number(u32::arbitrary(u)?), 3 => BodyExtension::List({ let body_extensions = { let len = u.arbitrary_len::()?; let mut tmp = Vec::with_capacity(len); for _ in 0..len { tmp.push(make_body_extension_rec(u, depth - 1)?); } tmp }; if !body_extensions.is_empty() { NonEmptyVec::try_from(body_extensions).unwrap() } else { NonEmptyVec::from(make_body_extension_terminator(u)?) } }), _ => unreachable!(), }) } make_body_extension_rec(u, 3) } } impl<'a> Arbitrary<'a> for DateTime { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { // Note: `chrono`s `NaiveDate::arbitrary` may `panic!`. // Thus, we implement this manually here. let local_datetime = chrono::NaiveDateTime::new( chrono::NaiveDate::from_ymd_opt( u.int_in_range(0..=9999)?, u.int_in_range(1..=12)?, u.int_in_range(1..=31)?, ) .ok_or(arbitrary::Error::IncorrectFormat)?, chrono::NaiveTime::arbitrary(u)?, ); let hours = u.int_in_range(0..=23 * 3600)?; let minutes = u.int_in_range(0..=59)? * 60; // Seconds must be zero due to IMAPs encoding. DateTime::try_from( FixedOffset::east_opt(hours + minutes) .unwrap() .from_local_datetime(&local_datetime) .unwrap(), ) .map_err(|_| arbitrary::Error::IncorrectFormat) } } impl<'a> Arbitrary<'a> for NaiveDate { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { NaiveDate::try_from(chrono::NaiveDate::arbitrary(u)?) .map_err(|_| arbitrary::Error::IncorrectFormat) } } #[cfg(test)] mod tests { use arbitrary::{Arbitrary, Error, Unstructured}; #[cfg(feature = "bounded-static")] use bounded_static::{IntoBoundedStatic, ToBoundedStatic}; use rand::{rngs::SmallRng, Rng, SeedableRng}; use crate::{ command::Command, response::{Greeting, Response}, }; /// Note: We could encode/decode/etc. here but only want to exercise the arbitrary logic itself. macro_rules! impl_test_arbitrary { ($object:ty) => { let mut rng = SmallRng::seed_from_u64(1337); let mut data = [0u8; 256]; // Randomize. rng.try_fill(&mut data).unwrap(); let mut unstructured = Unstructured::new(&data); let mut count = 0; loop { match <$object>::arbitrary(&mut unstructured) { Ok(_out) => { count += 1; #[cfg(feature = "bounded-static")] { let out_to_static = _out.to_static(); assert_eq!(_out, out_to_static); let out_into_static = _out.into_static(); assert_eq!(out_to_static, out_into_static); } if count >= 1_000 { break; } } Err(Error::NotEnoughData | Error::IncorrectFormat) => { // Randomize. rng.try_fill(&mut data).unwrap(); unstructured = Unstructured::new(&data); } Err(Error::EmptyChoose) => { unreachable!(); } Err(_) => { unimplemented!() } } } }; } #[test] fn test_arbitrary_greeting() { impl_test_arbitrary! {Greeting}; } #[test] fn test_arbitrary_command() { impl_test_arbitrary! {Command}; } #[test] fn test_arbitrary_response() { impl_test_arbitrary! {Response}; } } imap-codec-1.0.0/imap-types/src/auth.rs000066400000000000000000000073401447115025300177050ustar00rootroot00000000000000//! Authentication-related types. use std::{ borrow::Cow, fmt::{Display, Formatter}, }; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{ core::{impl_try_from, Atom}, secret::Secret, }; /// Authentication mechanism. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum AuthMechanism<'a> { /// The PLAIN SASL mechanism. /// /// ```imap /// AUTH=PLAIN /// ``` /// /// ```text /// base64(b"\x00\x00") /// ``` /// /// # Reference(s): /// /// * RFC4616: The PLAIN Simple Authentication and Security Layer (SASL) Mechanism Plain, /// The (non-standardized and slow) LOGIN SASL mechanism. /// /// ```imap /// AUTH=LOGIN /// ``` /// /// ```text /// base64(b"") /// base64(b"") /// ``` /// /// # Reference(s): /// /// + draft-murchison-sasl-login-00: The LOGIN SASL Mechanism Login, /// Google's OAuth 2.0 mechanism. /// /// ```imap /// AUTH=XOAUTH2 /// ``` /// /// ```text /// base64(b"user=\x01auth=Bearer \x01\x01") /// ``` /// /// # Reference(s): /// /// * XOAuth2, /// Some other (unknown) mechanism. Other(AuthMechanismOther<'a>), } impl_try_from!(Atom<'a>, 'a, &'a [u8], AuthMechanism<'a>); impl_try_from!(Atom<'a>, 'a, Vec, AuthMechanism<'a>); impl_try_from!(Atom<'a>, 'a, &'a str, AuthMechanism<'a>); impl_try_from!(Atom<'a>, 'a, String, AuthMechanism<'a>); impl_try_from!(Atom<'a>, 'a, Cow<'a, str>, AuthMechanism<'a>); impl<'a> From> for AuthMechanism<'a> { fn from(atom: Atom<'a>) -> Self { match atom.as_ref().to_ascii_uppercase().as_str() { "PLAIN" => Self::Plain, "LOGIN" => Self::Login, "XOAUTH2" => Self::XOAuth2, _ => Self::Other(AuthMechanismOther(atom)), } } } impl<'a> Display for AuthMechanism<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Self::Plain => "PLAIN", Self::Login => "LOGIN", Self::XOAuth2 => "XOAUTH2", Self::Other(other) => other.0.as_ref(), }) } } /// An (unknown) authentication mechanism. /// /// It's guaranteed that this type can't represent any mechanism from [`AuthMechanism`]. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct AuthMechanismOther<'a>(Atom<'a>); /// Data line used, e.g., during AUTHENTICATE. /// /// Holds the raw binary data, i.e., a `Vec`, *not* the BASE64 string. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct AuthenticateData(pub Secret>); #[cfg(test)] mod tests { use super::*; #[test] fn test_conversion() { assert!(AuthMechanism::try_from("plain").is_ok()); assert!(AuthMechanism::try_from("login").is_ok()); assert!(AuthMechanism::try_from("xoauth2").is_ok()); assert!(AuthMechanism::try_from("xxxplain").is_ok()); assert!(AuthMechanism::try_from("xxxlogin").is_ok()); assert!(AuthMechanism::try_from("xxxxoauth2").is_ok()); } } imap-codec-1.0.0/imap-types/src/body.rs000066400000000000000000000303111447115025300176730ustar00rootroot00000000000000//! Body(structure)-related types. #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{ core::{IString, NString, NonEmptyVec}, envelope::Envelope, }; /// Inner part of [`BodyStructure`]. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Body<'a> { /// Basic fields pub basic: BasicFields<'a>, /// Type-specific fields pub specific: SpecificFields<'a>, } /// Basic fields of a non-multipart body part. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct BasicFields<'a> { /// List of attribute/value pairs ([MIME-IMB].) pub parameter_list: Vec<(IString<'a>, IString<'a>)>, /// Content id ([MIME-IMB].) pub id: NString<'a>, /// Content description ([MIME-IMB].) pub description: NString<'a>, /// Content transfer encoding ([MIME-IMB].) pub content_transfer_encoding: IString<'a>, /// Size of the body in octets. /// /// Note that this size is the size in its transfer encoding /// and not the resulting size after any decoding. pub size: u32, } /// Specific fields of a non-multipart body part. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum SpecificFields<'a> { /// # Example (not in RFC) /// /// Single application/{voodoo, unknown, whatever, meh} is represented as "basic" /// /// ```text /// ( /// "application" "voodoo" NIL NIL NIL "7bit" 20 /// ^^^ ^^^ ^^^ ^^^^^^ ^^ /// | | | | | size /// | | | | content transfer encoding /// | | | description /// | | id /// | parameter list /// /// NIL NIL NIL NIL /// ^^^ ^^^ ^^^ ^^^ /// | | | | location /// | | | language /// | | disposition /// | md5 /// ) /// ``` Basic { /// A string giving the content media type name as defined in [MIME-IMB]. r#type: IString<'a>, /// A string giving the content subtype name as defined in [MIME-IMB]. subtype: IString<'a>, }, /// # Example (not in RFC) /// /// Single message/rfc822 is represented as "message" /// /// ```text /// ( /// "message" "rfc822" NIL NIL NIL "7bit" 123 /// ^^^ ^^^ ^^^ ^^^^^^ ^^^ /// | | | | | size /// | | | | content transfer encoding /// | | | description /// | | id /// | parameter list /// /// # envelope /// ( /// NIL "message.inner.subject.ljcwooqy" ((NIL NIL "extern" "company.com")) ((NIL NIL "extern" "company.com")) ((NIL NIL "extern" "company.com")) ((NIL NIL "admin" "seurity.com")) NIL NIL NIL NIL /// ) /// /// # body structure /// ( /// "text" "plain" ("charset" "us-ascii") NIL NIL "7bit" 31 /// 2 /// NIL NIL NIL NIL /// ) /// /// 6 /// ^ /// | number of lines /// /// NIL NIL NIL NIL /// ^^^ ^^^ ^^^ ^^^ /// | | | | location /// | | | language /// | | disposition /// | md5 /// ) /// ``` /// /// A body type of type MESSAGE and subtype RFC822 contains, immediately after the basic fields, Message { /// the envelope structure, envelope: Box>, /// body structure, body_structure: Box>, /// and size in text lines of the encapsulated message. number_of_lines: u32, }, /// # Example (not in RFC) /// /// Single text/plain is represented as "text" /// /// ```text /// ( /// "text" "plain" ("charset" "us-ascii") NIL NIL "7bit" 25 /// ^^^^^^^^^^^^^^^^^^^^^^ ^^^ ^^^ ^^^^^^ ^^ /// | | | | | size /// | | | | content transfer encoding /// | | | description /// | | id /// | parameter list /// /// 1 /// ^ /// | number of lines /// /// NIL NIL NIL NIL /// ^^^ ^^^ ^^^ ^^^ /// | | | | location /// | | | language /// | | disposition /// | md5 /// ) /// ``` Text { /// Subtype. subtype: IString<'a>, /// Size of the body in text lines. number_of_lines: u32, }, } /// The BODY(STRUCTURE). #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum BodyStructure<'a> { /// For example, a simple text message of 48 lines and 2279 octets /// can have a body structure of: /// /// ```text /// ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 2279 48) /// ``` Single { /// Inner body. body: Body<'a>, /// Extension data /// /// Extension data is never returned with the BODY fetch, /// but can be returned with a BODYSTRUCTURE fetch. /// Extension data, if present, MUST be in the defined order. /// /// Any following extension data are not yet defined in this /// version of the protocol, and would be as described above under /// multipart extension data. extension_data: Option>, }, /// Multiple parts are indicated by parenthesis nesting. Instead /// of a body type as the first element of the parenthesized list, /// there is a sequence of one or more nested body structures. The /// second (last?!) element of the parenthesized list is the multipart /// subtype (mixed, digest, parallel, alternative, etc.). /// /// For example, a two part message consisting of a text and a /// BASE64-encoded text attachment can have a body structure of: /// /// ```text /// ( /// ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 1152 23) /// ("TEXT" "PLAIN" ("CHARSET" "US-ASCII" "NAME" "cc.diff") "<960723163407.20117h@cac.washington.edu>" "Compiler diff" "BASE64" 4554 73) /// "MIXED" /// ) /// ``` /// /// Extension data follows the multipart subtype. Extension data /// is never returned with the BODY fetch, but can be returned with /// a BODYSTRUCTURE fetch. Extension data, if present, MUST be in /// the defined order. /// /// See [ExtensionMultiPartData](struct.ExtensionMultiPartData.html). /// /// Any following extension data are not yet defined in this /// version of the protocol. Such extension data can consist of /// zero or more NILs, strings, numbers, or potentially nested /// parenthesized lists of such data. Client implementations that /// do a BODYSTRUCTURE fetch MUST be prepared to accept such /// extension data. Server implementations MUST NOT send such /// extension data until it has been defined by a revision of this /// protocol. /// /// # Example (not in RFC) /// /// Multipart/mixed is represented as follows... /// /// ```text /// ( /// ("text" "html" ("charset" "us-ascii") NIL NIL "7bit" 28 0 NIL NIL NIL NIL) /// ("text" "plain" ("charset" "us-ascii") NIL NIL "7bit" 11 0 NIL NIL NIL NIL) /// "mixed" ("boundary" "xxx") NIL NIL NIL /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /// | /// | extension data /// ) /// ``` Multi { /// Inner bodies. bodies: NonEmptyVec>, /// Subtype. subtype: IString<'a>, /// Extension data. extension_data: Option>, }, } /// The extension data of a non-multipart body part. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct SinglePartExtensionData<'a> { /// A string giving the body MD5 value as defined in \[MD5\]. pub md5: NString<'a>, /// (Optional) additional data. pub tail: Option>, } /// The extension data of a multipart body part. /// /// # Trace (not in RFC) /// /// ```text /// ( /// ("text" "html" ("charset" "us-ascii") NIL NIL "7bit" 28 0 NIL NIL NIL NIL) /// ("text" "plain" ("charset" "us-ascii") NIL NIL "7bit" 11 0 NIL NIL NIL NIL) /// "mixed" ("boundary" "xxx") NIL NIL NIL /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /// | /// | extension multipart data /// ) /// ``` #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct MultiPartExtensionData<'a> { /// A parenthesized list of attribute/value pairs [e.g., ("foo" /// "bar" "baz" "rag") where "bar" is the value of "foo", and /// "rag" is the value of "baz"] as defined in [MIME-IMB]. pub parameter_list: Vec<(IString<'a>, IString<'a>)>, /// (Optional) additional data. pub tail: Option>, } /// Helper to enforce correct usage of [`SinglePartExtensionData`] and [`MultiPartExtensionData`]. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Disposition<'a> { /// A parenthesized list, consisting of a disposition type /// string, followed by a parenthesized list of disposition /// attribute/value pairs as defined in \[DISPOSITION\]. pub disposition: Option<(IString<'a>, Vec<(IString<'a>, IString<'a>)>)>, /// (Optional) additional data. pub tail: Option>, } /// Helper to enforce correct usage of [`SinglePartExtensionData`] and [`MultiPartExtensionData`]. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Language<'a> { /// A string or parenthesized list giving the body language /// value as defined in [LANGUAGE-TAGS]. pub language: Vec>, /// (Optional) additional data. pub tail: Option>, } /// Helper to enforce correct usage of [`SinglePartExtensionData`] and [`MultiPartExtensionData`]. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Location<'a> { /// A string list giving the body content URI as defined in \[LOCATION\]. pub location: NString<'a>, /// Extension data. pub extensions: Vec>, } /// Helper to enforce correct usage of [`SinglePartExtensionData`] and [`MultiPartExtensionData`]. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum BodyExtension<'a> { /// NString. NString(NString<'a>), /// Number. Number(u32), /// List. List(NonEmptyVec>), } imap-codec-1.0.0/imap-types/src/command.rs000066400000000000000000002433351447115025300203700ustar00rootroot00000000000000//! Client Commands. //! //! See . use std::borrow::Cow; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{ auth::AuthMechanism, command::error::{AppendError, CopyError, ListError, LoginError, RenameError}, core::{AString, Charset, Literal, NonEmptyVec, Tag}, datetime::DateTime, extensions::{compress::CompressionAlgorithm, enable::CapabilityEnable, quota::QuotaSet}, fetch::MacroOrMessageDataItemNames, flag::{Flag, StoreResponse, StoreType}, mailbox::{ListMailbox, Mailbox}, search::SearchKey, secret::Secret, sequence::SequenceSet, status::StatusDataItemName, }; /// Command. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Command<'a> { /// Tag. pub tag: Tag<'a>, /// Body, e.g., CAPABILITY, LOGIN, SELECT, etc. pub body: CommandBody<'a>, } impl<'a> Command<'a> { /// Create a new command. pub fn new(tag: T, body: CommandBody<'a>) -> Result where T: TryInto>, { Ok(Self { tag: tag.try_into()?, body, }) } /// Get the command name. pub fn name(&self) -> &'static str { self.body.name() } } /// Command body. /// /// This enum is used to encode all the different commands. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum CommandBody<'a> { // ----- Any State (see https://tools.ietf.org/html/rfc3501#section-6.1) ----- /// ### 6.1.1. CAPABILITY Command /// /// * Arguments: none /// * Responses: REQUIRED untagged response: CAPABILITY /// * Result: /// * OK - capability completed /// * BAD - command unknown or arguments invalid /// /// The CAPABILITY command requests a listing of capabilities that the /// server supports. The server MUST send a single untagged /// CAPABILITY response with "IMAP4rev1" as one of the listed /// capabilities before the (tagged) OK response. /// /// A capability name which begins with "AUTH=" indicates that the /// server supports that particular authentication mechanism. All /// such names are, by definition, part of this specification. For /// example, the authorization capability for an experimental /// "blurdybloop" authenticator would be "AUTH=XBLURDYBLOOP" and not /// "XAUTH=BLURDYBLOOP" or "XAUTH=XBLURDYBLOOP". /// /// Other capability names refer to extensions, revisions, or /// amendments to this specification. See the documentation of the /// CAPABILITY response for additional information. No capabilities, /// beyond the base IMAP4rev1 set defined in this specification, are /// enabled without explicit client action to invoke the capability. /// /// Client and server implementations MUST implement the STARTTLS, /// LOGINDISABLED, and AUTH=PLAIN (described in [IMAP-TLS]) /// capabilities. See the Security Considerations section for /// important information. /// /// See the section entitled "Client Commands - /// Experimental/Expansion" for information about the form of site or /// implementation-specific capabilities. Capability, /// ### 6.1.2. NOOP Command /// /// * Arguments: none /// * Responses: no specific responses for this command (but see below) /// * Result: /// * OK - noop completed /// * BAD - command unknown or arguments invalid /// /// The NOOP command always succeeds. It does nothing. /// /// Since any command can return a status update as untagged data, the /// NOOP command can be used as a periodic poll for new messages or /// message status updates during a period of inactivity (this is the /// preferred method to do this). The NOOP command can also be used /// to reset any inactivity autologout timer on the server. Noop, /// ### 6.1.3. LOGOUT Command /// /// * Arguments: none /// * Responses: REQUIRED untagged response: BYE /// * Result: /// * OK - logout completed /// * BAD - command unknown or arguments invalid /// /// The LOGOUT command informs the server that the client is done with /// the connection. The server MUST send a BYE untagged response /// before the (tagged) OK response, and then close the network /// connection. Logout, // ----- Not Authenticated State (https://tools.ietf.org/html/rfc3501#section-6.2) ----- /// ### 6.2.1. STARTTLS Command /// /// * Arguments: none /// * Responses: no specific response for this command /// * Result: /// * OK - starttls completed, begin TLS negotiation /// * BAD - command unknown or arguments invalid /// /// A \[TLS\] negotiation begins immediately after the CRLF at the end /// of the tagged OK response from the server. Once a client issues a /// STARTTLS command, it MUST NOT issue further commands until a /// server response is seen and the \[TLS\] negotiation is complete. /// /// The server remains in the non-authenticated state, even if client /// credentials are supplied during the \[TLS\] negotiation. This does /// not preclude an authentication mechanism such as EXTERNAL (defined /// in \[SASL\]) from using client identity determined by the \[TLS\] /// negotiation. /// /// Once \[TLS\] has been started, the client MUST discard cached /// information about server capabilities and SHOULD re-issue the /// CAPABILITY command. This is necessary to protect against man-in- /// the-middle attacks which alter the capabilities list prior to /// STARTTLS. The server MAY advertise different capabilities after /// STARTTLS. #[cfg(feature = "starttls")] #[cfg_attr(docsrs, doc(cfg(feature = "starttls")))] StartTLS, /// ### 6.2.2. AUTHENTICATE Command /// /// * Arguments: authentication mechanism name /// * Responses: continuation data can be requested /// * Result: /// * OK - authenticate completed, now in authenticated state /// * NO - authenticate failure: unsupported authentication /// mechanism, credentials rejected /// * BAD - command unknown or arguments invalid, /// authentication exchange cancelled /// /// The AUTHENTICATE command indicates a \[SASL\] authentication /// mechanism to the server. If the server supports the requested /// authentication mechanism, it performs an authentication protocol /// exchange to authenticate and identify the client. It MAY also /// negotiate an OPTIONAL security layer for subsequent protocol /// interactions. If the requested authentication mechanism is not /// supported, the server SHOULD reject the AUTHENTICATE command by /// sending a tagged NO response. /// /// The AUTHENTICATE command does not support the optional "initial /// response" feature of \[SASL\]. Section 5.1 of \[SASL\] specifies how /// to handle an authentication mechanism which uses an initial /// response. /// /// The service name specified by this protocol's profile of \[SASL\] is /// "imap". /// /// The authentication protocol exchange consists of a series of /// server challenges and client responses that are specific to the /// authentication mechanism. A server challenge consists of a /// command continuation request response with the "+" token followed /// by a BASE64 encoded string. The client response consists of a /// single line consisting of a BASE64 encoded string. If the client /// wishes to cancel an authentication exchange, it issues a line /// consisting of a single "*". If the server receives such a /// response, it MUST reject the AUTHENTICATE command by sending a /// tagged BAD response. /// /// If a security layer is negotiated through the \[SASL\] /// authentication exchange, it takes effect immediately following the /// CRLF that concludes the authentication exchange for the client, /// and the CRLF of the tagged OK response for the server. /// /// While client and server implementations MUST implement the /// AUTHENTICATE command itself, it is not required to implement any /// authentication mechanisms other than the PLAIN mechanism described /// in [IMAP-TLS]. Also, an authentication mechanism is not required /// to support any security layers. /// /// Note: a server implementation MUST implement a /// configuration in which it does NOT permit any plaintext /// password mechanisms, unless either the STARTTLS command /// has been negotiated or some other mechanism that /// protects the session from password snooping has been /// provided. Server sites SHOULD NOT use any configuration /// which permits a plaintext password mechanism without /// such a protection mechanism against password snooping. /// Client and server implementations SHOULD implement /// additional \[SASL\] mechanisms that do not use plaintext /// passwords, such the GSSAPI mechanism described in \[SASL\] /// and/or the [DIGEST-MD5] mechanism. /// /// Servers and clients can support multiple authentication /// mechanisms. The server SHOULD list its supported authentication /// mechanisms in the response to the CAPABILITY command so that the /// client knows which authentication mechanisms to use. /// /// A server MAY include a CAPABILITY response code in the tagged OK /// response of a successful AUTHENTICATE command in order to send /// capabilities automatically. It is unnecessary for a client to /// send a separate CAPABILITY command if it recognizes these /// automatic capabilities. This should only be done if a security /// layer was not negotiated by the AUTHENTICATE command, because the /// tagged OK response as part of an AUTHENTICATE command is not /// protected by encryption/integrity checking. \[SASL\] requires the /// client to re-issue a CAPABILITY command in this case. /// /// If an AUTHENTICATE command fails with a NO response, the client /// MAY try another authentication mechanism by issuing another /// AUTHENTICATE command. It MAY also attempt to authenticate by /// using the LOGIN command (see section 6.2.3 for more detail). In /// other words, the client MAY request authentication types in /// decreasing order of preference, with the LOGIN command as a last /// resort. /// /// The authorization identity passed from the client to the server /// during the authentication exchange is interpreted by the server as /// the user name whose privileges the client is requesting. Authenticate { /// Authentication mechanism. mechanism: AuthMechanism<'a>, /// Initial response (if any). /// /// This type holds the raw binary data, i.e., a `Vec`, *not* the BASE64 string. /// /// Note: Use this only when the server advertised the `SASL-IR` capability. initial_response: Option>>, }, /// ### 6.2.3. LOGIN Command /// /// * Arguments: /// * user name /// * password /// * Responses: no specific responses for this command /// * Result: /// * OK - login completed, now in authenticated state /// * NO - login failure: user name or password rejected /// * BAD - command unknown or arguments invalid /// /// The LOGIN command identifies the client to the server and carries /// the plaintext password authenticating this user. /// /// A server MAY include a CAPABILITY response code in the tagged OK /// response to a successful LOGIN command in order to send /// capabilities automatically. It is unnecessary for a client to /// send a separate CAPABILITY command if it recognizes these /// automatic capabilities. /// /// Note: Use of the LOGIN command over an insecure network /// (such as the Internet) is a security risk, because anyone /// monitoring network traffic can obtain plaintext passwords. /// The LOGIN command SHOULD NOT be used except as a last /// resort, and it is recommended that client implementations /// have a means to disable any automatic use of the LOGIN /// command. /// /// Unless either the STARTTLS command has been negotiated or /// some other mechanism that protects the session from /// password snooping has been provided, a server /// implementation MUST implement a configuration in which it /// advertises the LOGINDISABLED capability and does NOT permit /// the LOGIN command. Server sites SHOULD NOT use any /// configuration which permits the LOGIN command without such /// a protection mechanism against password snooping. A client /// implementation MUST NOT send a LOGIN command if the /// LOGINDISABLED capability is advertised. Login { /// Username. username: AString<'a>, /// Password. password: Secret>, }, // ----- Authenticated State (https://tools.ietf.org/html/rfc3501#section-6.3) ----- /// ### 6.3.1. SELECT Command /// /// * Arguments: mailbox name /// * Responses: /// * REQUIRED untagged responses: FLAGS, EXISTS, RECENT /// * REQUIRED OK untagged responses: UNSEEN, PERMANENTFLAGS, UIDNEXT, UIDVALIDITY /// * Result: /// * OK - select completed, now in selected state /// * NO - select failure, now in authenticated state: no such mailbox, can't access mailbox /// * BAD - command unknown or arguments invalid /// /// The SELECT command selects a mailbox so that messages in the /// mailbox can be accessed. Before returning an OK to the client, /// the server MUST send the following untagged data to the client. /// Note that earlier versions of this protocol only required the /// FLAGS, EXISTS, and RECENT untagged data; consequently, client /// implementations SHOULD implement default behavior for missing data /// as discussed with the individual item. /// /// FLAGS Defined flags in the mailbox. See the description /// of the FLAGS response for more detail. /// /// \ EXISTS The number of messages in the mailbox. See the /// description of the EXISTS response for more detail. /// /// \ RECENT The number of messages with the \Recent flag set. /// See the description of the RECENT response for more /// detail. /// /// OK [UNSEEN \] /// The message sequence number of the first unseen /// message in the mailbox. If this is missing, the /// client can not make any assumptions about the first /// unseen message in the mailbox, and needs to issue a /// SEARCH command if it wants to find it. /// /// OK [PERMANENTFLAGS (\)] /// A list of message flags that the client can change /// permanently. If this is missing, the client should /// assume that all flags can be changed permanently. /// /// OK [UIDNEXT \] /// The next unique identifier value. Refer to section /// 2.3.1.1 for more information. If this is missing, /// the client can not make any assumptions about the /// next unique identifier value. /// /// OK [UIDVALIDITY \] /// The unique identifier validity value. Refer to /// section 2.3.1.1 for more information. If this is /// missing, the server does not support unique /// identifiers. /// /// Only one mailbox can be selected at a time in a connection; /// simultaneous access to multiple mailboxes requires multiple /// connections. The SELECT command automatically deselects any /// currently selected mailbox before attempting the new selection. /// Consequently, if a mailbox is selected and a SELECT command that /// fails is attempted, no mailbox is selected. /// /// If the client is permitted to modify the mailbox, the server /// SHOULD prefix the text of the tagged OK response with the /// "[READ-WRITE]" response code. /// /// If the client is not permitted to modify the mailbox but is /// permitted read access, the mailbox is selected as read-only, and /// the server MUST prefix the text of the tagged OK response to /// SELECT with the "[READ-ONLY]" response code. Read-only access /// through SELECT differs from the EXAMINE command in that certain /// read-only mailboxes MAY permit the change of permanent state on a /// per-user (as opposed to global) basis. Netnews messages marked in /// a server-based .newsrc file are an example of such per-user /// permanent state that can be modified with read-only mailboxes. Select { /// Mailbox. mailbox: Mailbox<'a>, }, /// Unselect a mailbox. /// /// This should bring the client back to the AUTHENTICATED state. Unselect, /// 6.3.2. EXAMINE Command /// /// Arguments: mailbox name /// Responses: REQUIRED untagged responses: FLAGS, EXISTS, RECENT /// REQUIRED OK untagged responses: UNSEEN, PERMANENTFLAGS, /// UIDNEXT, UIDVALIDITY /// Result: OK - examine completed, now in selected state /// NO - examine failure, now in authenticated state: no /// such mailbox, can't access mailbox /// BAD - command unknown or arguments invalid /// /// The EXAMINE command is identical to SELECT and returns the same /// output; however, the selected mailbox is identified as read-only. /// No changes to the permanent state of the mailbox, including /// per-user state, are permitted; in particular, EXAMINE MUST NOT /// cause messages to lose the \Recent flag. /// /// The text of the tagged OK response to the EXAMINE command MUST /// begin with the "[READ-ONLY]" response code. Examine { /// Mailbox. mailbox: Mailbox<'a>, }, /// ### 6.3.3. CREATE Command /// /// * Arguments: mailbox name /// * Responses: no specific responses for this command /// * Result: /// * OK - create completed /// * NO - create failure: can't create mailbox with that name /// * BAD - command unknown or arguments invalid /// /// The CREATE command creates a mailbox with the given name. An OK /// response is returned only if a new mailbox with that name has been /// created. It is an error to attempt to create INBOX or a mailbox /// with a name that refers to an extant mailbox. Any error in /// creation will return a tagged NO response. /// /// If the mailbox name is suffixed with the server's hierarchy /// separator character (as returned from the server by a LIST /// command), this is a declaration that the client intends to create /// mailbox names under this name in the hierarchy. Server /// implementations that do not require this declaration MUST ignore /// the declaration. In any case, the name created is without the /// trailing hierarchy delimiter. /// /// If the server's hierarchy separator character appears elsewhere in /// the name, the server SHOULD create any superior hierarchical names /// that are needed for the CREATE command to be successfully /// completed. In other words, an attempt to create "foo/bar/zap" on /// a server in which "/" is the hierarchy separator character SHOULD /// create foo/ and foo/bar/ if they do not already exist. /// /// If a new mailbox is created with the same name as a mailbox which /// was deleted, its unique identifiers MUST be greater than any /// unique identifiers used in the previous incarnation of the mailbox /// UNLESS the new incarnation has a different unique identifier /// validity value. See the description of the UID command for more /// detail. /// /// Note: The interpretation of this example depends on whether /// "/" was returned as the hierarchy separator from LIST. If /// "/" is the hierarchy separator, a new level of hierarchy /// named "owatagusiam" with a member called "blurdybloop" is /// created. Otherwise, two mailboxes at the same hierarchy /// level are created. Create { /// Mailbox. mailbox: Mailbox<'a>, }, /// 6.3.4. DELETE Command /// /// Arguments: mailbox name /// Responses: no specific responses for this command /// Result: OK - delete completed /// NO - delete failure: can't delete mailbox with that name /// BAD - command unknown or arguments invalid /// /// The DELETE command permanently removes the mailbox with the given /// name. A tagged OK response is returned only if the mailbox has /// been deleted. It is an error to attempt to delete INBOX or a /// mailbox name that does not exist. /// /// The DELETE command MUST NOT remove inferior hierarchical names. /// For example, if a mailbox "foo" has an inferior "foo.bar" /// (assuming "." is the hierarchy delimiter character), removing /// "foo" MUST NOT remove "foo.bar". It is an error to attempt to /// delete a name that has inferior hierarchical names and also has /// the \Noselect mailbox name attribute (see the description of the /// LIST response for more details). /// /// It is permitted to delete a name that has inferior hierarchical /// names and does not have the \Noselect mailbox name attribute. In /// this case, all messages in that mailbox are removed, and the name /// will acquire the \Noselect mailbox name attribute. /// /// The value of the highest-used unique identifier of the deleted /// mailbox MUST be preserved so that a new mailbox created with the /// same name will not reuse the identifiers of the former /// incarnation, UNLESS the new incarnation has a different unique /// identifier validity value. See the description of the UID command /// for more detail. Delete { /// Mailbox. mailbox: Mailbox<'a>, }, /// 6.3.5. RENAME Command /// /// Arguments: existing mailbox name /// new mailbox name /// Responses: no specific responses for this command /// Result: OK - rename completed /// NO - rename failure: can't rename mailbox with that name, /// can't rename to mailbox with that name /// BAD - command unknown or arguments invalid /// /// The RENAME command changes the name of a mailbox. A tagged OK /// response is returned only if the mailbox has been renamed. It is /// an error to attempt to rename from a mailbox name that does not /// exist or to a mailbox name that already exists. Any error in /// renaming will return a tagged NO response. /// /// If the name has inferior hierarchical names, then the inferior /// hierarchical names MUST also be renamed. For example, a rename of /// "foo" to "zap" will rename "foo/bar" (assuming "/" is the /// hierarchy delimiter character) to "zap/bar". /// /// If the server's hierarchy separator character appears in the name, /// the server SHOULD create any superior hierarchical names that are /// needed for the RENAME command to complete successfully. In other /// words, an attempt to rename "foo/bar/zap" to baz/rag/zowie on a /// server in which "/" is the hierarchy separator character SHOULD /// create baz/ and baz/rag/ if they do not already exist. /// /// The value of the highest-used unique identifier of the old mailbox /// name MUST be preserved so that a new mailbox created with the same /// name will not reuse the identifiers of the former incarnation, /// UNLESS the new incarnation has a different unique identifier /// validity value. See the description of the UID command for more /// detail. /// /// Renaming INBOX is permitted, and has special behavior. It moves /// all messages in INBOX to a new mailbox with the given name, /// leaving INBOX empty. If the server implementation supports /// inferior hierarchical names of INBOX, these are unaffected by a /// rename of INBOX. Rename { /// Current name. from: Mailbox<'a>, /// New name. to: Mailbox<'a>, }, /// ### 6.3.6. SUBSCRIBE Command /// /// * Arguments: mailbox /// * Responses: no specific responses for this command /// * Result: /// * OK - subscribe completed /// * NO - subscribe failure: can't subscribe to that name /// * BAD - command unknown or arguments invalid /// /// The SUBSCRIBE command adds the specified mailbox name to the /// server's set of "active" or "subscribed" mailboxes as returned by /// the LSUB command. This command returns a tagged OK response only /// if the subscription is successful. /// /// A server MAY validate the mailbox argument to SUBSCRIBE to verify /// that it exists. However, it MUST NOT unilaterally remove an /// existing mailbox name from the subscription list even if a mailbox /// by that name no longer exists. /// /// Note: This requirement is because a server site can /// choose to routinely remove a mailbox with a well-known /// name (e.g., "system-alerts") after its contents expire, /// with the intention of recreating it when new contents /// are appropriate. Subscribe { /// Mailbox. mailbox: Mailbox<'a>, }, /// 6.3.7. UNSUBSCRIBE Command /// /// Arguments: mailbox name /// Responses: no specific responses for this command /// Result: OK - unsubscribe completed /// NO - unsubscribe failure: can't unsubscribe that name /// BAD - command unknown or arguments invalid /// /// The UNSUBSCRIBE command removes the specified mailbox name from /// the server's set of "active" or "subscribed" mailboxes as returned /// by the LSUB command. This command returns a tagged OK response /// only if the unsubscription is successful. Unsubscribe { /// Mailbox. mailbox: Mailbox<'a>, }, /// ### 6.3.8. LIST Command /// /// * Arguments: /// * reference name /// * mailbox name with possible wildcards /// * Responses: untagged responses: LIST /// * Result: /// * OK - list completed /// * NO - list failure: can't list that reference or name /// * BAD - command unknown or arguments invalid /// /// The LIST command returns a subset of names from the complete set /// of all names available to the client. Zero or more untagged LIST /// replies are returned, containing the name attributes, hierarchy /// delimiter, and name; see the description of the LIST reply for /// more detail. /// /// The LIST command SHOULD return its data quickly, without undue /// delay. For example, it SHOULD NOT go to excess trouble to /// calculate the \Marked or \Unmarked status or perform other /// processing; if each name requires 1 second of processing, then a /// list of 1200 names would take 20 minutes! /// /// An empty ("" string) reference name argument indicates that the /// mailbox name is interpreted as by SELECT. The returned mailbox /// names MUST match the supplied mailbox name pattern. A non-empty /// reference name argument is the name of a mailbox or a level of /// mailbox hierarchy, and indicates the context in which the mailbox /// name is interpreted. /// /// An empty ("" string) mailbox name argument is a special request to /// return the hierarchy delimiter and the root name of the name given /// in the reference. The value returned as the root MAY be the empty /// string if the reference is non-rooted or is an empty string. In /// all cases, a hierarchy delimiter (or NIL if there is no hierarchy) /// is returned. This permits a client to get the hierarchy delimiter /// (or find out that the mailbox names are flat) even when no /// mailboxes by that name currently exist. /// /// The reference and mailbox name arguments are interpreted into a /// canonical form that represents an unambiguous left-to-right /// hierarchy. The returned mailbox names will be in the interpreted /// form. /// /// Note: The interpretation of the reference argument is /// implementation-defined. It depends upon whether the /// server implementation has a concept of the "current /// working directory" and leading "break out characters", /// which override the current working directory. /// /// For example, on a server which exports a UNIX or NT /// filesystem, the reference argument contains the current /// working directory, and the mailbox name argument would /// contain the name as interpreted in the current working /// directory. /// /// If a server implementation has no concept of break out /// characters, the canonical form is normally the reference /// name appended with the mailbox name. Note that if the /// server implements the namespace convention (section /// 5.1.2), "#" is a break out character and must be treated /// as such. /// /// If the reference argument is not a level of mailbox /// hierarchy (that is, it is a \NoInferiors name), and/or /// the reference argument does not end with the hierarchy /// delimiter, it is implementation-dependent how this is /// interpreted. For example, a reference of "foo/bar" and /// mailbox name of "rag/baz" could be interpreted as /// "foo/bar/rag/baz", "foo/barrag/baz", or "foo/rag/baz". /// A client SHOULD NOT use such a reference argument except /// at the explicit request of the user. A hierarchical /// browser MUST NOT make any assumptions about server /// interpretation of the reference unless the reference is /// a level of mailbox hierarchy AND ends with the hierarchy /// delimiter. /// /// Any part of the reference argument that is included in the /// interpreted form SHOULD prefix the interpreted form. It SHOULD /// also be in the same form as the reference name argument. This /// rule permits the client to determine if the returned mailbox name /// is in the context of the reference argument, or if something about /// the mailbox argument overrode the reference argument. Without /// this rule, the client would have to have knowledge of the server's /// naming semantics including what characters are "breakouts" that /// override a naming context. /// /// For example, here are some examples of how references /// and mailbox names might be interpreted on a UNIX-based /// server: /// /// ```text /// Reference Mailbox Name Interpretation /// ------------ ------------ -------------- /// ~smith/Mail/ foo.* ~smith/Mail/foo.* /// archive/ % archive/% /// #news. comp.mail.* #news.comp.mail.* /// ~smith/Mail/ /usr/doc/foo /usr/doc/foo /// archive/ ~fred/Mail/* ~fred/Mail/* /// ``` /// /// The first three examples demonstrate interpretations in /// the context of the reference argument. Note that /// "~smith/Mail" SHOULD NOT be transformed into something /// like "/u2/users/smith/Mail", or it would be impossible /// for the client to determine that the interpretation was /// in the context of the reference. /// /// The character "*" is a wildcard, and matches zero or more /// characters at this position. The character "%" is similar to "*", /// but it does not match a hierarchy delimiter. If the "%" wildcard /// is the last character of a mailbox name argument, matching levels /// of hierarchy are also returned. If these levels of hierarchy are /// not also selectable mailboxes, they are returned with the /// \Noselect mailbox name attribute (see the description of the LIST /// response for more details). /// /// Server implementations are permitted to "hide" otherwise /// accessible mailboxes from the wildcard characters, by preventing /// certain characters or names from matching a wildcard in certain /// situations. For example, a UNIX-based server might restrict the /// interpretation of "*" so that an initial "/" character does not /// match. /// /// The special name INBOX is included in the output from LIST, if /// INBOX is supported by this server for this user and if the /// uppercase string "INBOX" matches the interpreted reference and /// mailbox name arguments with wildcards as described above. The /// criteria for omitting INBOX is whether SELECT INBOX will return /// failure; it is not relevant whether the user's real INBOX resides /// on this or some other server. List { /// Reference. reference: Mailbox<'a>, /// Mailbox (wildcard). mailbox_wildcard: ListMailbox<'a>, }, /// ### 6.3.9. LSUB Command /// /// * Arguments: /// * reference name /// * mailbox name with possible wildcards /// * Responses: untagged responses: LSUB /// * Result: /// * OK - lsub completed /// * NO - lsub failure: can't list that reference or name /// * BAD - command unknown or arguments invalid /// /// The LSUB command returns a subset of names from the set of names /// that the user has declared as being "active" or "subscribed". /// Zero or more untagged LSUB replies are returned. The arguments to /// LSUB are in the same form as those for LIST. /// /// The returned untagged LSUB response MAY contain different mailbox /// flags from a LIST untagged response. If this should happen, the /// flags in the untagged LIST are considered more authoritative. /// /// A special situation occurs when using LSUB with the % wildcard. /// Consider what happens if "foo/bar" (with a hierarchy delimiter of /// "/") is subscribed but "foo" is not. A "%" wildcard to LSUB must /// return foo, not foo/bar, in the LSUB response, and it MUST be /// flagged with the \Noselect attribute. /// /// The server MUST NOT unilaterally remove an existing mailbox name /// from the subscription list even if a mailbox by that name no /// longer exists. Lsub { /// Reference. reference: Mailbox<'a>, /// Mailbox (wildcard). mailbox_wildcard: ListMailbox<'a>, }, /// ### 6.3.10. STATUS Command /// /// * Arguments: /// * mailbox name /// * status data item names /// * Responses: untagged responses: STATUS /// * Result: /// * OK - status completed /// * NO - status failure: no status for that name /// * BAD - command unknown or arguments invalid /// /// The STATUS command requests the status of the indicated mailbox. /// It does not change the currently selected mailbox, nor does it /// affect the state of any messages in the queried mailbox (in /// particular, STATUS MUST NOT cause messages to lose the \Recent /// flag). /// /// The STATUS command provides an alternative to opening a second /// IMAP4rev1 connection and doing an EXAMINE command on a mailbox to /// query that mailbox's status without deselecting the current /// mailbox in the first IMAP4rev1 connection. /// /// Unlike the LIST command, the STATUS command is not guaranteed to /// be fast in its response. Under certain circumstances, it can be /// quite slow. In some implementations, the server is obliged to /// open the mailbox read-only internally to obtain certain status /// information. Also unlike the LIST command, the STATUS command /// does not accept wildcards. /// /// Note: The STATUS command is intended to access the /// status of mailboxes other than the currently selected /// mailbox. Because the STATUS command can cause the /// mailbox to be opened internally, and because this /// information is available by other means on the selected /// mailbox, the STATUS command SHOULD NOT be used on the /// currently selected mailbox. /// /// The STATUS command MUST NOT be used as a "check for new /// messages in the selected mailbox" operation (refer to /// sections 7, 7.3.1, and 7.3.2 for more information about /// the proper method for new message checking). /// /// Because the STATUS command is not guaranteed to be fast /// in its results, clients SHOULD NOT expect to be able to /// issue many consecutive STATUS commands and obtain /// reasonable performance. Status { /// Mailbox. mailbox: Mailbox<'a>, /// Status data items. item_names: Cow<'a, [StatusDataItemName]>, }, /// 6.3.11. APPEND Command /// /// Arguments: mailbox name /// OPTIONAL flag parenthesized list /// OPTIONAL date/time string /// message literal /// Responses: no specific responses for this command /// Result: OK - append completed /// NO - append error: can't append to that mailbox, error /// in flags or date/time or message text /// BAD - command unknown or arguments invalid /// /// The APPEND command appends the literal argument as a new message /// to the end of the specified destination mailbox. This argument /// SHOULD be in the format of an [RFC-2822] message. 8-bit /// characters are permitted in the message. A server implementation /// that is unable to preserve 8-bit data properly MUST be able to /// reversibly convert 8-bit APPEND data to 7-bit using a [MIME-IMB] /// content transfer encoding. /// /// Note: There MAY be exceptions, e.g., draft messages, in /// which required [RFC-2822] header lines are omitted in /// the message literal argument to APPEND. The full /// implications of doing so MUST be understood and /// carefully weighed. /// /// If a flag parenthesized list is specified, the flags SHOULD be set /// in the resulting message; otherwise, the flag list of the /// resulting message is set to empty by default. In either case, the /// Recent flag is also set. /// /// If a date-time is specified, the internal date SHOULD be set in /// the resulting message; otherwise, the internal date of the /// resulting message is set to the current date and time by default. /// /// If the append is unsuccessful for any reason, the mailbox MUST be /// restored to its state before the APPEND attempt; no partial /// appending is permitted. /// /// If the destination mailbox does not exist, a server MUST return an /// error, and MUST NOT automatically create the mailbox. Unless it /// is certain that the destination mailbox can not be created, the /// server MUST send the response code "\[TRYCREATE\]" as the prefix of /// the text of the tagged NO response. This gives a hint to the /// client that it can attempt a CREATE command and retry the APPEND /// if the CREATE is successful. /// /// If the mailbox is currently selected, the normal new message /// actions SHOULD occur. Specifically, the server SHOULD notify the /// client immediately via an untagged EXISTS response. If the server /// does not do so, the client MAY issue a NOOP command (or failing /// that, a CHECK command) after one or more APPEND commands. /// /// Note: The APPEND command is not used for message delivery, /// because it does not provide a mechanism to transfer \[SMTP\] /// envelope information. Append { /// Mailbox. mailbox: Mailbox<'a>, /// Flags. flags: Vec>, /// Datetime. date: Option, /// Message to append. message: Literal<'a>, }, // ----- Selected State (https://tools.ietf.org/html/rfc3501#section-6.4) ----- /// ### 6.4.1. CHECK Command /// /// * Arguments: none /// * Responses: no specific responses for this command /// * Result: /// * OK - check completed /// * BAD - command unknown or arguments invalid /// /// The CHECK command requests a checkpoint of the currently selected /// mailbox. A checkpoint refers to any implementation-dependent /// housekeeping associated with the mailbox (e.g., resolving the /// server's in-memory state of the mailbox with the state on its /// disk) that is not normally executed as part of each command. A /// checkpoint MAY take a non-instantaneous amount of real time to /// complete. If a server implementation has no such housekeeping /// considerations, CHECK is equivalent to NOOP. /// /// There is no guarantee that an EXISTS untagged response will happen /// as a result of CHECK. NOOP, not CHECK, SHOULD be used for new /// message polling. Check, /// ### 6.4.2. CLOSE Command /// /// * Arguments: none /// * Responses: no specific responses for this command /// * Result: /// * OK - close completed, now in authenticated state /// * BAD - command unknown or arguments invalid /// /// The CLOSE command permanently removes all messages that have the /// \Deleted flag set from the currently selected mailbox, and returns /// to the authenticated state from the selected state. No untagged /// EXPUNGE responses are sent. /// /// No messages are removed, and no error is given, if the mailbox is /// selected by an EXAMINE command or is otherwise selected read-only. /// /// Even if a mailbox is selected, a SELECT, EXAMINE, or LOGOUT /// command MAY be issued without previously issuing a CLOSE command. /// The SELECT, EXAMINE, and LOGOUT commands implicitly close the /// currently selected mailbox without doing an expunge. However, /// when many messages are deleted, a CLOSE-LOGOUT or CLOSE-SELECT /// sequence is considerably faster than an EXPUNGE-LOGOUT or /// EXPUNGE-SELECT because no untagged EXPUNGE responses (which the /// client would probably ignore) are sent. Close, /// 6.4.3. EXPUNGE Command /// /// Arguments: none /// Responses: untagged responses: EXPUNGE /// Result: OK - expunge completed /// NO - expunge failure: can't expunge (e.g., permission /// denied) /// BAD - command unknown or arguments invalid /// /// The EXPUNGE command permanently removes all messages that have the /// \Deleted flag set from the currently selected mailbox. Before /// returning an OK to the client, an untagged EXPUNGE response is /// sent for each message that is removed. /// /// Note: In this example, messages 3, 4, 7, and 11 had the /// \Deleted flag set. See the description of the EXPUNGE /// response for further explanation. Expunge, /// ### 6.4.4. SEARCH Command /// /// * Arguments: /// * OPTIONAL \[CHARSET\] specification /// * searching criteria (one or more) /// * Responses: REQUIRED untagged response: SEARCH /// * Result: /// * OK - search completed /// * NO - search error: can't search that \[CHARSET\] or criteria /// * BAD - command unknown or arguments invalid /// /// The SEARCH command searches the mailbox for messages that match /// the given searching criteria. Searching criteria consist of one /// or more search keys. The untagged SEARCH response from the server /// contains a listing of message sequence numbers corresponding to /// those messages that match the searching criteria. /// /// When multiple keys are specified, the result is the intersection /// (AND function) of all the messages that match those keys. For /// example, the criteria DELETED FROM "SMITH" SINCE 1-Feb-1994 refers /// to all deleted messages from Smith that were placed in the mailbox /// since February 1, 1994. A search key can also be a parenthesized /// list of one or more search keys (e.g., for use with the OR and NOT /// keys). /// /// Server implementations MAY exclude [MIME-IMB] body parts with /// terminal content media types other than TEXT and MESSAGE from /// consideration in SEARCH matching. /// /// The OPTIONAL \[CHARSET\] specification consists of the word /// "CHARSET" followed by a registered \[CHARSET\]. It indicates the /// \[CHARSET\] of the strings that appear in the search criteria. /// [MIME-IMB] content transfer encodings, and [MIME-HDRS] strings in /// [RFC-2822]/[MIME-IMB] headers, MUST be decoded before comparing /// text in a \[CHARSET\] other than US-ASCII. US-ASCII MUST be /// supported; other \[CHARSET\]s MAY be supported. /// /// If the server does not support the specified \[CHARSET\], it MUST /// return a tagged NO response (not a BAD). This response SHOULD /// contain the BADCHARSET response code, which MAY list the /// \[CHARSET\]s supported by the server. /// /// In all search keys that use strings, a message matches the key if /// the string is a substring of the field. The matching is /// case-insensitive. /// /// See [SearchKey] enum. /// /// Note: Since this document is restricted to 7-bit ASCII /// text, it is not possible to show actual UTF-8 data. The /// "XXXXXX" is a placeholder for what would be 6 octets of /// 8-bit data in an actual transaction. Search { /// Charset. charset: Option>, /// Criteria. criteria: SearchKey<'a>, /// Use UID variant. uid: bool, }, /// ### 6.4.5. FETCH Command /// /// * Arguments: /// * sequence set /// * message data item names or macro /// * Responses: untagged responses: FETCH /// * Result: /// * OK - fetch completed /// * NO - fetch error: can't fetch that data /// * BAD - command unknown or arguments invalid /// /// The FETCH command retrieves data associated with a message in the /// mailbox. The data items to be fetched can be either a single atom /// or a parenthesized list. /// /// Most data items, identified in the formal syntax under the /// msg-att-static rule, are static and MUST NOT change for any /// particular message. Other data items, identified in the formal /// syntax under the msg-att-dynamic rule, MAY change, either as a /// result of a STORE command or due to external events. /// /// For example, if a client receives an ENVELOPE for a /// message when it already knows the envelope, it can /// safely ignore the newly transmitted envelope. Fetch { /// Set of messages. sequence_set: SequenceSet, /// Message data items (or a macro). macro_or_item_names: MacroOrMessageDataItemNames<'a>, /// Use UID variant. uid: bool, }, /// ### 6.4.6. STORE Command /// /// * Arguments: /// * sequence set /// * message data item name /// * value for message data item /// * Responses: untagged responses: FETCH /// * Result: /// * OK - store completed /// * NO - store error: can't store that data /// * BAD - command unknown or arguments invalid /// /// The STORE command alters data associated with a message in the /// mailbox. Normally, STORE will return the updated value of the /// data with an untagged FETCH response. A suffix of ".SILENT" in /// the data item name prevents the untagged FETCH, and the server /// SHOULD assume that the client has determined the updated value /// itself or does not care about the updated value. /// /// Note: Regardless of whether or not the ".SILENT" suffix /// was used, the server SHOULD send an untagged FETCH /// response if a change to a message's flags from an /// external source is observed. The intent is that the /// status of the flags is determinate without a race /// condition. /// /// The currently defined data items that can be stored are: /// /// FLAGS \ /// Replace the flags for the message (other than \Recent) with the /// argument. The new value of the flags is returned as if a FETCH /// of those flags was done. /// /// FLAGS.SILENT \ /// Equivalent to FLAGS, but without returning a new value. /// /// +FLAGS \ /// Add the argument to the flags for the message. The new value /// of the flags is returned as if a FETCH of those flags was done. /// /// +FLAGS.SILENT \ /// Equivalent to +FLAGS, but without returning a new value. /// /// -FLAGS \ /// Remove the argument from the flags for the message. The new /// value of the flags is returned as if a FETCH of those flags was /// done. /// /// -FLAGS.SILENT \ /// Equivalent to -FLAGS, but without returning a new value. Store { /// Set of messages. sequence_set: SequenceSet, /// Kind of storage, i.e., replace, add, or remove. kind: StoreType, /// Kind of response, i.e., answer or silent. response: StoreResponse, /// Flags. flags: Vec>, // FIXME(misuse): must not accept "\*" or "\Recent" /// Use UID variant. uid: bool, }, /// 6.4.7. COPY Command /// /// Arguments: sequence set /// mailbox name /// Responses: no specific responses for this command /// Result: OK - copy completed /// NO - copy error: can't copy those messages or to that /// name /// BAD - command unknown or arguments invalid /// /// The COPY command copies the specified message(s) to the end of the /// specified destination mailbox. The flags and internal date of the /// message(s) SHOULD be preserved, and the Recent flag SHOULD be set, /// in the copy. /// /// If the destination mailbox does not exist, a server SHOULD return /// an error. It SHOULD NOT automatically create the mailbox. Unless /// it is certain that the destination mailbox can not be created, the /// server MUST send the response code "\[TRYCREATE\]" as the prefix of /// the text of the tagged NO response. This gives a hint to the /// client that it can attempt a CREATE command and retry the COPY if /// the CREATE is successful. /// /// If the COPY command is unsuccessful for any reason, server /// implementations MUST restore the destination mailbox to its state /// before the COPY attempt. Copy { /// Set of messages. sequence_set: SequenceSet, /// Destination mailbox. mailbox: Mailbox<'a>, /// Use UID variant. uid: bool, }, /// The UID mechanism was inlined into copy, fetch, store, and search. /// as an additional parameter. /// /// ### 6.4.8. UID Command /// /// * Arguments: /// * command name /// * command arguments /// * Responses: untagged responses: FETCH, SEARCH /// * Result: /// * OK - UID command completed /// * NO - UID command error /// * BAD - command unknown or arguments invalid /// /// The UID command has two forms. In the first form, it takes as its /// arguments a COPY, FETCH, or STORE command with arguments /// appropriate for the associated command. However, the numbers in /// the sequence set argument are unique identifiers instead of /// message sequence numbers. Sequence set ranges are permitted, but /// there is no guarantee that unique identifiers will be contiguous. /// /// A non-existent unique identifier is ignored without any error /// message generated. Thus, it is possible for a UID FETCH command /// to return an OK without any data or a UID COPY or UID STORE to /// return an OK without performing any operations. /// /// In the second form, the UID command takes a SEARCH command with /// SEARCH command arguments. The interpretation of the arguments is /// the same as with SEARCH; however, the numbers returned in a SEARCH /// response for a UID SEARCH command are unique identifiers instead /// of message sequence numbers. For example, the command UID SEARCH /// 1:100 UID 443:557 returns the unique identifiers corresponding to /// the intersection of two sequence sets, the message sequence number /// range 1:100 and the UID range 443:557. /// /// Note: in the above example, the UID range 443:557 /// appears. The same comment about a non-existent unique /// identifier being ignored without any error message also /// applies here. Hence, even if neither UID 443 or 557 /// exist, this range is valid and would include an existing /// UID 495. /// /// Also note that a UID range of 559:* always includes the /// UID of the last message in the mailbox, even if 559 is /// higher than any assigned UID value. This is because the /// contents of a range are independent of the order of the /// range endpoints. Thus, any UID range with * as one of /// the endpoints indicates at least one message (the /// message with the highest numbered UID), unless the /// mailbox is empty. /// /// The number after the "*" in an untagged FETCH response is always a /// message sequence number, not a unique identifier, even for a UID /// command response. However, server implementations MUST implicitly /// include the UID message data item as part of any FETCH response /// caused by a UID command, regardless of whether a UID was specified /// as a message data item to the FETCH. /// /// Note: The rule about including the UID message data item as part /// of a FETCH response primarily applies to the UID FETCH and UID /// STORE commands, including a UID FETCH command that does not /// include UID as a message data item. Although it is unlikely that /// the other UID commands will cause an untagged FETCH, this rule /// applies to these commands as well. // ----- Experimental/Expansion (https://tools.ietf.org/html/rfc3501#section-6.5) ----- // ### 6.5.1. X Command // // * Arguments: implementation defined // * Responses: implementation defined // * Result: // * OK - command completed // * NO - failure // * BAD - command unknown or arguments invalid // // Any command prefixed with an X is an experimental command. // Commands which are not part of this specification, a standard or // standards-track revision of this specification, or an // IESG-approved experimental protocol, MUST use the X prefix. // // Any added untagged responses issued by an experimental command // MUST also be prefixed with an X. Server implementations MUST NOT // send any such untagged responses, unless the client requested it // by issuing the associated experimental command. //X, /// IDLE command. Idle, /// ENABLE command. Enable { /// Capabilities to enable. capabilities: NonEmptyVec>, }, /// COMPRESS command. Compress { /// Compression algorithm. algorithm: CompressionAlgorithm, }, /// Takes the name of a quota root and returns the quota root's resource usage and limits in an untagged QUOTA response. /// /// Arguments: /// * quota root /// /// Responses: /// * REQUIRED untagged responses: QUOTA /// /// Result: /// * OK - getquota completed /// * NO - getquota error: no such quota root, permission denied /// * BAD - command unknown or arguments invalid /// /// # Example (IMAP) /// /// ```imap /// S: * CAPABILITY [...] QUOTA QUOTA=RES-STORAGE [...] /// [...] /// C: G0001 GETQUOTA "!partition/sda4" /// S: * QUOTA "!partition/sda4" (STORAGE 104 10923847) /// S: G0001 OK Getquota complete /// ``` GetQuota { /// Name of quota root. root: AString<'a>, }, /// Takes a mailbox name and returns the list of quota roots for the mailbox in an untagged QUOTAROOT response. /// For each listed quota root, it also returns the quota root's resource usage and limits in an untagged QUOTA response. /// /// Arguments: /// * mailbox name /// /// Responses: /// * REQUIRED untagged responses: QUOTAROOT, QUOTA /// /// Result: /// * OK - getquotaroot completed /// * NO - getquotaroot error: permission denied /// * BAD - command unknown or arguments invalid /// /// Note that the mailbox name parameter doesn't have to reference an existing mailbox. /// This can be handy in order to determine which quota root would apply to a mailbox when it gets created /// /// # Example (IMAP) /// /// ```imap /// S: * CAPABILITY [...] QUOTA QUOTA=RES-STORAGE QUOTA=RES-MESSAGE /// [...] /// C: G0002 GETQUOTAROOT INBOX /// S: * QUOTAROOT INBOX "#user/alice" "!partition/sda4" /// S: * QUOTA "#user/alice" (MESSAGE 42 1000) /// S: * QUOTA "!partition/sda4" (STORAGE 104 10923847) /// S: G0002 OK Getquotaroot complete /// ``` GetQuotaRoot { /// Name of mailbox. mailbox: Mailbox<'a>, }, /// Changes the mailbox quota root resource limits to the specified limits. /// /// Arguments: /// * quota root list of resource limits /// /// Responses: /// * untagged responses: QUOTA /// /// Result: /// /// * OK - setquota completed /// * NO - setquota error: can't set that data /// * BAD - command unknown or arguments invalid /// /// Note: requires the server to advertise the "QUOTASET" capability. /// /// # Example (IMAP) /// /// ```imap /// S: * CAPABILITY [...] QUOTA QUOTASET QUOTA=RES-STORAGE QUOTA=RES- /// MESSAGE [...] /// [...] /// C: S0000 GETQUOTA "#user/alice" /// S: * QUOTA "#user/alice" (STORAGE 54 111 MESSAGE 42 1000) /// S: S0000 OK Getquota completed /// C: S0001 SETQUOTA "#user/alice" (STORAGE 510) /// S: * QUOTA "#user/alice" (STORAGE 58 512) /// // The server has rounded the STORAGE quota limit requested to /// the nearest 512 blocks of 1024 octets; otherwise, another client /// has performed a near-simultaneous SETQUOTA using a limit of 512. /// S: S0001 OK Rounded quota /// C: S0002 SETQUOTA "!partition/sda4" (STORAGE 99999999) /// S: * QUOTA "!partition/sda4" (STORAGE 104 10923847) /// // The server has not changed the quota, since this is a /// filesystem limit, and it cannot be changed. The QUOTA /// response here is entirely optional. /// S: S0002 NO Cannot change system limit /// ``` SetQuota { /// Name of quota root. root: AString<'a>, /// List of resource limits. quotas: Vec>, }, /// MOVE command. Move { /// Set of messages. sequence_set: SequenceSet, /// Destination mailbox. mailbox: Mailbox<'a>, /// Use UID variant. uid: bool, }, } impl<'a> CommandBody<'a> { /// Prepend a tag to finalize the command body to a command. pub fn tag(self, tag: T) -> Result, T::Error> where T: TryInto>, { Ok(Command { tag: tag.try_into()?, body: self, }) } // ----- Constructors ----- /// Construct an AUTHENTICATE command. pub fn authenticate(mechanism: AuthMechanism<'a>) -> Self { CommandBody::Authenticate { mechanism, initial_response: None, } } /// Construct an AUTHENTICATE command (with an initial response, SASL-IR). /// /// Note: Use this only when the server advertised the `SASL-IR` capability. pub fn authenticate_with_ir(mechanism: AuthMechanism<'a>, initial_response: I) -> Self where I: Into>, { CommandBody::Authenticate { mechanism, initial_response: Some(Secret::new(initial_response.into())), } } /// Construct a LOGIN command. pub fn login(username: U, password: P) -> Result> where U: TryInto>, P: TryInto>, { Ok(CommandBody::Login { username: username.try_into().map_err(LoginError::Username)?, password: Secret::new(password.try_into().map_err(LoginError::Password)?), }) } /// Construct a SELECT command. pub fn select(mailbox: M) -> Result where M: TryInto>, { Ok(CommandBody::Select { mailbox: mailbox.try_into()?, }) } /// Construct an EXAMINE command. pub fn examine(mailbox: M) -> Result where M: TryInto>, { Ok(CommandBody::Examine { mailbox: mailbox.try_into()?, }) } /// Construct a CREATE command. pub fn create(mailbox: M) -> Result where M: TryInto>, { Ok(CommandBody::Create { mailbox: mailbox.try_into()?, }) } /// Construct a DELETE command. pub fn delete(mailbox: M) -> Result where M: TryInto>, { Ok(CommandBody::Delete { mailbox: mailbox.try_into()?, }) } /// Construct a RENAME command. pub fn rename(mailbox: F, new_mailbox: T) -> Result> where F: TryInto>, T: TryInto>, { Ok(CommandBody::Rename { from: mailbox.try_into().map_err(RenameError::From)?, to: new_mailbox.try_into().map_err(RenameError::To)?, }) } /// Construct a SUBSCRIBE command. pub fn subscribe(mailbox: M) -> Result where M: TryInto>, { Ok(CommandBody::Subscribe { mailbox: mailbox.try_into()?, }) } /// Construct an UNSUBSCRIBE command. pub fn unsubscribe(mailbox: M) -> Result where M: TryInto>, { Ok(CommandBody::Unsubscribe { mailbox: mailbox.try_into()?, }) } /// Construct a LIST command. pub fn list( reference: A, mailbox_wildcard: B, ) -> Result> where A: TryInto>, B: TryInto>, { Ok(CommandBody::List { reference: reference.try_into().map_err(ListError::Reference)?, mailbox_wildcard: mailbox_wildcard.try_into().map_err(ListError::Mailbox)?, }) } /// Construct a LSUB command. pub fn lsub( reference: A, mailbox_wildcard: B, ) -> Result> where A: TryInto>, B: TryInto>, { Ok(CommandBody::Lsub { reference: reference.try_into().map_err(ListError::Reference)?, mailbox_wildcard: mailbox_wildcard.try_into().map_err(ListError::Mailbox)?, }) } /// Construct a STATUS command. pub fn status(mailbox: M, item_names: I) -> Result where M: TryInto>, I: Into>, { let mailbox = mailbox.try_into()?; Ok(CommandBody::Status { mailbox, item_names: item_names.into(), }) } /// Construct an APPEND command. pub fn append( mailbox: M, flags: Vec>, date: Option, message: D, ) -> Result> where M: TryInto>, D: TryInto>, { Ok(CommandBody::Append { mailbox: mailbox.try_into().map_err(AppendError::Mailbox)?, flags, date, message: message.try_into().map_err(AppendError::Data)?, }) } /// Construct a SEARCH command. pub fn search(charset: Option>, criteria: SearchKey<'a>, uid: bool) -> Self { CommandBody::Search { charset, criteria, uid, } } /// Construct a FETCH command. pub fn fetch(sequence_set: S, macro_or_item_names: I, uid: bool) -> Result where S: TryInto, I: Into>, { let sequence_set = sequence_set.try_into()?; Ok(CommandBody::Fetch { sequence_set, macro_or_item_names: macro_or_item_names.into(), uid, }) } /// Construct a STORE command. pub fn store( sequence_set: S, kind: StoreType, response: StoreResponse, flags: Vec>, uid: bool, ) -> Result where S: TryInto, { let sequence_set = sequence_set.try_into()?; Ok(CommandBody::Store { sequence_set, kind, response, flags, uid, }) } /// Construct a COPY command. pub fn copy( sequence_set: S, mailbox: M, uid: bool, ) -> Result> where S: TryInto, M: TryInto>, { Ok(CommandBody::Copy { sequence_set: sequence_set.try_into().map_err(CopyError::Sequence)?, mailbox: mailbox.try_into().map_err(CopyError::Mailbox)?, uid, }) } /// Get the name of the command. pub fn name(&self) -> &'static str { match self { Self::Capability => "CAPABILITY", Self::Noop => "NOOP", Self::Logout => "LOGOUT", #[cfg(feature = "starttls")] Self::StartTLS => "STARTTLS", Self::Authenticate { .. } => "AUTHENTICATE", Self::Login { .. } => "LOGIN", Self::Select { .. } => "SELECT", Self::Unselect => "UNSELECT", Self::Examine { .. } => "EXAMINE", Self::Create { .. } => "CREATE", Self::Delete { .. } => "DELETE", Self::Rename { .. } => "RENAME", Self::Subscribe { .. } => "SUBSCRIBE", Self::Unsubscribe { .. } => "UNSUBSCRIBE", Self::List { .. } => "LIST", Self::Lsub { .. } => "LSUB", Self::Status { .. } => "STATUS", Self::Append { .. } => "APPEND", Self::Check => "CHECK", Self::Close => "CLOSE", Self::Expunge => "EXPUNGE", Self::Search { .. } => "SEARCH", Self::Fetch { .. } => "FETCH", Self::Store { .. } => "STORE", Self::Copy { .. } => "COPY", Self::Idle => "IDLE", Self::Enable { .. } => "ENABLE", Self::Compress { .. } => "COMPRESS", Self::GetQuota { .. } => "GETQUOTA", Self::GetQuotaRoot { .. } => "GETQUOTAROOT", Self::SetQuota { .. } => "SETQUOTA", Self::Move { .. } => "MOVE", } } } /// Error-related types. pub mod error { use thiserror::Error; #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub enum LoginError { #[error("Invalid username: {0}")] Username(U), #[error("Invalid password: {0}")] Password(P), } #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub enum RenameError { #[error("Invalid (from) mailbox: {0}")] From(F), #[error("Invalid (to) mailbox: {0}")] To(T), } #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub enum ListError { #[error("Invalid reference: {0}")] Reference(R), #[error("Invalid mailbox: {0}")] Mailbox(M), } #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub enum AppendError { #[error("Invalid mailbox: {0}")] Mailbox(M), #[error("Invalid data: {0}")] Data(D), } #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub enum CopyError { #[error("Invalid sequence: {0}")] Sequence(S), #[error("Invalid mailbox: {0}")] Mailbox(M), } } #[cfg(test)] mod tests { use chrono::DateTime as ChronoDateTime; use super::*; use crate::{ auth::AuthMechanism, core::{AString, Charset, IString, Literal, NonEmptyVec}, datetime::DateTime, extensions::{ compress::CompressionAlgorithm, enable::{CapabilityEnable, Utf8Kind}, }, fetch::{Macro, MacroOrMessageDataItemNames, MessageDataItemName, Part, Section}, flag::{Flag, StoreType}, mailbox::{ListMailbox, Mailbox}, search::SearchKey, secret::Secret, sequence::{SeqOrUid, Sequence, SequenceSet}, status::StatusDataItemName, }; #[test] fn test_conversion_command_body() { let cmds = vec![ CommandBody::Capability, CommandBody::Noop, CommandBody::Logout, #[cfg(feature = "starttls")] CommandBody::StartTLS, CommandBody::authenticate(AuthMechanism::Plain), CommandBody::authenticate(AuthMechanism::Login), CommandBody::authenticate_with_ir(AuthMechanism::Plain, b"XXXXXXXX".as_ref()), CommandBody::authenticate_with_ir(AuthMechanism::Login, b"YYYYYYYY".as_ref()), CommandBody::login("alice", "I_am_an_atom").unwrap(), CommandBody::login("alice", "I am \\ \"quoted\"").unwrap(), CommandBody::login("alice", "I am a literal²").unwrap(), CommandBody::login( AString::Atom("alice".try_into().unwrap()), AString::String(crate::core::IString::Literal( vec![0xff, 0xff, 0xff].try_into().unwrap(), )), ) .unwrap(), CommandBody::select("inbox").unwrap(), CommandBody::select("atom").unwrap(), CommandBody::select("C:\\").unwrap(), CommandBody::select("²").unwrap(), CommandBody::select("Trash").unwrap(), CommandBody::examine("inbox").unwrap(), CommandBody::examine("atom").unwrap(), CommandBody::examine("C:\\").unwrap(), CommandBody::examine("²").unwrap(), CommandBody::examine("Trash").unwrap(), CommandBody::create("inBoX").unwrap(), CommandBody::delete("inBOX").unwrap(), CommandBody::rename("iNBoS", "INboX").unwrap(), CommandBody::subscribe("inbox").unwrap(), CommandBody::unsubscribe("INBOX").unwrap(), CommandBody::list("iNbOx", "test").unwrap(), CommandBody::list("inbox", ListMailbox::Token("test".try_into().unwrap())).unwrap(), CommandBody::lsub( "inbox", ListMailbox::String(IString::Quoted("\x7f".try_into().unwrap())), ) .unwrap(), CommandBody::list("inBoX", ListMailbox::Token("test".try_into().unwrap())).unwrap(), CommandBody::lsub( "INBOX", ListMailbox::String(IString::Quoted("\x7f".try_into().unwrap())), ) .unwrap(), CommandBody::status("inbox", vec![StatusDataItemName::Messages]).unwrap(), CommandBody::append( "inbox", vec![], Some( DateTime::try_from( ChronoDateTime::parse_from_rfc2822("Tue, 1 Jul 2003 10:52:37 +0200") .unwrap(), ) .unwrap(), ), vec![0xff, 0xff, 0xff], ) .unwrap(), CommandBody::append( "inbox", vec![Flag::Keyword("test".try_into().unwrap())], Some( DateTime::try_from( ChronoDateTime::parse_from_rfc2822("Tue, 1 Jul 2003 10:52:37 +0200") .unwrap(), ) .unwrap(), ), vec![0xff, 0xff, 0xff], ) .unwrap(), CommandBody::Check, CommandBody::Close, CommandBody::Expunge, CommandBody::search( None, SearchKey::And( vec![SearchKey::All, SearchKey::New, SearchKey::Unseen] .try_into() .unwrap(), ), false, ), CommandBody::search( None, SearchKey::And( vec![SearchKey::All, SearchKey::New, SearchKey::Unseen] .try_into() .unwrap(), ), true, ), CommandBody::search( None, SearchKey::And( vec![SearchKey::SequenceSet(SequenceSet( vec![Sequence::Single(SeqOrUid::Value(42.try_into().unwrap()))] .try_into() .unwrap(), ))] .try_into() .unwrap(), ), true, ), CommandBody::search(None, SearchKey::SequenceSet("42".try_into().unwrap()), true), CommandBody::search(None, SearchKey::SequenceSet("*".try_into().unwrap()), true), CommandBody::search( None, SearchKey::Or(Box::new(SearchKey::Draft), Box::new(SearchKey::All)), true, ), CommandBody::search( Some(Charset::try_from("UTF-8").unwrap()), SearchKey::Or(Box::new(SearchKey::Draft), Box::new(SearchKey::All)), true, ), CommandBody::fetch( "1", vec![MessageDataItemName::BodyExt { partial: None, section: Some(Section::Part(Part( vec![1.try_into().unwrap(), 1.try_into().unwrap()] .try_into() .unwrap(), ))), peek: true, }], false, ) .unwrap(), CommandBody::fetch("1:*,2,3", Macro::Full, true).unwrap(), CommandBody::store( "1,2:*", StoreType::Remove, StoreResponse::Answer, vec![Flag::Seen, Flag::Draft], false, ) .unwrap(), CommandBody::store( "1:5", StoreType::Add, StoreResponse::Answer, vec![Flag::Keyword("TEST".try_into().unwrap())], true, ) .unwrap(), CommandBody::copy("1", "inbox", false).unwrap(), CommandBody::copy("1337", "archive", true).unwrap(), ]; for (no, cmd_body) in cmds.into_iter().enumerate() { println!("Test: {}, {:?}", no, cmd_body); let _ = cmd_body.tag(format!("A{}", no)).unwrap(); } } #[test] fn test_command_body_name() { let tests = [ (CommandBody::Capability, "CAPABILITY"), (CommandBody::Noop, "NOOP"), (CommandBody::Logout, "LOGOUT"), #[cfg(feature = "starttls")] (CommandBody::StartTLS, "STARTTLS"), ( CommandBody::Authenticate { mechanism: AuthMechanism::Plain, initial_response: None, }, "AUTHENTICATE", ), ( CommandBody::Login { username: AString::try_from("user").unwrap(), password: Secret::new(AString::try_from("pass").unwrap()), }, "LOGIN", ), ( CommandBody::Select { mailbox: Mailbox::Inbox, }, "SELECT", ), (CommandBody::Unselect, "UNSELECT"), ( CommandBody::Examine { mailbox: Mailbox::Inbox, }, "EXAMINE", ), ( CommandBody::Create { mailbox: Mailbox::Inbox, }, "CREATE", ), ( CommandBody::Delete { mailbox: Mailbox::Inbox, }, "DELETE", ), ( CommandBody::Rename { from: Mailbox::Inbox, to: Mailbox::Inbox, }, "RENAME", ), ( CommandBody::Subscribe { mailbox: Mailbox::Inbox, }, "SUBSCRIBE", ), ( CommandBody::Unsubscribe { mailbox: Mailbox::Inbox, }, "UNSUBSCRIBE", ), ( CommandBody::List { reference: Mailbox::Inbox, mailbox_wildcard: ListMailbox::try_from("").unwrap(), }, "LIST", ), ( CommandBody::Lsub { reference: Mailbox::Inbox, mailbox_wildcard: ListMailbox::try_from("").unwrap(), }, "LSUB", ), ( CommandBody::Status { mailbox: Mailbox::Inbox, item_names: vec![].into(), }, "STATUS", ), ( CommandBody::Append { mailbox: Mailbox::Inbox, date: None, message: Literal::try_from("").unwrap(), flags: vec![], }, "APPEND", ), (CommandBody::Check, "CHECK"), (CommandBody::Close, "CLOSE"), (CommandBody::Expunge, "EXPUNGE"), ( CommandBody::Search { charset: None, criteria: SearchKey::Recent, uid: true, }, "SEARCH", ), ( CommandBody::Fetch { sequence_set: SequenceSet::try_from(1u32).unwrap(), macro_or_item_names: MacroOrMessageDataItemNames::Macro(Macro::Full), uid: true, }, "FETCH", ), ( CommandBody::Store { sequence_set: SequenceSet::try_from(1).unwrap(), flags: vec![], response: StoreResponse::Silent, kind: StoreType::Add, uid: true, }, "STORE", ), ( CommandBody::Copy { sequence_set: SequenceSet::try_from(1).unwrap(), mailbox: Mailbox::Inbox, uid: true, }, "COPY", ), (CommandBody::Idle, "IDLE"), ( CommandBody::Enable { capabilities: NonEmptyVec::from(CapabilityEnable::Utf8(Utf8Kind::Only)), }, "ENABLE", ), ( CommandBody::Compress { algorithm: CompressionAlgorithm::Deflate, }, "COMPRESS", ), ( CommandBody::GetQuota { root: AString::try_from("root").unwrap(), }, "GETQUOTA", ), ( CommandBody::GetQuotaRoot { mailbox: Mailbox::Inbox, }, "GETQUOTAROOT", ), ( CommandBody::SetQuota { root: AString::try_from("root").unwrap(), quotas: vec![], }, "SETQUOTA", ), ( CommandBody::Move { sequence_set: SequenceSet::try_from(1).unwrap(), mailbox: Mailbox::Inbox, uid: true, }, "MOVE", ), ]; for (test, expected) in tests { assert_eq!(test.name(), expected); } } } imap-codec-1.0.0/imap-types/src/core.rs000066400000000000000000001531061447115025300176760ustar00rootroot00000000000000//! Core data types. //! //! To ensure correctness and to support all forms of data transmission, imap-types uses types such //! as [`AString`], [`Atom`], [`IString`], [`Quoted`], and [`Literal`]. When constructing messages, //! imap-types can automatically choose the best representation. However, it's always possible to //! manually select a specific representation. //! //! The core types exist for two reasons. First, they guarantee that invalid messages cannot be //! produced. For example, a [`Tag`] will never contain whitespace as this would break parsing. //! Furthermore, the representation of a value may change the IMAP protocol flow. A username, for //! example, can be represented as an atom, a quoted string, or a literal. While atoms and quoted //! strings are similar, a literal requires a different protocol flow and implementations must take //! this into account. //! //! While this seems complicated at first, there are good news: You don't need to think about IMAP //! too much. imap-types *ensures* that everything you do is correct. If you are able to construct //! an invalid message, this is considered a bug in imap-types. //! //! # Overview //! //! ```text //! ┌───────┐ ┌─────────────────┐ //! │AString│ │ NString │ //! └──┬─┬──┘ │(Option)│ //! │ │ └─────┬───────────┘ //! │ └──────┐ │ //! │ │ │ //! ┌────┐ ┌──▼────┐ ┌─▼───▼─┐ //! │Atom│ │AtomExt│ │IString│ //! └────┘ └───────┘ └┬─────┬┘ //! │ │ //! ┌─────▼─┐ ┌─▼────┐ //! │Literal│ │Quoted│ //! └───────┘ └──────┘ //! ``` use std::{ borrow::Cow, fmt::{Debug, Display, Formatter}, str::from_utf8, vec::IntoIter, }; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::utils::indicators::{ is_any_text_char_except_quoted_specials, is_astring_char, is_atom_char, is_char8, is_text_char, }; macro_rules! impl_try_from { ($via:ty, $lifetime:lifetime, $from:ty, $target:ty) => { impl<$lifetime> TryFrom<$from> for $target { type Error = <$via as TryFrom<$from>>::Error; fn try_from(value: $from) -> Result { let value = <$via>::try_from(value)?; Ok(Self::from(value)) } } }; } pub(crate) use impl_try_from; use crate::error::{ValidationError, ValidationErrorKind}; /// A string subset to model IMAP's `atom`s. /// /// Rules: /// /// * Length must be >= 1 /// * Only some characters are allowed, e.g., no whitespace /// /// # ABNF definition /// /// ```abnf /// atom = 1*ATOM-CHAR /// ATOM-CHAR = /// CHAR = %x01-7F /// ; any 7-bit US-ASCII character, excluding NUL /// atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards / quoted-specials / resp-specials /// SP = %x20 /// CTL = %x00-1F / %x7F /// ; controls /// list-wildcards = "%" / "*" /// quoted-specials = DQUOTE / "\" /// DQUOTE = %x22 /// ; " (Double Quote) /// resp-specials = "]" /// ``` #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] pub struct Atom<'a>(pub(crate) Cow<'a, str>); // We want a slightly more dense `Debug` implementation. impl<'a> Debug for Atom<'a> { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { write!(f, "Atom({:?})", self.0) } } impl<'a> Atom<'a> { /// Validates if value conforms to atom's ABNF definition. pub fn validate(value: impl AsRef<[u8]>) -> Result<(), ValidationError> { let value = value.as_ref(); if value.is_empty() { return Err(ValidationError::new(ValidationErrorKind::Empty)); } if let Some(at) = value.iter().position(|b| !is_atom_char(*b)) { return Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: value[at], at, })); }; Ok(()) } /// Returns a reference to the inner value. pub fn inner(&self) -> &str { self.0.as_ref() } /// Consumes the atom, returning the inner value. pub fn into_inner(self) -> Cow<'a, str> { self.0 } /// Constructs an atom without validation. /// /// # Warning: IMAP conformance /// /// The caller must ensure that `inner` is valid according to [`Self::validate`]. Failing to do /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows. /// Do not call this constructor with untrusted data. #[cfg(feature = "unvalidated")] #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))] pub fn unvalidated(inner: C) -> Self where C: Into>, { let inner = inner.into(); #[cfg(debug_assertions)] Self::validate(inner.as_bytes()).unwrap(); Self(inner) } } impl<'a> TryFrom<&'a [u8]> for Atom<'a> { type Error = ValidationError; fn try_from(value: &'a [u8]) -> Result { Self::validate(value)?; // Safety: `unwrap` can't panic due to `validate`. Ok(Self(Cow::Borrowed(from_utf8(value).unwrap()))) } } impl<'a> TryFrom> for Atom<'a> { type Error = ValidationError; fn try_from(value: Vec) -> Result { Self::validate(&value)?; // Safety: `unwrap` can't panic due to `validate`. Ok(Self(Cow::Owned(String::from_utf8(value).unwrap()))) } } impl<'a> TryFrom<&'a str> for Atom<'a> { type Error = ValidationError; fn try_from(value: &'a str) -> Result { Self::validate(value)?; Ok(Self(Cow::Borrowed(value))) } } impl<'a> TryFrom for Atom<'a> { type Error = ValidationError; fn try_from(value: String) -> Result { Self::validate(&value)?; Ok(Atom(Cow::Owned(value))) } } impl<'a> TryFrom> for Atom<'a> { type Error = ValidationError; fn try_from(value: Cow<'a, str>) -> Result { Self::validate(value.as_bytes())?; Ok(Atom(value)) } } impl<'a> AsRef for Atom<'a> { fn as_ref(&self) -> &str { self.0.as_ref() } } impl<'a> Display for Atom<'a> { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { write!(f, "{}", self.0) } } /// A string subset to model IMAP's `1*ASTRING-CHAR` ("extended `atom`"). /// /// This type is required due to the use of `1*ASTRING-CHAR` in `astring`, see ABNF definition below. /// /// Rules: /// /// * Length must be >= 1 /// * Only some characters are allowed, e.g., no whitespace /// /// # ABNF definition /// /// ```abnf /// astring = 1*ASTRING-CHAR / string /// ; ^^^^^^^^^^^^^^ /// ; | /// ; `AtomExt` /// /// ASTRING-CHAR = ATOM-CHAR / resp-specials /// ; ^^^^^^^^^ ^^^^^^^^^^^^^ /// ; | | /// ; | Additionally allowed in `AtomExt` /// ; See `Atom` /// ``` #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, PartialEq, Eq, Hash)] pub struct AtomExt<'a>(pub(crate) Cow<'a, str>); // We want a slightly more dense `Debug` implementation. impl<'a> Debug for AtomExt<'a> { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { write!(f, "AtomExt({:?})", self.0) } } impl<'a> AtomExt<'a> { /// Validates if value conforms to extended atom's ABNF definition. pub fn validate(value: impl AsRef<[u8]>) -> Result<(), ValidationError> { let value = value.as_ref(); if value.is_empty() { return Err(ValidationError::new(ValidationErrorKind::Empty)); } if let Some(at) = value.iter().position(|b| !is_astring_char(*b)) { return Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: value[at], at, })); }; Ok(()) } /// Returns a reference to the inner value. pub fn inner(&self) -> &str { self.0.as_ref() } /// Consumes the atom, returning the inner value. pub fn into_inner(self) -> Cow<'a, str> { self.0 } /// Constructs an extended atom without validation. /// /// # Warning: IMAP conformance /// /// The caller must ensure that `inner` is valid according to [`Self::validate`]. Failing to do /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows. /// Do not call this constructor with untrusted data. #[cfg(feature = "unvalidated")] #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))] pub fn unvalidated(inner: C) -> Self where C: Into>, { let inner = inner.into(); #[cfg(debug_assertions)] Self::validate(inner.as_bytes()).unwrap(); Self(inner) } } impl<'a> TryFrom<&'a [u8]> for AtomExt<'a> { type Error = ValidationError; fn try_from(value: &'a [u8]) -> Result { Self::validate(value)?; // Safety: `unwrap` can't panic due to `validate`. Ok(Self(Cow::Borrowed(from_utf8(value).unwrap()))) } } impl<'a> TryFrom> for AtomExt<'a> { type Error = ValidationError; fn try_from(value: Vec) -> Result { Self::validate(&value)?; // Safety: `unwrap` can't panic due to `validate`. Ok(Self(Cow::Owned(String::from_utf8(value).unwrap()))) } } impl<'a> TryFrom<&'a str> for AtomExt<'a> { type Error = ValidationError; fn try_from(value: &'a str) -> Result { Self::validate(value)?; Ok(Self(Cow::Borrowed(value))) } } impl<'a> TryFrom for AtomExt<'a> { type Error = ValidationError; fn try_from(value: String) -> Result { Self::validate(&value)?; Ok(Self(Cow::Owned(value))) } } impl<'a> From> for AtomExt<'a> { fn from(value: Atom<'a>) -> Self { Self(value.0) } } impl<'a> AsRef for AtomExt<'a> { fn as_ref(&self) -> &str { &self.0 } } /// Either a quoted string or a literal. /// /// Note: The empty string is represented as either "" (a quoted string with zero characters between /// double quotes) or as {0} followed by CRLF (a literal with an octet count of 0). /// /// # ABNF definition /// /// ```abnf /// string = quoted / literal /// ; ^^^^^^ ^^^^^^^ /// ; | | /// ; | See `Literal` /// ; See `Quoted` /// ``` #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum IString<'a> { /// Literal, see [`Literal`]. Literal(Literal<'a>), /// Quoted string, see[`Quoted`]. Quoted(Quoted<'a>), } impl<'a> IString<'a> { pub fn into_inner(self) -> Cow<'a, [u8]> { match self { Self::Literal(literal) => literal.into_inner(), Self::Quoted(quoted) => match quoted.into_inner() { Cow::Borrowed(s) => Cow::Borrowed(s.as_bytes()), Cow::Owned(s) => Cow::Owned(s.into_bytes()), }, } } } impl<'a> TryFrom<&'a [u8]> for IString<'a> { type Error = ValidationError; fn try_from(value: &'a [u8]) -> Result { if let Ok(quoted) = Quoted::try_from(value) { return Ok(IString::Quoted(quoted)); } Ok(IString::Literal(Literal::try_from(value)?)) } } impl TryFrom> for IString<'_> { type Error = ValidationError; fn try_from(value: Vec) -> Result { // TODO(efficiency) if let Ok(quoted) = Quoted::try_from(value.clone()) { return Ok(IString::Quoted(quoted)); } Ok(IString::Literal(Literal::try_from(value)?)) } } impl<'a> TryFrom<&'a str> for IString<'a> { type Error = ValidationError; fn try_from(value: &'a str) -> Result { if let Ok(quoted) = Quoted::try_from(value) { return Ok(IString::Quoted(quoted)); } Ok(IString::Literal(Literal::try_from(value)?)) } } impl<'a> TryFrom for IString<'a> { type Error = ValidationError; fn try_from(value: String) -> Result { // TODO(efficiency) if let Ok(quoted) = Quoted::try_from(value.clone()) { return Ok(IString::Quoted(quoted)); } Ok(IString::Literal(Literal::try_from(value)?)) } } impl<'a> From> for IString<'a> { fn from(value: Literal<'a>) -> Self { Self::Literal(value) } } impl<'a> From> for IString<'a> { fn from(value: Quoted<'a>) -> Self { Self::Quoted(value) } } impl<'a> AsRef<[u8]> for IString<'a> { fn as_ref(&self) -> &[u8] { match self { Self::Quoted(quoted) => quoted.as_ref().as_bytes(), Self::Literal(literal) => literal.as_ref(), } } } /// A sequence of zero or more (non-null) bytes prefixed with a length. /// /// "A literal is a sequence of zero or more octets (including CR and LF), prefix-quoted with an octet count in the form of an open brace ("{"), the number of octets, close brace ("}"), and CRLF. /// In the case of literals transmitted from server to client, the CRLF is immediately followed by the octet data. /// In the case of literals transmitted from client to server, the client MUST wait to receive a command continuation request (...) before sending the octet data (and the remainder of the command). /// /// Note: Even if the octet count is 0, a client transmitting a literal MUST wait to receive a command continuation request." ([RFC 3501](https://www.rfc-editor.org/rfc/rfc3501.html)) /// /// # ABNF definition /// /// ```abnf /// literal = "{" number "}" CRLF *CHAR8 /// ; Number represents the number of CHAR8s /// number = 1*DIGIT /// ; Unsigned 32-bit integer /// ; (0 <= n < 4,294,967,296) /// DIGIT = %x30-39 /// ; 0-9 /// CRLF = CR LF /// ; Internet standard newline /// CHAR8 = %x01-ff /// ; any OCTET except NUL, %x00 /// ``` #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, PartialEq, Eq, Hash)] pub struct Literal<'a> { pub(crate) data: Cow<'a, [u8]>, /// Specifies whether this is a synchronizing or non-synchronizing literal. /// /// `true` (default) denotes a synchronizing literal, e.g., `{3}\r\nfoo`. /// `false` denotes a non-synchronizing literal, e.g., `{3+}\r\nfoo`. /// /// Note: In the special case that a server advertised a `LITERAL-` capability, AND the literal /// has more than 4096 bytes a non-synchronizing literal must still be treated as synchronizing. pub(crate) mode: LiteralMode, } // We want a more readable `Debug` implementation. impl<'a> Debug for Literal<'a> { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { struct BStr<'a>(&'a Cow<'a, [u8]>); impl<'a> Debug for BStr<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "b\"{}\"", crate::utils::escape_byte_string(self.0.as_ref()) ) } } f.debug_struct("Literal") .field("data", &BStr(&self.data)) .field("mode", &self.mode) .finish() } } impl<'a> Literal<'a> { pub fn validate(value: impl AsRef<[u8]>) -> Result<(), ValidationError> { let value = value.as_ref(); if let Some(at) = value.iter().position(|b| !is_char8(*b)) { return Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: value[at], at, })); }; Ok(()) } pub fn data(&self) -> &[u8] { self.data.as_ref() } pub fn mode(&self) -> LiteralMode { self.mode } pub fn set_mode(&mut self, mode: LiteralMode) { self.mode = mode; } pub fn into_sync(mut self) -> Self { self.mode = LiteralMode::Sync; self } pub fn into_non_sync(mut self) -> Self { self.mode = LiteralMode::NonSync; self } pub fn into_inner(self) -> Cow<'a, [u8]> { self.data } /// Constructs a literal without validation. /// /// # Warning: IMAP conformance /// /// The caller must ensure that `data` is valid according to [`Self::validate`]. Failing to do /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows. /// Do not call this constructor with untrusted data. #[cfg(feature = "unvalidated")] #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))] pub fn unvalidated(data: D) -> Self where D: Into>, { let data = data.into(); #[cfg(debug_assertions)] Self::validate(&data).unwrap(); Self { data, mode: LiteralMode::Sync, } } #[cfg(feature = "unvalidated")] #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))] pub fn unvalidated_non_sync(data: D) -> Self where D: Into>, { let data = data.into(); #[cfg(debug_assertions)] Self::validate(&data).unwrap(); Self { data, mode: LiteralMode::NonSync, } } } impl<'a> TryFrom<&'a [u8]> for Literal<'a> { type Error = ValidationError; fn try_from(value: &'a [u8]) -> Result { Self::validate(value)?; Ok(Literal { data: Cow::Borrowed(value), mode: LiteralMode::Sync, }) } } impl<'a> TryFrom> for Literal<'a> { type Error = ValidationError; fn try_from(value: Vec) -> Result { Self::validate(&value)?; Ok(Literal { data: Cow::Owned(value), mode: LiteralMode::Sync, }) } } impl<'a> TryFrom<&'a str> for Literal<'a> { type Error = ValidationError; fn try_from(value: &'a str) -> Result { Self::validate(value)?; Ok(Literal { data: Cow::Borrowed(value.as_bytes()), mode: LiteralMode::Sync, }) } } impl<'a> TryFrom for Literal<'a> { type Error = ValidationError; fn try_from(value: String) -> Result { Self::validate(&value)?; Ok(Literal { data: Cow::Owned(value.into_bytes()), mode: LiteralMode::Sync, }) } } impl<'a> AsRef<[u8]> for Literal<'a> { fn as_ref(&self) -> &[u8] { &self.data } } /// Literal mode, i.e., sync or non-sync. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum LiteralMode { /// A synchronizing literal, i.e., `{}\r\n`. Sync, /// A non-synchronizing literal according to RFC 7888, i.e., `{+}\r\n`. NonSync, } /// A quoted string. /// /// "The quoted string form is an alternative that avoids the overhead of processing a literal at the cost of limitations of characters which may be used. /// /// A quoted string is a sequence of zero or more 7-bit characters, excluding CR and LF, with double quote (<">) characters at each end." ([RFC 3501](https://www.rfc-editor.org/rfc/rfc3501.html)) /// /// # ABNF definition /// /// ```abnf /// quoted = DQUOTE *QUOTED-CHAR DQUOTE /// DQUOTE = %x22 /// ; " (Double Quote) /// QUOTED-CHAR = / "\" quoted-specials /// TEXT-CHAR = /// CHAR = %x01-7F /// ; any 7-bit US-ASCII character, excluding NUL /// CR = %x0D /// ; carriage return /// LF = %x0A /// ; linefeed /// quoted-specials = DQUOTE / "\" /// ``` #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, PartialEq, Eq, Hash)] pub struct Quoted<'a>(pub(crate) Cow<'a, str>); impl<'a> Debug for Quoted<'a> { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { write!(f, "Quoted({:?})", self.0) } } impl<'a> Quoted<'a> { pub fn validate(value: impl AsRef<[u8]>) -> Result<(), ValidationError> { let value = value.as_ref(); if let Some(at) = value.iter().position(|b| !is_text_char(*b)) { return Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: value[at], at, })); }; Ok(()) } pub fn inner(&self) -> &str { self.0.as_ref() } pub fn into_inner(self) -> Cow<'a, str> { self.0 } /// Constructs a quoted string without validation. /// /// # Warning: IMAP conformance /// /// The caller must ensure that `inner` is valid according to [`Self::validate`]. Failing to do /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows. /// Do not call this constructor with untrusted data. #[cfg(feature = "unvalidated")] #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))] pub fn unvalidated(inner: C) -> Self where C: Into>, { let inner = inner.into(); #[cfg(debug_assertions)] Self::validate(inner.as_bytes()).unwrap(); Self(inner) } } impl<'a> TryFrom<&'a [u8]> for Quoted<'a> { type Error = ValidationError; fn try_from(value: &'a [u8]) -> Result { Quoted::validate(value)?; // Safety: `unwrap` can't panic due to `validate`. Ok(Quoted(Cow::Borrowed(from_utf8(value).unwrap()))) } } impl TryFrom> for Quoted<'_> { type Error = ValidationError; fn try_from(value: Vec) -> Result { Quoted::validate(&value)?; // Safety: `unwrap` can't panic due to `validate`. Ok(Quoted(Cow::Owned(String::from_utf8(value).unwrap()))) } } impl<'a> TryFrom<&'a str> for Quoted<'a> { type Error = ValidationError; fn try_from(value: &'a str) -> Result { Quoted::validate(value)?; Ok(Quoted(Cow::Borrowed(value))) } } impl<'a> TryFrom for Quoted<'a> { type Error = ValidationError; fn try_from(value: String) -> Result { Quoted::validate(&value)?; Ok(Quoted(Cow::Owned(value))) } } impl<'a> AsRef for Quoted<'a> { fn as_ref(&self) -> &str { &self.0 } } /// Either NIL or a string. /// /// This is modeled using Rust's `Option` type. /// /// # ABNF definition /// /// ```abnf /// nstring = string / nil /// ; ^^^^^^ /// ; | /// ; See `IString` /// /// nil = "NIL" /// ``` #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct NString<'a>( // This wrapper is merely used for formatting. // The inner value can be public. pub Option>, ); impl<'a> NString<'a> { pub fn into_option(self) -> Option> { self.0.map(|inner| inner.into_inner()) } } macro_rules! impl_try_from_nstring { ($from:ty) => { impl<'a> TryFrom<$from> for NString<'a> { type Error = ValidationError; fn try_from(value: $from) -> Result { Ok(Self(Some(IString::try_from(value)?))) } } }; } impl_try_from_nstring!(&'a [u8]); impl_try_from_nstring!(Vec); impl_try_from_nstring!(&'a str); impl_try_from_nstring!(String); impl<'a> From> for NString<'a> { fn from(value: Literal<'a>) -> Self { Self(Some(IString::from(value))) } } impl<'a> From> for NString<'a> { fn from(value: Quoted<'a>) -> Self { Self(Some(IString::from(value))) } } /// Either an (extended) atom or a string. /// /// # ABNF definition /// /// ```abnf /// astring = 1*ASTRING-CHAR / string /// ; ^^^^^^^^^^^^^^ /// ; | /// ; See `AtomExt` /// ``` #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum AString<'a> { // `1*ATOM-CHAR` does not allow resp-specials, but `1*ASTRING-CHAR` does ... :-/ Atom(AtomExt<'a>), // 1*ASTRING-CHAR / String(IString<'a>), // string } impl<'a> TryFrom<&'a [u8]> for AString<'a> { type Error = ValidationError; fn try_from(value: &'a [u8]) -> Result { if let Ok(atom) = AtomExt::try_from(value) { return Ok(AString::Atom(atom)); } Ok(AString::String(IString::try_from(value)?)) } } impl TryFrom> for AString<'_> { type Error = ValidationError; fn try_from(value: Vec) -> Result { // TODO(efficiency) if let Ok(atom) = AtomExt::try_from(value.clone()) { return Ok(AString::Atom(atom)); } Ok(AString::String(IString::try_from(value)?)) } } impl<'a> TryFrom<&'a str> for AString<'a> { type Error = ValidationError; fn try_from(value: &'a str) -> Result { if let Ok(atom) = AtomExt::try_from(value) { return Ok(AString::Atom(atom)); } Ok(AString::String(IString::try_from(value)?)) } } impl<'a> TryFrom for AString<'a> { type Error = ValidationError; fn try_from(value: String) -> Result { // TODO(efficiency) if let Ok(atom) = AtomExt::try_from(value.clone()) { return Ok(AString::Atom(atom)); } Ok(AString::String(IString::try_from(value)?)) } } impl<'a> From> for AString<'a> { fn from(atom: Atom<'a>) -> Self { AString::Atom(AtomExt::from(atom)) } } impl<'a> From> for AString<'a> { fn from(atom: AtomExt<'a>) -> Self { AString::Atom(atom) } } impl<'a> From> for AString<'a> { fn from(value: Quoted<'a>) -> Self { AString::String(IString::Quoted(value)) } } impl<'a> From> for AString<'a> { fn from(value: Literal<'a>) -> Self { AString::String(IString::Literal(value)) } } impl<'a> AsRef<[u8]> for AString<'a> { fn as_ref(&self) -> &[u8] { match self { Self::Atom(atom_ext) => atom_ext.as_ref().as_bytes(), Self::String(istr) => istr.as_ref(), } } } /// A short alphanumeric identifier. /// /// Each client command is prefixed with an identifier (typically, e.g., A0001, A0002, etc.) called /// a "tag". /// /// # ABNF definition /// /// ```abnf /// tag = 1* /// ASTRING-CHAR = ATOM-CHAR / resp-specials /// ATOM-CHAR = /// CHAR = %x01-7F /// ; any 7-bit US-ASCII character, excluding NUL /// atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards / quoted-specials / resp-specials /// SP = %x20 /// CTL = %x00-1F / %x7F /// ; controls /// list-wildcards = "%" / "*" /// quoted-specials = DQUOTE / "\" /// DQUOTE = %x22 /// ; " (Double Quote) /// resp-specials = "]" /// ``` #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(PartialEq, Eq, Hash, Clone)] pub struct Tag<'a>(pub(crate) Cow<'a, str>); // We want a slightly more dense `Debug` implementation. impl<'a> Debug for Tag<'a> { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { write!(f, "Tag({:?})", self.0) } } impl<'a> Tag<'a> { pub fn validate(value: impl AsRef<[u8]>) -> Result<(), ValidationError> { let value = value.as_ref(); if value.is_empty() { return Err(ValidationError::new(ValidationErrorKind::Empty)); } if let Some(at) = value .iter() .position(|b| !is_astring_char(*b) || *b == b'+') { return Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: value[at], at, })); }; Ok(()) } pub fn inner(&self) -> &str { self.0.as_ref() } /// Constructs a tag without validation. /// /// # Warning: IMAP conformance /// /// The caller must ensure that `inner` is valid according to [`Self::validate`]. Failing to do /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows. /// Do not call this constructor with untrusted data. #[cfg(feature = "unvalidated")] #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))] pub fn unvalidated(inner: C) -> Self where C: Into>, { let inner = inner.into(); #[cfg(debug_assertions)] Self::validate(inner.as_bytes()).unwrap(); Self(inner) } } impl<'a> TryFrom<&'a [u8]> for Tag<'a> { type Error = ValidationError; fn try_from(value: &'a [u8]) -> Result { Self::validate(value)?; // Safety: `unwrap` can't fail due to `validate`. Ok(Self(Cow::Borrowed(from_utf8(value).unwrap()))) } } impl<'a> TryFrom> for Tag<'a> { type Error = ValidationError; fn try_from(value: Vec) -> Result { Self::validate(&value)?; // Safety: `unwrap` can't fail due to `validate`. Ok(Self(Cow::Owned(String::from_utf8(value).unwrap()))) } } impl<'a> TryFrom<&'a str> for Tag<'a> { type Error = ValidationError; fn try_from(value: &'a str) -> Result { Self::validate(value)?; Ok(Self(Cow::Borrowed(value))) } } impl<'a> TryFrom for Tag<'a> { type Error = ValidationError; fn try_from(value: String) -> Result { Self::validate(&value)?; Ok(Self(Cow::Owned(value))) } } impl<'a> AsRef for Tag<'a> { fn as_ref(&self) -> &str { self.0.as_ref() } } /// A human-readable text string used in some server responses. /// /// # Example /// /// ```imap /// S: * OK IMAP4rev1 server ready /// // ^^^^^^^^^^^^^^^^^^^^^^ /// // | /// // `Text` /// ``` /// /// # ABNF definition /// /// ```abnf /// text = 1*TEXT-CHAR /// TEXT-CHAR = /// CHAR = %x01-7F ; any 7-bit US-ASCII character, excluding NUL /// CR = %x0D ; carriage return /// LF = %x0A ; linefeed /// ``` #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(PartialEq, Eq, Hash, Clone)] pub struct Text<'a>(pub(crate) Cow<'a, str>); // We want a slightly more dense `Debug` implementation. impl<'a> Debug for Text<'a> { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { write!(f, "Text({:?})", self.0) } } impl<'a> Text<'a> { pub fn validate(value: impl AsRef<[u8]>) -> Result<(), ValidationError> { let value = value.as_ref(); if value.is_empty() { return Err(ValidationError::new(ValidationErrorKind::Empty)); } if let Some(at) = value.iter().position(|b| !is_text_char(*b)) { return Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: value[at], at, })); }; Ok(()) } pub fn inner(&self) -> &str { self.0.as_ref() } pub fn into_inner(self) -> Cow<'a, str> { self.0 } /// Constructs a text without validation. /// /// # Warning: IMAP conformance /// /// The caller must ensure that `inner` is valid according to [`Self::validate`]. Failing to do /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows. /// Do not call this constructor with untrusted data. #[cfg(feature = "unvalidated")] #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))] pub fn unvalidated(inner: C) -> Self where C: Into>, { let inner = inner.into(); #[cfg(debug_assertions)] Self::validate(inner.as_bytes()).unwrap(); Self(inner) } } impl<'a> TryFrom<&'a [u8]> for Text<'a> { type Error = ValidationError; fn try_from(value: &'a [u8]) -> Result { Self::validate(value)?; // Safety: `unwrap` can't panic due to `validate`. Ok(Self(Cow::Borrowed(from_utf8(value).unwrap()))) } } impl<'a> TryFrom> for Text<'a> { type Error = ValidationError; fn try_from(value: Vec) -> Result { Self::validate(&value)?; // Safety: `unwrap` can't panic due to `validate`. Ok(Self(Cow::Owned(String::from_utf8(value).unwrap()))) } } impl<'a> TryFrom<&'a str> for Text<'a> { type Error = ValidationError; fn try_from(value: &'a str) -> Result { Self::validate(value)?; Ok(Self(Cow::Borrowed(value))) } } impl<'a> TryFrom for Text<'a> { type Error = ValidationError; fn try_from(value: String) -> Result { Self::validate(&value)?; Ok(Self(Cow::Owned(value))) } } impl<'a> AsRef for Text<'a> { fn as_ref(&self) -> &str { self.0.as_ref() } } /// A quoted char. /// /// # ABNF definition /// /// ```abnf /// QUOTED-CHAR = / "\" quoted-specials /// TEXT-CHAR = /// CHAR = %x01-7F ; any 7-bit US-ASCII character, excluding NUL /// CR = %x0D ; carriage return /// LF = %x0A ; linefeed /// quoted-specials = DQUOTE / "\" /// DQUOTE = %x22 ; " (Double Quote) /// ``` #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Copy, Debug, PartialEq, Eq, Hash, Clone)] pub struct QuotedChar(char); impl QuotedChar { pub fn validate(input: char) -> Result<(), ValidationError> { if input.is_ascii() && (is_any_text_char_except_quoted_specials(input as u8) || input == '\\' || input == '"') { Ok(()) } else { Err(ValidationError::new(ValidationErrorKind::Invalid)) } } pub fn inner(&self) -> char { self.0 } /// Constructs a quoted char without validation. /// /// # Warning: IMAP conformance /// /// The caller must ensure that `inner` is valid according to [`Self::validate`]. Failing to do /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows. /// Do not call this constructor with untrusted data. #[cfg(feature = "unvalidated")] #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))] pub fn unvalidated(inner: char) -> Self { #[cfg(debug_assertions)] Self::validate(inner).unwrap(); Self(inner) } } impl TryFrom for QuotedChar { type Error = ValidationError; fn try_from(value: char) -> Result { Self::validate(value)?; Ok(QuotedChar(value)) } } /// A charset. /// /// # ABNF definition /// /// Note: IMAP is not very clear on what constitutes a charset string. We try to figure it out by /// looking at the `search` rule. (See [#266](https://github.com/duesee/imap-codec/issues/266).) /// /// ```abnf /// search = "SEARCH" [SP "CHARSET" SP astring] 1*(SP search-key) /// ; ^^^^^^^ /// ; | /// ; `Charset` // ; CHARSET argument to MUST be registered with IANA /// ``` /// /// So, it seems that it should be an `AString`. However the IMAP standard also points to ... /// ```abnf /// mime-charset = 1*mime-charset-chars /// mime-charset-chars = ALPHA / DIGIT / /// "!" / "#" / "$" / "%" / "&" / /// "'" / "+" / "-" / "^" / "_" / /// "`" / "{" / "}" / "~" /// ALPHA = "A".."Z" ; Case insensitive ASCII Letter /// DIGIT = "0".."9" ; Numeric digit /// ``` #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Charset<'a> { Atom(Atom<'a>), Quoted(Quoted<'a>), } impl<'a> From> for Charset<'a> { fn from(value: Atom<'a>) -> Self { Self::Atom(value) } } impl<'a> From> for Charset<'a> { fn from(value: Quoted<'a>) -> Self { Self::Quoted(value) } } impl<'a> TryFrom<&'a [u8]> for Charset<'a> { type Error = ValidationError; fn try_from(value: &'a [u8]) -> Result { if let Ok(atom) = Atom::try_from(value) { return Ok(Self::Atom(atom)); } Ok(Self::Quoted(Quoted::try_from(value)?)) } } impl<'a> TryFrom> for Charset<'a> { type Error = ValidationError; fn try_from(value: Vec) -> Result { // TODO(efficiency) if let Ok(atom) = Atom::try_from(value.clone()) { return Ok(Self::Atom(atom)); } Ok(Self::Quoted(Quoted::try_from(value)?)) } } impl<'a> TryFrom<&'a str> for Charset<'a> { type Error = ValidationError; fn try_from(value: &'a str) -> Result { if let Ok(atom) = Atom::try_from(value) { return Ok(Self::Atom(atom)); } Ok(Self::Quoted(Quoted::try_from(value)?)) } } impl<'a> TryFrom for Charset<'a> { type Error = ValidationError; fn try_from(value: String) -> Result { // TODO(efficiency) if let Ok(atom) = Atom::try_from(value.clone()) { return Ok(Self::Atom(atom)); } Ok(Self::Quoted(Quoted::try_from(value)?)) } } impl<'a> AsRef for Charset<'a> { fn as_ref(&self) -> &str { match self { Self::Atom(atom) => atom.as_ref(), Self::Quoted(quoted) => quoted.as_ref(), } } } /// A `Vec` that always contains >= 1 elements. /// /// Some messages in IMAP require a list of at least one element. We encoded these situations in a /// non-empty vector type to not produce invalid messages. /// /// The `Debug` implementation equals `Vec` with an attached `+` at the end. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, PartialEq, Eq, Hash)] pub struct NonEmptyVec(pub(crate) Vec); impl Debug for NonEmptyVec where T: Debug, { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { self.0.fmt(f)?; write!(f, "+") } } impl NonEmptyVec { pub fn validate(value: &[T]) -> Result<(), ValidationError> { if value.is_empty() { return Err(ValidationError::new(ValidationErrorKind::Empty)); } Ok(()) } /// Constructs a non-empty vector without validation. /// /// # Warning: IMAP conformance /// /// The caller must ensure that `inner` is valid according to [`Self::validate`]. Failing to do /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows. /// Do not call this constructor with untrusted data. #[cfg(feature = "unvalidated")] #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))] pub fn unvalidated(inner: Vec) -> Self { #[cfg(debug_assertions)] Self::validate(&inner).unwrap(); Self(inner) } pub fn into_inner(self) -> Vec { self.0 } } impl From for NonEmptyVec { fn from(value: T) -> Self { NonEmptyVec(vec![value]) } } impl TryFrom> for NonEmptyVec { type Error = ValidationError; fn try_from(inner: Vec) -> Result { Self::validate(&inner)?; Ok(Self(inner)) } } impl IntoIterator for NonEmptyVec { type Item = T; type IntoIter = IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl AsRef<[T]> for NonEmptyVec { fn as_ref(&self) -> &[T] { &self.0 } } #[cfg(test)] mod tests { use std::str::from_utf8; use super::*; #[test] fn test_conversion_atom() { #[allow(clippy::type_complexity)] let tests: Vec<( &[u8], (Result, Result), )> = vec![ ( b"A", ( Ok(Atom(Cow::Borrowed("A"))), Ok(Atom(Cow::Owned("A".into()))), ), ), ( b"ABC", ( Ok(Atom(Cow::Borrowed("ABC"))), Ok(Atom(Cow::Owned("ABC".into()))), ), ), ( b" A", ( Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: b' ', at: 0, })), Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: b' ', at: 0, })), ), ), ( b"A ", ( Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: b' ', at: 1, })), Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: b' ', at: 1, })), ), ), ( b"", ( Err(ValidationError::new(ValidationErrorKind::Empty)), Err(ValidationError::new(ValidationErrorKind::Empty)), ), ), ( b"A\x00", ( Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: 0x00, at: 1, })), Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: 0x00, at: 1, })), ), ), ( b"A\x00", ( Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: 0x00, at: 1, })), Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: 0x00, at: 1, })), ), ), ]; for (test, (expected, expected_owned)) in tests.into_iter() { let got = Atom::try_from(test); assert_eq!(expected, got); if let Ok(got) = got { assert_eq!(got.as_ref().as_bytes(), test); } let got = Atom::try_from(test.to_owned()); assert_eq!(expected_owned, got); if let Ok(got) = got { assert_eq!(got.as_ref().as_bytes(), test); } if let Ok(test_str) = from_utf8(test) { let got = Atom::try_from(test_str); assert_eq!(expected, got); if let Ok(got) = got { assert_eq!(got.as_ref().as_bytes(), test); } let got = Atom::try_from(test_str.to_owned()); assert_eq!(expected_owned, got); if let Ok(got) = got { assert_eq!(got.as_ref().as_bytes(), test); } } } } #[test] fn test_conversion_atom_ext() { #[allow(clippy::type_complexity)] let tests: Vec<( &[u8], ( Result, Result, ), )> = vec![ ( b"A", ( Ok(AtomExt(Cow::Borrowed("A"))), Ok(AtomExt(Cow::Owned("A".into()))), ), ), ( b"ABC", ( Ok(AtomExt(Cow::Borrowed("ABC"))), Ok(AtomExt(Cow::Owned("ABC".into()))), ), ), ( b"!partition/sda4", ( Ok(AtomExt(Cow::Borrowed("!partition/sda4"))), Ok(AtomExt(Cow::Owned("!partition/sda4".into()))), ), ), ( b" A", ( Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: b' ', at: 0, })), Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: b' ', at: 0, })), ), ), ( b"A ", ( Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: b' ', at: 1, })), Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: b' ', at: 1, })), ), ), ( b"", ( Err(ValidationError::new(ValidationErrorKind::Empty)), Err(ValidationError::new(ValidationErrorKind::Empty)), ), ), ( b"A\x00", ( Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: 0x00, at: 1, })), Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: 0x00, at: 1, })), ), ), ( b"\x00", ( Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: 0x00, at: 0, })), Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: 0x00, at: 0, })), ), ), ]; for (test, (expected, expected_owned)) in tests.into_iter() { let got = AtomExt::try_from(test); assert_eq!(expected, got); if let Ok(got) = got { assert_eq!(got.as_ref().as_bytes(), test); } let got = AtomExt::try_from(test.to_owned()); assert_eq!(expected_owned, got); if let Ok(got) = got { assert_eq!(got.as_ref().as_bytes(), test); } if let Ok(test_str) = from_utf8(test) { let got = AtomExt::try_from(test_str); assert_eq!(expected, got); if let Ok(got) = got { assert_eq!(got.as_ref().as_bytes(), test); } let got = AtomExt::try_from(test_str.to_owned()); assert_eq!(expected_owned, got); if let Ok(got) = got { assert_eq!(got.as_ref().as_bytes(), test); } } } } #[test] fn test_conversion_astring() { #[allow(clippy::type_complexity)] let tests: Vec<( &[u8], ( Result, Result, ), )> = vec![ ( b"A", ( Ok(AString::Atom(AtomExt(Cow::Borrowed("A")))), Ok(AString::Atom(AtomExt(Cow::Owned("A".into())))), ), ), ( b"ABC", ( Ok(AString::Atom(AtomExt(Cow::Borrowed("ABC")))), Ok(AString::Atom(AtomExt(Cow::Owned("ABC".into())))), ), ), ( b"", ( Ok(AString::String(IString::Quoted(Quoted(Cow::Borrowed(""))))), Ok(AString::String(IString::Quoted(Quoted(Cow::Owned( "".to_owned(), ))))), ), ), ( b" A", ( Ok(AString::String(IString::Quoted(Quoted(Cow::Borrowed( " A", ))))), Ok(AString::String(IString::Quoted(Quoted(Cow::Owned( " A".to_owned(), ))))), ), ), ( b"A ", ( Ok(AString::String(IString::Quoted(Quoted(Cow::Borrowed( "A ", ))))), Ok(AString::String(IString::Quoted(Quoted(Cow::Owned( "A ".to_owned(), ))))), ), ), ( b"\"", ( Ok(AString::String(IString::Quoted(Quoted(Cow::Borrowed( "\"", ))))), Ok(AString::String(IString::Quoted(Quoted(Cow::Owned( "\"".to_owned(), ))))), ), ), ( b"\\\"", ( Ok(AString::String(IString::Quoted(Quoted(Cow::Borrowed( "\\\"", ))))), Ok(AString::String(IString::Quoted(Quoted(Cow::Owned( "\\\"".to_owned(), ))))), ), ), ( b"A\x00", ( Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: 0, at: 1, })), Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: 0, at: 1, })), ), ), ( b"\x00", ( Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: 0, at: 0, })), Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: 0, at: 0, })), ), ), ]; for (test, (expected, expected_owned)) in tests.into_iter() { let got = AString::try_from(test); assert_eq!(expected, got); if let Ok(got) = got { assert_eq!(got.as_ref(), test); } let got = AString::try_from(test.to_owned()); assert_eq!(expected_owned, got); if let Ok(got) = got { assert_eq!(got.as_ref(), test); } if let Ok(test_str) = from_utf8(test) { let got = AString::try_from(test_str); assert_eq!(expected, got); if let Ok(got) = got { assert_eq!(got.as_ref(), test); } let got = AString::try_from(test_str.to_owned()); assert_eq!(expected_owned, got); if let Ok(got) = got { assert_eq!(got.as_ref(), test); } } } } #[test] fn test_conversion_istring() { assert_eq!( IString::try_from("AAA").unwrap(), IString::Quoted("AAA".try_into().unwrap()) ); assert_eq!( IString::try_from("\"AAA").unwrap(), IString::Quoted("\"AAA".try_into().unwrap()) ); assert_ne!( IString::try_from("\"AAA").unwrap(), IString::Quoted("\\\"AAA".try_into().unwrap()) ); } } imap-codec-1.0.0/imap-types/src/datetime.rs000066400000000000000000000200421447115025300205320ustar00rootroot00000000000000//! Date and time-related types. use std::fmt::{Debug, Formatter}; #[cfg(feature = "bounded-static")] use bounded_static::{IntoBoundedStatic, ToBoundedStatic}; use chrono::{Datelike, FixedOffset}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::datetime::error::{DateTimeError, NaiveDateError}; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, Eq, PartialEq, Hash)] pub struct DateTime(chrono::DateTime); impl DateTime { pub fn validate(value: &chrono::DateTime) -> Result<(), DateTimeError> { // Only a subset of `chrono`s `DateTime` is valid in IMAP. if !(0..=9999).contains(&value.year()) { return Err(DateTimeError::YearOutOfRange { got: value.year() }); } if value.timestamp_subsec_nanos() != 0 { return Err(DateTimeError::UnalignedNanoSeconds { got: value.timestamp_subsec_nanos(), }); } if value.offset().local_minus_utc() % 60 != 0 { return Err(DateTimeError::UnalignedOffset { got: value.offset().local_minus_utc() % 60, }); } Ok(()) } /// Constructs a date time without validation. /// /// # Warning: IMAP conformance /// /// The caller must ensure that `value` is valid according to [`Self::validate`]. Failing to do /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows. /// Do not call this constructor with untrusted data. #[cfg(feature = "unvalidated")] #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))] pub fn unvalidated(value: chrono::DateTime) -> Self { Self(value) } } impl TryFrom> for DateTime { type Error = DateTimeError; fn try_from(value: chrono::DateTime) -> Result { Self::validate(&value)?; Ok(Self(value)) } } impl Debug for DateTime { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Debug::fmt(&self.0, f) } } impl AsRef> for DateTime { fn as_ref(&self) -> &chrono::DateTime { &self.0 } } #[cfg(feature = "bounded-static")] impl IntoBoundedStatic for DateTime { type Static = Self; fn into_static(self) -> Self::Static { self } } #[cfg(feature = "bounded-static")] impl ToBoundedStatic for DateTime { type Static = Self; fn to_static(&self) -> Self::Static { self.clone() } } #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, Eq, PartialEq, Hash)] pub struct NaiveDate(chrono::NaiveDate); impl NaiveDate { pub fn validate(value: &chrono::NaiveDate) -> Result<(), NaiveDateError> { // Only a subset of `chrono`s `NaiveDate` is valid in IMAP. if !(0..=9999).contains(&value.year()) { return Err(NaiveDateError::YearOutOfRange { got: value.year() }); } Ok(()) } /// Constructs a naive date without validation. /// /// # Warning: IMAP conformance /// /// The caller must ensure that `value` is valid according to [`Self::validate`]. Failing to do /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows. /// Do not call this constructor with untrusted data. #[cfg(feature = "unvalidated")] #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))] pub fn unvalidated(value: chrono::NaiveDate) -> Self { Self(value) } } impl TryFrom for NaiveDate { type Error = NaiveDateError; fn try_from(value: chrono::NaiveDate) -> Result { Self::validate(&value)?; Ok(Self(value)) } } impl Debug for NaiveDate { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { Debug::fmt(&self.0, f) } } impl AsRef for NaiveDate { fn as_ref(&self) -> &chrono::NaiveDate { &self.0 } } #[cfg(feature = "bounded-static")] impl IntoBoundedStatic for NaiveDate { type Static = Self; fn into_static(self) -> Self::Static { self } } #[cfg(feature = "bounded-static")] impl ToBoundedStatic for NaiveDate { type Static = Self; fn to_static(&self) -> Self::Static { self.clone() } } /// Error-related types. pub mod error { use thiserror::Error; #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub enum DateTimeError { #[error("expected `0 <= year <= 9999`, got {got}")] YearOutOfRange { got: i32 }, #[error("expected `nanos == 0`, got {got}")] UnalignedNanoSeconds { got: u32 }, #[error("expected `offset % 60 == 0`, got {got}")] UnalignedOffset { got: i32 }, } #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub enum NaiveDateError { #[error("expected `0 <= year <= 9999`, got {got}")] YearOutOfRange { got: i32 }, } } #[cfg(test)] mod tests { use chrono::{TimeZone, Timelike}; use super::*; #[test] fn test_conversion_date_time_failing() { let tests = [ ( DateTime::try_from( chrono::FixedOffset::east_opt(3600) .unwrap() .from_local_datetime(&chrono::NaiveDateTime::new( chrono::NaiveDate::from_ymd_opt(-1, 2, 1).unwrap(), chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(), )) .unwrap(), ), DateTimeError::YearOutOfRange { got: -1 }, ), ( DateTime::try_from( chrono::FixedOffset::east_opt(3600) .unwrap() .from_local_datetime(&chrono::NaiveDateTime::new( chrono::NaiveDate::from_ymd_opt(10000, 2, 1).unwrap(), chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(), )) .unwrap(), ), DateTimeError::YearOutOfRange { got: 10000 }, ), ( DateTime::try_from( chrono::FixedOffset::east_opt(1) .unwrap() .from_local_datetime(&chrono::NaiveDateTime::new( chrono::NaiveDate::from_ymd_opt(0, 2, 1).unwrap(), chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(), )) .unwrap(), ), DateTimeError::UnalignedOffset { got: 1 }, ), ( DateTime::try_from( chrono::FixedOffset::east_opt(59) .unwrap() .from_local_datetime(&chrono::NaiveDateTime::new( chrono::NaiveDate::from_ymd_opt(9999, 2, 1).unwrap(), chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(), )) .unwrap(), ), DateTimeError::UnalignedOffset { got: 59 }, ), ( DateTime::try_from( chrono::FixedOffset::east_opt(60) .unwrap() .from_local_datetime(&chrono::NaiveDateTime::new( chrono::NaiveDate::from_ymd_opt(0, 2, 1).unwrap(), chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(), )) .unwrap() .with_nanosecond(1) .unwrap(), ), DateTimeError::UnalignedNanoSeconds { got: 1 }, ), ]; for (got, expected) in tests { println!("{}", got.clone().unwrap_err()); println!("{:?}", got.clone().unwrap_err()); assert_eq!(expected, got.unwrap_err()); } } } imap-codec-1.0.0/imap-types/src/envelope.rs000066400000000000000000000032521447115025300205570ustar00rootroot00000000000000//! Envelope-related types. #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::core::NString; #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Envelope<'a> { pub date: NString<'a>, pub subject: NString<'a>, pub from: Vec>, pub sender: Vec>, pub reply_to: Vec>, pub to: Vec>, pub cc: Vec>, pub bcc: Vec>, pub in_reply_to: NString<'a>, pub message_id: NString<'a>, } /// An address structure describes an electronic mail address. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] /// TODO(misuse): /// /// Here are many invariants ... /// /// mailbox: /// NIL indicates end of [RFC-2822] group; /// if non-NIL and host is NIL, holds [RFC-2822] group name. /// Otherwise, holds [RFC-2822] local-part after removing [RFC-2822] quoting /// /// host: /// NIL indicates [RFC-2822] group syntax. /// Otherwise, holds [RFC-2822] domain name pub struct Address<'a> { /// Personal name pub name: NString<'a>, /// At-domain-list (source route) pub adl: NString<'a>, /// Mailbox name pub mailbox: NString<'a>, /// Host name pub host: NString<'a>, } imap-codec-1.0.0/imap-types/src/error.rs000066400000000000000000000016051447115025300200730ustar00rootroot00000000000000//! Error-related types. use std::fmt::{Display, Formatter}; use thiserror::Error; /// A validation error. /// /// This error can be returned during validation of a value, e.g., a tag, atom, etc. #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub struct ValidationError { kind: ValidationErrorKind, } impl Display for ValidationError { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { write!(f, "Validation failed: {}", self.kind) } } #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub(crate) enum ValidationErrorKind { #[error("Must not be empty")] Empty, #[error("Invalid value")] Invalid, #[error("Invalid byte b'\\x{byte:02x}' at index {at}")] InvalidByteAt { byte: u8, at: usize }, } impl ValidationError { pub(crate) fn new(kind: ValidationErrorKind) -> Self { Self { kind } } } imap-codec-1.0.0/imap-types/src/extensions.rs000066400000000000000000000001671447115025300211430ustar00rootroot00000000000000//! IMAP extensions. pub mod compress; pub mod enable; pub mod idle; pub mod r#move; pub mod quota; pub mod unselect; imap-codec-1.0.0/imap-types/src/extensions/000077500000000000000000000000001447115025300205715ustar00rootroot00000000000000imap-codec-1.0.0/imap-types/src/extensions/compress.rs000066400000000000000000000101541447115025300227730ustar00rootroot00000000000000//! The IMAP COMPRESS Extension //! //! This extension defines a new type ... //! //! * [`CompressionAlgorithm`](crate::extensions::compress::CompressionAlgorithm) //! //! ... and extends ... //! //! * the [`Capability`](crate::response::Capability) enum with a new variant [`Capability::Compress`](crate::response::Capability#variant.Compress), //! * the [`Command`](crate::command::Command) enum with a new variant [`Command::Compress`](crate::command::Command#variant.Compress), and //! * the [`Code`](crate::response::Code) enum with a new variant [`Code::CompressionActive`](crate::response::Code#variant.CompressionActive). use std::fmt::{Display, Formatter}; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{ command::CommandBody, core::Atom, error::{ValidationError, ValidationErrorKind}, }; impl<'a> CommandBody<'a> { pub fn compress(algorithm: CompressionAlgorithm) -> Self { CommandBody::Compress { algorithm } } } #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum CompressionAlgorithm { Deflate, } impl Display for CompressionAlgorithm { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Self::Deflate => "DEFLATE", }) } } impl<'a> TryFrom<&'a str> for CompressionAlgorithm { type Error = ValidationError; fn try_from(value: &str) -> Result { match value.to_ascii_lowercase().as_ref() { "deflate" => Ok(Self::Deflate), _ => Err(ValidationError::new(ValidationErrorKind::Invalid)), } } } impl<'a> TryFrom<&'a [u8]> for CompressionAlgorithm { type Error = ValidationError; fn try_from(value: &[u8]) -> Result { match value.to_ascii_lowercase().as_slice() { b"deflate" => Ok(Self::Deflate), _ => Err(ValidationError::new(ValidationErrorKind::Invalid)), } } } impl<'a> TryFrom> for CompressionAlgorithm { type Error = ValidationError; fn try_from(atom: Atom<'a>) -> Result { match atom.as_ref().to_ascii_lowercase().as_ref() { "deflate" => Ok(Self::Deflate), _ => Err(ValidationError::new(ValidationErrorKind::Invalid)), } } } impl AsRef for CompressionAlgorithm { fn as_ref(&self) -> &str { match self { CompressionAlgorithm::Deflate => "DEFLATE", } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_conversion() { let tests = [(CompressionAlgorithm::Deflate, "DEFLATE")]; for (object, string) in tests { // Create from `&[u8]`. let got = CompressionAlgorithm::try_from(string.as_bytes()).unwrap(); assert_eq!(object, got); // Create from `&str`. let got = CompressionAlgorithm::try_from(string).unwrap(); assert_eq!(object, got); // Create from `Atom`. let got = CompressionAlgorithm::try_from(Atom::try_from(string).unwrap()).unwrap(); assert_eq!(object, got); // AsRef let encoded = object.as_ref(); assert_eq!(encoded, string); } } #[test] fn test_conversion_failing() { let tests = [ "", "D", "DE", "DEF", "DEFL", "DEFLA", "DEFLAT", "DEFLATX", "DEFLATEX", "XDEFLATE", ]; for string in tests { // Create from `&[u8]`. assert!(CompressionAlgorithm::try_from(string.as_bytes()).is_err()); // Create from `&str`. assert!(CompressionAlgorithm::try_from(string).is_err()); if !string.is_empty() { // Create from `Atom`. assert!(CompressionAlgorithm::try_from(Atom::try_from(string).unwrap()).is_err()); } } } } imap-codec-1.0.0/imap-types/src/extensions/enable.rs000066400000000000000000000100251447115025300223630ustar00rootroot00000000000000//! The IMAP ENABLE Extension //! //! This extension extends ... //! //! * the [Capability](crate::response::Capability) enum with a new variant [Capability::Enable](crate::response::Capability#variant.Enable), //! * the [CommandBody](crate::command::CommandBody) enum with a new variant [CommandBody::Enable](crate::command::CommandBody#variant.Enable), and //! * the [Data](crate::response::Data) enum with a new variant [Data::Enabled](crate::response::Data#variant.Enabled). use std::fmt::{Display, Formatter}; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{ command::CommandBody, core::{Atom, NonEmptyVec}, error::ValidationError, }; impl<'a> CommandBody<'a> { pub fn enable(capabilities: C) -> Result where C: TryInto>>, { Ok(CommandBody::Enable { capabilities: capabilities.try_into()?, }) } } #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum CapabilityEnable<'a> { Utf8(Utf8Kind), #[cfg(feature = "ext_condstore_qresync")] #[cfg_attr(docsrs, doc(cfg(feature = "ext_condstore_qresync")))] CondStore, Other(CapabilityEnableOther<'a>), } impl<'a> TryFrom<&'a str> for CapabilityEnable<'a> { type Error = ValidationError; fn try_from(value: &'a str) -> Result { Ok(Self::from(Atom::try_from(value)?)) } } impl<'a> From> for CapabilityEnable<'a> { fn from(atom: Atom<'a>) -> Self { match atom.as_ref().to_ascii_lowercase().as_ref() { "utf8=accept" => Self::Utf8(Utf8Kind::Accept), "utf8=only" => Self::Utf8(Utf8Kind::Only), #[cfg(feature = "ext_condstore_qresync")] "condstore" => Self::CondStore, _ => Self::Other(CapabilityEnableOther(atom)), } } } impl<'a> Display for CapabilityEnable<'a> { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { match self { Self::Utf8(kind) => write!(f, "UTF8={}", kind), #[cfg(feature = "ext_condstore_qresync")] Self::CondStore => write!(f, "CONDSTORE"), Self::Other(other) => write!(f, "{}", other.0), } } } /// An (unknown) capability. /// /// It's guaranteed that this type can't represent any capability from [`CapabilityEnable`]. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct CapabilityEnableOther<'a>(Atom<'a>); #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum Utf8Kind { Accept, Only, } impl Display for Utf8Kind { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { f.write_str(match self { Self::Accept => "ACCEPT", Self::Only => "ONLY", }) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_conversion_capability_enable() { assert_eq!( CapabilityEnable::from(Atom::try_from("utf8=only").unwrap()), CapabilityEnable::Utf8(Utf8Kind::Only) ); assert_eq!( CapabilityEnable::from(Atom::try_from("utf8=accept").unwrap()), CapabilityEnable::Utf8(Utf8Kind::Accept) ); assert_eq!( CapabilityEnable::try_from("utf").unwrap(), CapabilityEnable::Other(CapabilityEnableOther(Atom::try_from("utf").unwrap())) ); assert_eq!( CapabilityEnable::try_from("xxxxx").unwrap(), CapabilityEnable::Other(CapabilityEnableOther(Atom::try_from("xxxxx").unwrap())) ); } } imap-codec-1.0.0/imap-types/src/extensions/idle.rs000066400000000000000000000020351447115025300220540ustar00rootroot00000000000000//! IMAP4 IDLE command //! //! This extension adds a new method ... //! //! * [`CommandBody::idle()`](crate::command::CommandBody#method.idle) //! //! ... adds a new type ... //! //! * [`IdleDone`](crate::extensions::idle::IdleDone) //! //! ... and extends ... //! //! * [`CommandBody`](crate::command::CommandBody) enum with a new variant [`CommandBody::Idle`](crate::command::CommandBody#variant.Idle), and //! * [`Capability`](crate::response::Capability) enum with a new variant [`Capability::Idle`](crate::response::Capability#variant.Idle). #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; /// Denotes the continuation data message "DONE\r\n" to end the IDLE command. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct IdleDone; imap-codec-1.0.0/imap-types/src/extensions/move.rs000066400000000000000000000016371447115025300221140ustar00rootroot00000000000000//! IMAP - MOVE Extension use crate::{ command::CommandBody, extensions::r#move::error::MoveError, mailbox::Mailbox, sequence::SequenceSet, }; impl<'a> CommandBody<'a> { pub fn r#move( sequence_set: S, mailbox: M, uid: bool, ) -> Result> where S: TryInto, M: TryInto>, { Ok(CommandBody::Move { sequence_set: sequence_set.try_into().map_err(MoveError::Sequence)?, mailbox: mailbox.try_into().map_err(MoveError::Mailbox)?, uid, }) } } /// Error-related types. pub mod error { use thiserror::Error; #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub enum MoveError { #[error("Invalid sequence: {0}")] Sequence(S), #[error("Invalid mailbox: {0}")] Mailbox(M), } } imap-codec-1.0.0/imap-types/src/extensions/quota.rs000066400000000000000000000233471447115025300223010ustar00rootroot00000000000000//! The IMAP QUOTA Extension //! //! This extends ... //! //! * [`Capability`](crate::response::Capability) with new variants: //! //! - [`Capability::Quota`](crate::response::Capability::Quota) //! - [`Capability::QuotaRes`](crate::response::Capability::QuotaRes) //! - [`Capability::QuotaSet`](crate::response::Capability::QuotaSet) //! //! * [`CommandBod`y](crate::command::CommandBody) with new variants: //! //! - [`Command::GetQuota`](crate::command::CommandBody::GetQuota) //! - [`Command::GetQuotaRoot`](crate::command::CommandBody::GetQuotaRoot) //! - [`Command::SetQuota`](crate::command::CommandBody::SetQuota) //! //! * [`Data`](crate::response::Data) with new variants: //! //! - [`Data::Quota`](crate::response::Data::Quota) //! - [`Data::QuotaRoot`](crate::response::Data::QuotaRoot) //! //! * [`Code`](crate::response::Code) with a new variant: //! //! - [`Code::OverQuota`](crate::response::Code::OverQuota) //! //! * [`StatusDataItemName`](crate::status::StatusDataItemName) with new variants: //! //! - [`StatusDataItemName::Deleted`](crate::status::StatusDataItemName::Deleted) //! - [`StatusDataItemName::DeletedStorage`](crate::status::StatusDataItemName::DeletedStorage) //! //! * [`StatusDataItem`](crate::status::StatusDataItem) with new variants: //! //! - [`StatusDataItem::Deleted`](crate::status::StatusDataItem::Deleted) //! - [`StatusDataItem::DeletedStorage`](crate::status::StatusDataItem::DeletedStorage) use std::{ borrow::Cow, fmt::{Display, Formatter}, }; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{ command::CommandBody, core::{impl_try_from, AString, Atom, NonEmptyVec}, extensions::quota::error::{QuotaError, QuotaRootError, SetQuotaError}, mailbox::Mailbox, response::Data, }; impl<'a> CommandBody<'a> { pub fn get_quota(root: A) -> Result where A: TryInto>, { Ok(CommandBody::GetQuota { root: root.try_into()?, }) } pub fn get_quota_root(mailbox: M) -> Result where M: TryInto>, { Ok(CommandBody::GetQuotaRoot { mailbox: mailbox.try_into()?, }) } pub fn set_quota(root: R, quotas: S) -> Result> where R: TryInto>, S: TryInto>>, { Ok(CommandBody::SetQuota { root: root.try_into().map_err(SetQuotaError::Root)?, quotas: quotas.try_into().map_err(SetQuotaError::QuotaSet)?, }) } } impl<'a> Data<'a> { pub fn quota(root: R, quotas: Q) -> Result> where R: TryInto>, Q: TryInto>>, { Ok(Self::Quota { root: root.try_into().map_err(QuotaError::Root)?, quotas: quotas.try_into().map_err(QuotaError::Quotas)?, }) } pub fn quota_root( mailbox: M, roots: R, ) -> Result> where M: TryInto>, R: TryInto>>, { Ok(Self::QuotaRoot { mailbox: mailbox.try_into().map_err(QuotaRootError::Mailbox)?, roots: roots.try_into().map_err(QuotaRootError::Roots)?, }) } } /// A resource type for use in IMAP's QUOTA extension. /// /// Supported resource names MUST be advertised as a capability by prepending the resource name with "QUOTA=RES-". #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Resource<'a> { /// The physical space estimate, in units of 1024 octets, of the mailboxes governed by the quota /// root. /// /// This MAY not be the same as the sum of the RFC822.SIZE of the messages. Some implementations /// MAY include metadata sizes for the messages and mailboxes, and other implementations MAY /// store messages in such a way that the physical space used is smaller, for example, due to /// use of compression. Additional messages might not increase the usage. Clients MUST NOT use /// the usage figure for anything other than informational purposes; for example, they MUST NOT /// refuse to APPEND a message if the limit less the usage is smaller than the RFC822.SIZE /// divided by 1024 octets of the message, but it MAY warn about such condition. The usage /// figure may change as a result of performing actions not associated with adding new messages /// to the mailbox, such as SEARCH, since this may increase the amount of metadata included in /// the calculations. /// /// When the server supports this resource type, it MUST also support the DELETED-STORAGE status /// data item. /// /// Support for this resource MUST be indicated by the server by advertising the /// "QUOTA=RES-STORAGE" capability. Storage, /// The number of messages stored within the mailboxes governed by the quota root. /// /// This MUST be an exact number; however, clients MUST NOT assume that a change in the usage /// indicates a change in the number of messages available, since the quota root may include /// mailboxes the client has no access to. /// /// When the server supports this resource type, it MUST also support the DELETED status data /// item. /// /// Support for this resource MUST be indicated by the server by advertising the /// "QUOTA=RES-MESSAGE" capability. Message, /// The number of mailboxes governed by the quota root. /// /// This MUST be an exact number; however, clients MUST NOT assume that a change in the usage /// indicates a change in the number of mailboxes, since the quota root may include mailboxes /// the client has no access to. /// /// Support for this resource MUST be indicated by the server by advertising the /// "QUOTA=RES-MAILBOX" capability. Mailbox, /// The maximum size of all annotations \[RFC5257\], in units of 1024 octets, associated with all /// messages in the mailboxes governed by the quota root. /// /// Support for this resource MUST be indicated by the server by advertising the /// "QUOTA=RES-ANNOTATION-STORAGE" capability. AnnotationStorage, /// An (unknown) resource. Other(ResourceOther<'a>), } /// An (unknown) resource. /// /// It's guaranteed that this type can't represent any resource from [`Resource`]. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ResourceOther<'a>(Atom<'a>); impl_try_from!(Atom<'a>, 'a, &'a [u8], Resource<'a>); impl_try_from!(Atom<'a>, 'a, Vec, Resource<'a>); impl_try_from!(Atom<'a>, 'a, &'a str, Resource<'a>); impl_try_from!(Atom<'a>, 'a, String, Resource<'a>); impl_try_from!(Atom<'a>, 'a, Cow<'a, str>, Resource<'a>); impl<'a> From> for Resource<'a> { fn from(atom: Atom<'a>) -> Self { match atom.inner().to_ascii_uppercase().as_ref() { "STORAGE" => Resource::Storage, "MESSAGE" => Resource::Message, "MAILBOX" => Resource::Mailbox, "ANNOTATION-STORAGE" => Resource::AnnotationStorage, _ => Resource::Other(ResourceOther(atom)), } } } impl<'a> Display for Resource<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Self::Storage => "STORAGE", Self::Message => "MESSAGE", Self::Mailbox => "MAILBOX", Self::AnnotationStorage => "ANNOTATION-STORAGE", Self::Other(other) => other.0.as_ref(), }) } } /// A type that holds a resource name, usage, and limit. /// Used in the response of the GETQUOTA command. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct QuotaGet<'a> { pub resource: Resource<'a>, pub usage: u64, pub limit: u64, } impl<'a> QuotaGet<'a> { pub fn new(resource: Resource<'a>, usage: u64, limit: u64) -> Self { Self { resource, usage, limit, } } } /// A type that holds a resource name and limit. /// Used in the SETQUOTA command. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct QuotaSet<'a> { pub resource: Resource<'a>, pub limit: u64, } impl<'a> QuotaSet<'a> { pub fn new(resource: Resource<'a>, limit: u64) -> Self { Self { resource, limit } } } /// Error-related types. pub mod error { use thiserror::Error; #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub enum QuotaError { #[error("Invalid root: {0}")] Root(R), #[error("Invalid quotas: {0}")] Quotas(Q), } #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub enum QuotaRootError { #[error("Invalid mailbox: {0}")] Mailbox(M), #[error("Invalid roots: {0}")] Roots(R), } #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub enum SetQuotaError { #[error("Invalid root: {0}")] Root(R), #[error("Invalid quota set: {0}")] QuotaSet(S), } } imap-codec-1.0.0/imap-types/src/extensions/unselect.rs000066400000000000000000000002361447115025300227620ustar00rootroot00000000000000//! The IMAP UNSELECT command use crate::command::CommandBody; impl CommandBody<'_> { pub fn unselect() -> Self { CommandBody::Unselect } } imap-codec-1.0.0/imap-types/src/fetch.rs000066400000000000000000000364621447115025300200440ustar00rootroot00000000000000//! Fetch-related types. use std::{ fmt::{Display, Formatter}, num::NonZeroU32, }; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{ body::BodyStructure, core::{AString, NString, NonEmptyVec}, datetime::DateTime, envelope::Envelope, flag::FlagFetch, }; /// Shorthands for commonly-used message data items. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum Macro { /// Shorthand for `(FLAGS INTERNALDATE RFC822.SIZE)`. Fast, /// Shorthand for `(FLAGS INTERNALDATE RFC822.SIZE ENVELOPE)`. All, /// Shorthand for `(FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY)`. Full, } impl Macro { pub fn expand(&self) -> Vec { use MessageDataItemName::*; match self { Self::All => vec![Flags, InternalDate, Rfc822Size, Envelope], Self::Fast => vec![Flags, InternalDate, Rfc822Size], Self::Full => vec![Flags, InternalDate, Rfc822Size, Envelope, Body], } } } impl Display for Macro { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Macro::All => "ALL", Macro::Fast => "FAST", Macro::Full => "FULL", }) } } /// Either a macro or a list of message data items. /// /// A macro must be used by itself, and not in conjunction with other macros or data items. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum MacroOrMessageDataItemNames<'a> { Macro(Macro), MessageDataItemNames(Vec>), } impl<'a> From for MacroOrMessageDataItemNames<'a> { fn from(m: Macro) -> Self { MacroOrMessageDataItemNames::Macro(m) } } impl<'a> From>> for MacroOrMessageDataItemNames<'a> { fn from(item_names: Vec>) -> Self { MacroOrMessageDataItemNames::MessageDataItemNames(item_names) } } /// Message data item name used to request a message data item. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[doc(alias = "FetchAttribute")] pub enum MessageDataItemName<'a> { /// Non-extensible form of `BODYSTRUCTURE`. /// /// ```imap /// BODY /// ``` Body, /// The text of a particular body section. /// /// ```imap /// BODY[
]<> /// ``` BodyExt { /// The section specification is a set of zero or more part specifiers delimited by periods. /// /// An empty section specification refers to the entire message, including the header. /// /// See [`crate::fetch::Section`] and [`crate::fetch::PartSpecifier`]. /// /// Every message has at least one part number. Non-[MIME-IMB] /// messages, and non-multipart [MIME-IMB] messages with no /// encapsulated message, only have a part 1. /// /// Multipart messages are assigned consecutive part numbers, as /// they occur in the message. If a particular part is of type /// message or multipart, its parts MUST be indicated by a period /// followed by the part number within that nested multipart part. /// /// A part of type MESSAGE/RFC822 also has nested part numbers, /// referring to parts of the MESSAGE part's body. section: Option>, /// It is possible to fetch a substring of the designated text. /// This is done by appending an open angle bracket ("<"), the /// octet position of the first desired octet, a period, the /// maximum number of octets desired, and a close angle bracket /// (">") to the part specifier. If the starting octet is beyond /// the end of the text, an empty string is returned. /// /// Any partial fetch that attempts to read beyond the end of the /// text is truncated as appropriate. A partial fetch that starts /// at octet 0 is returned as a partial fetch, even if this /// truncation happened. /// /// Note: This means that BODY[]<0.2048> of a 1500-octet message /// will return BODY[]<0> with a literal of size 1500, not /// BODY[]. /// /// Note: A substring fetch of a HEADER.FIELDS or /// HEADER.FIELDS.NOT part specifier is calculated after /// subsetting the header. partial: Option<(u32, NonZeroU32)>, /// Defines, wheather BODY or BODY.PEEK should be used. /// /// `BODY[...]` implicitly sets the `\Seen` flag where `BODY.PEEK[...]` does not. peek: bool, }, /// The [MIME-IMB] body structure of a message. /// /// This is computed by the server by parsing the [MIME-IMB] header fields in the [RFC-2822] /// header and [MIME-IMB] headers. /// /// ```imap /// BODYSTRUCTURE /// ``` BodyStructure, /// The envelope structure of a message. /// /// This is computed by the server by parsing the [RFC-2822] header into the component parts, /// defaulting various fields as necessary. /// /// ```imap /// ENVELOPE /// ``` Envelope, /// The flags that are set for a message. /// /// ```imap /// FLAGS /// ``` Flags, /// The internal date of a message. /// /// ```imap /// INTERNALDATE /// ``` InternalDate, /// Functionally equivalent to `BODY[]`. /// /// Differs in the syntax of the resulting untagged FETCH data (`RFC822` is returned). /// /// ```imap /// RFC822 /// ``` /// /// Note: `BODY[]` is constructed as ... /// /// ```rust /// # use imap_types::fetch::MessageDataItemName; /// MessageDataItemName::BodyExt { /// section: None, /// partial: None, /// peek: false, /// }; /// ``` Rfc822, /// Functionally equivalent to `BODY.PEEK[HEADER]`. /// /// Differs in the syntax of the resulting untagged FETCH data (`RFC822.HEADER` is returned). /// /// ```imap /// RFC822.HEADER /// ``` Rfc822Header, /// The [RFC-2822] size of a message. /// /// ```imap /// RFC822.SIZE /// ``` Rfc822Size, /// Functionally equivalent to `BODY[TEXT]`. /// /// Differs in the syntax of the resulting untagged FETCH data (`RFC822.TEXT` is returned). /// ```imap /// RFC822.TEXT /// ``` Rfc822Text, /// The unique identifier for a message. /// /// ```imap /// UID /// ``` Uid, } /// Message data item. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[doc(alias = "FetchAttributeValue")] pub enum MessageDataItem<'a> { /// A form of `BODYSTRUCTURE` without extension data. /// /// ```imap /// BODY /// ``` Body(BodyStructure<'a>), /// The body contents of the specified section. /// /// 8-bit textual data is permitted if a \[CHARSET\] identifier is /// part of the body parameter parenthesized list for this section. /// Note that headers (part specifiers HEADER or MIME, or the /// header portion of a MESSAGE/RFC822 part), MUST be 7-bit; 8-bit /// characters are not permitted in headers. Note also that the /// [RFC-2822] delimiting blank line between the header and the /// body is not affected by header line subsetting; the blank line /// is always included as part of header data, except in the case /// of a message which has no body and no blank line. /// /// Non-textual data such as binary data MUST be transfer encoded /// into a textual form, such as BASE64, prior to being sent to the /// client. To derive the original binary data, the client MUST /// decode the transfer encoded string. /// /// ```imap /// BODY[
]<> /// ``` BodyExt { /// The specified section. section: Option>, /// If the origin octet is specified, this string is a substring of /// the entire body contents, starting at that origin octet. This /// means that `BODY[]<0>` MAY be truncated, but `BODY[]` is NEVER /// truncated. /// /// Note: The origin octet facility MUST NOT be used by a server /// in a FETCH response unless the client specifically requested /// it by means of a FETCH of a `BODY[
]<>` data /// item. origin: Option, /// The string SHOULD be interpreted by the client according to the /// content transfer encoding, body type, and subtype. data: NString<'a>, }, /// The [MIME-IMB] body structure of a message. /// /// This is computed by the server by parsing the [MIME-IMB] header fields, defaulting various /// fields as necessary. /// /// ```imap /// BODYSTRUCTURE /// ``` BodyStructure(BodyStructure<'a>), /// The envelope structure of a message. /// /// This is computed by the server by parsing the [RFC-2822] header into the component parts, /// defaulting various fields as necessary. /// /// ```imap /// ENVELOPE /// ``` Envelope(Envelope<'a>), /// A list of flags that are set for a message. /// /// ```imap /// FLAGS /// ``` Flags(Vec>), /// A string representing the internal date of a message. /// /// ```imap /// INTERNALDATE /// ``` InternalDate(DateTime), /// Equivalent to `BODY[]`. /// /// ```imap /// RFC822 /// ``` Rfc822(NString<'a>), /// Equivalent to `BODY[HEADER]`. /// /// Note that this did not result in `\Seen` being set, because `RFC822.HEADER` response data /// occurs as a result of a `FETCH` of `RFC822.HEADER`. `BODY[HEADER]` response data occurs as a /// result of a `FETCH` of `BODY[HEADER]` (which sets `\Seen`) or `BODY.PEEK[HEADER]` (which /// does not set `\Seen`). /// /// ```imap /// RFC822.HEADER /// ``` Rfc822Header(NString<'a>), /// A number expressing the [RFC-2822] size of a message. /// /// ```imap /// RFC822.SIZE /// ``` Rfc822Size(u32), /// Equivalent to `BODY[TEXT]`. /// /// ```imap /// RFC822.TEXT /// ``` Rfc822Text(NString<'a>), /// A number expressing the unique identifier of a message. /// /// ```imap /// UID /// ``` Uid(NonZeroU32), } /// A part specifier is either a part number or one of the following: /// `HEADER`, `HEADER.FIELDS`, `HEADER.FIELDS.NOT`, `MIME`, and `TEXT`. /// /// The HEADER, HEADER.FIELDS, and HEADER.FIELDS.NOT part /// specifiers refer to the [RFC-2822] header of the message or of /// an encapsulated [MIME-IMT] MESSAGE/RFC822 message. /// HEADER.FIELDS and HEADER.FIELDS.NOT are followed by a list of /// field-name (as defined in [RFC-2822]) names, and return a /// subset of the header. /// /// The field-matching is case-insensitive but otherwise exact. /// Subsetting does not exclude the [RFC-2822] delimiting blank line between the header /// and the body; the blank line is included in all header fetches, /// except in the case of a message which has no body and no blank /// line. /// /// The HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, and TEXT part /// specifiers can be the sole part specifier or can be prefixed by /// one or more numeric part specifiers, provided that the numeric /// part specifier refers to a part of type MESSAGE/RFC822. /// /// Here is an example of a complex message with some of its part specifiers: /// /// ```text /// HEADER ([RFC-2822] header of the message) /// TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED /// 1 TEXT/PLAIN /// 2 APPLICATION/OCTET-STREAM /// 3 MESSAGE/RFC822 /// 3.HEADER ([RFC-2822] header of the message) /// 3.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED /// 3.1 TEXT/PLAIN /// 3.2 APPLICATION/OCTET-STREAM /// 4 MULTIPART/MIXED /// 4.1 IMAGE/GIF /// 4.1.MIME ([MIME-IMB] header for the IMAGE/GIF) /// 4.2 MESSAGE/RFC822 /// 4.2.HEADER ([RFC-2822] header of the message) /// 4.2.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED /// 4.2.1 TEXT/PLAIN /// 4.2.2 MULTIPART/ALTERNATIVE /// 4.2.2.1 TEXT/PLAIN /// 4.2.2.2 TEXT/RICHTEXT /// ``` #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Section<'a> { Part(Part), Header(Option), /// The subset returned by HEADER.FIELDS contains only those header fields with a field-name that /// matches one of the names in the list. HeaderFields(Option, NonEmptyVec>), // TODO: what if none matches? /// Similarly, the subset returned by HEADER.FIELDS.NOT contains only the header fields /// with a non-matching field-name. HeaderFieldsNot(Option, NonEmptyVec>), // TODO: what if none matches? /// The TEXT part specifier refers to the text body of the message, omitting the [RFC-2822] header. Text(Option), /// The MIME part specifier MUST be prefixed by one or more numeric part specifiers /// and refers to the [MIME-IMB] header for this part. Mime(Part), } #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Part(pub NonEmptyVec); /// A part specifier is either a part number or one of the following: /// `HEADER`, `HEADER.FIELDS`, `HEADER.FIELDS.NOT`, `MIME`, and `TEXT`. /// /// The HEADER, HEADER.FIELDS, and HEADER.FIELDS.NOT part /// specifiers refer to the [RFC-2822] header of the message or of /// an encapsulated [MIME-IMT] MESSAGE/RFC822 message. /// HEADER.FIELDS and HEADER.FIELDS.NOT are followed by a list of /// field-name (as defined in [RFC-2822]) names, and return a /// subset of the header. /// /// The field-matching is case-insensitive but otherwise exact. /// Subsetting does not exclude the [RFC-2822] delimiting blank line between the header /// and the body; the blank line is included in all header fetches, /// except in the case of a message which has no body and no blank /// line. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum PartSpecifier<'a> { PartNumber(u32), Header, HeaderFields(NonEmptyVec>), HeaderFieldsNot(NonEmptyVec>), Mime, Text, } imap-codec-1.0.0/imap-types/src/flag.rs000066400000000000000000000161731447115025300176610ustar00rootroot00000000000000//! Flag-related types. use std::fmt::{Display, Formatter}; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{core::Atom, error::ValidationError}; /// There are two types of flags in IMAP4rev1: System and keyword flags. /// /// A system flag is a flag name that is pre-defined in RFC3501. /// All system flags begin with "\\" and certain system flags (`\Deleted` and `\Seen`) have special semantics. /// Flags that begin with "\\" but are not pre-defined system flags, are extension flags. /// Clients MUST accept them and servers MUST NOT send them except when defined by future standard or standards-track revisions. /// /// A keyword is defined by the server implementation. /// Keywords do not begin with "\\" and servers may permit the client to define new ones /// in the mailbox by sending the `\*` flag ([`FlagPerm::Asterisk`]) in the PERMANENTFLAGS response.. /// /// Note that a flag of either type can be permanent or session-only. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Flag<'a> { /// Message has been answered (`\Answered`). Answered, /// Message is "deleted" for removal by later EXPUNGE (`\Deleted`). Deleted, /// Message has not completed composition (marked as a draft) (`\Draft`). Draft, /// Message is "flagged" for urgent/special attention (`\Flagged`). Flagged, /// Message has been read (`\Seen`). Seen, /// A future expansion of a system flag. Extension(FlagExtension<'a>), /// A keyword. Keyword(Atom<'a>), } /// An (extension) flag. /// /// It's guaranteed that this type can't represent any flag from [`Flag`]. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct FlagExtension<'a>(Atom<'a>); impl<'a> Flag<'a> { pub fn system(atom: Atom<'a>) -> Self { match atom.as_ref().to_ascii_lowercase().as_ref() { "answered" => Self::Answered, "deleted" => Self::Deleted, "draft" => Self::Draft, "flagged" => Self::Flagged, "seen" => Self::Seen, _ => Self::Extension(FlagExtension(atom)), } } pub fn keyword(atom: Atom<'a>) -> Self { Self::Keyword(atom) } } impl<'a> TryFrom<&'a str> for Flag<'a> { type Error = ValidationError; fn try_from(value: &'a str) -> Result { Ok(if let Some(value) = value.strip_prefix('\\') { Self::system(Atom::try_from(value)?) } else { Self::keyword(Atom::try_from(value)?) }) } } impl<'a> Display for Flag<'a> { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { match self { Flag::Answered => f.write_str("\\Answered"), Flag::Deleted => f.write_str("\\Deleted"), Flag::Draft => f.write_str("\\Draft"), Flag::Flagged => f.write_str("\\Flagged"), Flag::Seen => f.write_str("\\Seen"), Flag::Extension(other) => write!(f, "\\{}", other.0), Flag::Keyword(atom) => write!(f, "{}", atom), } } } #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum FlagFetch<'a> { Flag(Flag<'a>), /// Message is "recently" arrived in this mailbox. (`\Recent`) /// /// This session is the first session to have been notified about this message; if the session /// is read-write, subsequent sessions will not see \Recent set for this message. /// /// Note: This flag can not be altered by the client. Recent, } #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum FlagPerm<'a> { Flag(Flag<'a>), /// Indicates that it is possible to create new keywords by /// attempting to store those flags in the mailbox (`\*`). Asterisk, } /// Four name attributes are defined. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum FlagNameAttribute<'a> { /// It is not possible for any child levels of hierarchy to exist /// under this name; no child levels exist now and none can be /// created in the future. (`\Noinferiors`) Noinferiors, /// It is not possible to use this name as a selectable mailbox. (`\Noselect`) Noselect, /// The mailbox has been marked "interesting" by the server; the /// mailbox probably contains messages that have been added since /// the last time the mailbox was selected. (`\Marked`) Marked, /// The mailbox does not contain any additional messages since the /// last time the mailbox was selected. (`\Unmarked`) Unmarked, /// An extension flags. Extension(FlagNameAttributeExtension<'a>), } /// An extension flag. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct FlagNameAttributeExtension<'a>(Atom<'a>); impl<'a> FlagNameAttribute<'a> { pub fn is_selectability(&self) -> bool { matches!( self, FlagNameAttribute::Noselect | FlagNameAttribute::Marked | FlagNameAttribute::Unmarked ) } } impl<'a> From> for FlagNameAttribute<'a> { fn from(atom: Atom<'a>) -> Self { match atom.as_ref().to_ascii_lowercase().as_ref() { "noinferiors" => Self::Noinferiors, "noselect" => Self::Noselect, "marked" => Self::Marked, "unmarked" => Self::Unmarked, _ => Self::Extension(FlagNameAttributeExtension(atom)), } } } impl<'a> Display for FlagNameAttribute<'a> { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { match self { Self::Noinferiors => f.write_str("\\Noinferiors"), Self::Noselect => f.write_str("\\Noselect"), Self::Marked => f.write_str("\\Marked"), Self::Unmarked => f.write_str("\\Unmarked"), Self::Extension(extension) => write!(f, "\\{}", extension.0), } } } #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum StoreType { Replace, Add, Remove, } #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum StoreResponse { Answer, Silent, } imap-codec-1.0.0/imap-types/src/lib.rs000066400000000000000000000216711447115025300175150ustar00rootroot00000000000000//! # Misuse-resistant IMAP types //! //! The most prominent types in imap-types are [`Greeting`](response::Greeting), [`Command`](command::Command), and [`Response`](response::Response), and we use the term "message" to refer to either of them. //! Messages can be created in different ways. //! However, what all ways have in common is, that the API does not allow the creation of invalid ones. //! //! For example, all commands in IMAP are prefixed with a "tag". //! Although IMAP's tags are just strings, they have additional rules, such as that no whitespace is allowed. //! Thus, imap-types encapsulate them in [`Tag`](core::Tag) struct to ensure that invalid ones can't be created. //! //! ## Understanding and using the core types //! //! Similar to [`Tag`](core::Tag)s, there are more "core types" (or "string types"), such as, [`Atom`](core::Atom), [`Quoted`](core::Quoted), or [`Literal`](core::Literal). //! Besides being used for correctness, these types play a crucial role in IMAP because they determine the IMAP protocol flow. //! Sending a password as a literal requires a different protocol flow than sending the password as an atom or a quoted string. //! So, even though imap-types can choose the most efficient representation for a datum automatically, it's good to become familiar with the [`core`] module at some point to master the IMAP protocol. //! //! ## Construction of messages //! //! imap-types relies a lot on the standard conversion traits, i.e., [`From`], [`TryFrom`], [`Into`], and [`TryInto`]. //! Make good use of them. //! More convenient constructors are available for types that are more cumbersome to create. //! //! Note: When you are *sure* that the thing you want to create is valid, you can unlock various `unvalidated(...)` functions through the `unvalidated` feature. //! This allows us to bypass certain checks in release builds. //! //! ### Example //! //! ``` //! use imap_types::{ //! command::{Command, CommandBody}, //! core::Tag, //! }; //! //! // # Variant 1 //! // Create a `Command` with `tag` "A123" and `body` "NOOP". //! // (Note: `Command::new()` returns `Err(...)` when the tag is invalid.) //! let cmd = Command::new("A123", CommandBody::Noop).unwrap(); //! //! // # Variant 2 //! // Create a `CommandBody` first and finalize it into //! // a `Command` by attaching a tag later. //! let cmd = CommandBody::Noop.tag("A123").unwrap(); //! //! // # Variant 3 //! // Create a `Command` directly. //! let cmd = Command { //! tag: Tag::try_from("A123").unwrap(), //! body: CommandBody::Noop, //! }; //! ``` //! //! ## More complex messages //! //! ### Example //! //! The following example is a server fetch response containing the size and MIME structure of a message with the sequence number (or UID) 42. //! //! ``` //! use std::{borrow::Cow, num::NonZeroU32}; //! //! use imap_types::{ //! body::{BasicFields, Body, BodyStructure, SinglePartExtensionData, SpecificFields}, //! core::{IString, NString, NonEmptyVec}, //! fetch::MessageDataItem, //! response::{Data, Response}, //! }; //! //! let fetch = { //! let data = Data::Fetch { //! seq: NonZeroU32::new(42).unwrap(), //! items: NonEmptyVec::try_from(vec![ //! MessageDataItem::Rfc822Size(1337), //! MessageDataItem::Body(BodyStructure::Single { //! body: Body { //! basic: BasicFields { //! parameter_list: vec![], //! id: NString(None), //! description: NString(Some( //! IString::try_from("Important message.").unwrap(), //! )), //! content_transfer_encoding: IString::try_from("base64").unwrap(), //! size: 512, //! }, //! specific: SpecificFields::Basic { //! r#type: IString::try_from("text").unwrap(), //! subtype: IString::try_from("html").unwrap(), //! }, //! }, //! extension_data: None, //! }), //! ]) //! .unwrap(), //! }; //! //! Response::Data(data) //! }; //! ``` //! //! # Supported IMAP extensions //! //! |Description | //! |-------------------------------------------------------------| //! |IMAP4 Non-synchronizing Literals ([RFC 2088], [RFC 7888]) | //! |IMAP MOVE Extension ([RFC 6851]) | //! |IMAP UNSELECT command ([RFC 3691]) | //! |IMAP Extension for SASL Initial Client Response ([RFC 4959]) | //! |The IMAP COMPRESS Extension ([RFC 4978]) | //! |The IMAP ENABLE Extension ([RFC 5161]) | //! |IMAP4 IDLE command ([RFC 2177]) | //! |IMAP QUOTA Extension ([RFC 9208]) | //! //! # Features //! //! This crate uses the following features to enable experimental IMAP extensions: //! //! |Feature |Description |Status | //! |---------------------|-------------------------------------------------------------------------------------|----------| //! |ext_condstore_qresync|Quick Flag Changes Resynchronization and Quick Mailbox Resynchronization ([RFC 7162])|Unfinished| //! |ext_login_referrals |IMAP4 Login Referrals ([RFC 2221]) |Unfinished| //! |ext_mailbox_referrals|IMAP4 Mailbox Referrals ([RFC 2193]) |Unfinished| //! |starttls |IMAP4rev1 ([RFC 3501]; section 6.2.1) | | //! //! STARTTLS is not an IMAP extension but feature-gated because it [should be avoided](https://nostarttls.secvuln.info/). //! For better performance and security, use "implicit TLS", i.e., IMAP-over-TLS on port 993, and don't use STARTTLS at all. //! //! Furthermore, imap-types uses the following features to facilitate interoperability: //! //! | Feature | Description | Enabled by default | //! |------------------|----------------------------------------------------------------|--------------------| //! | arbitrary | Derive `Arbitrary` implementations. | No | //! | bounded-static | Derive `ToStatic/IntoStatic` implementations. | No | //! | serde | Derive `serde`s `Serialize` and `Deserialize` implementations. | No | //! | unvalidated | Unlock `unvalidated` constructors. | No | //! //! When using `arbitrary`, all types defined in imap-types implement the [Arbitrary] trait to ease testing. //! This is used, for example, to generate instances during fuzz-testing. //! (See, e.g., `imap-types/fuzz/fuzz_targets/to_static.rs`) //! When using `bounded-static`, all types provide a `to_static` and `into_static` method that converts a type into its "owned" variant. //! This is useful when you want to pass objects around, e.g., into other threads, a vector, etc. //! When the `serde` feature is used, all types implement [Serde](https://serde.rs/)'s [Serialize](https://docs.serde.rs/serde/trait.Serialize.html) and //! [Deserialize](https://docs.serde.rs/serde/trait.Deserialize.html) traits. (Try running `cargo run --example serde_json`.) //! //! [Arbitrary]: https://docs.rs/arbitrary/1.0.1/arbitrary/trait.Arbitrary.html //! [parse_command]: https://github.com/duesee/imap-codec/blob/main/imap-codec/examples/parse_command.rs //! [RFC 2088]: https://datatracker.ietf.org/doc/html/rfc2088 //! [RFC 2177]: https://datatracker.ietf.org/doc/html/rfc2177 //! [RFC 2193]: https://datatracker.ietf.org/doc/html/rfc2193 //! [RFC 2221]: https://datatracker.ietf.org/doc/html/rfc2221 //! [RFC 3501]: https://datatracker.ietf.org/doc/html/rfc3501 //! [RFC 3691]: https://datatracker.ietf.org/doc/html/rfc3691 //! [RFC 4959]: https://datatracker.ietf.org/doc/html/rfc4959 //! [RFC 4978]: https://datatracker.ietf.org/doc/html/rfc4978 //! [RFC 5161]: https://datatracker.ietf.org/doc/html/rfc5161 //! [RFC 6851]: https://datatracker.ietf.org/doc/html/rfc6851 //! [RFC 7162]: https://datatracker.ietf.org/doc/html/rfc7162 //! [RFC 7888]: https://datatracker.ietf.org/doc/html/rfc7888 //! [RFC 9208]: https://datatracker.ietf.org/doc/html/rfc9208 #![forbid(unsafe_code)] #![deny(missing_debug_implementations)] // TODO(#313) // #![deny(missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] #[cfg(feature = "arbitrary")] mod arbitrary; pub mod auth; pub mod body; pub mod command; pub mod core; pub mod datetime; pub mod envelope; pub mod error; pub mod extensions; pub mod fetch; pub mod flag; pub mod mailbox; pub mod response; pub mod search; pub mod secret; pub mod sequence; pub mod state; pub mod status; pub mod utils; #[cfg(feature = "bounded-static")] pub use bounded_static; imap-codec-1.0.0/imap-types/src/mailbox.rs000066400000000000000000000246241447115025300204030ustar00rootroot00000000000000//! Mailbox-related types. use std::{borrow::Cow, str::from_utf8}; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{ core::{impl_try_from, AString, IString}, error::{ValidationError, ValidationErrorKind}, mailbox::error::MailboxOtherError, utils::indicators::is_list_char, }; #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ListCharString<'a>(pub(crate) Cow<'a, str>); impl<'a> ListCharString<'a> { pub fn validate(value: impl AsRef<[u8]>) -> Result<(), ValidationError> { let value = value.as_ref(); if value.is_empty() { return Err(ValidationError::new(ValidationErrorKind::Empty)); } if let Some(at) = value.iter().position(|b| !is_list_char(*b)) { return Err(ValidationError::new(ValidationErrorKind::InvalidByteAt { byte: value[at], at, })); }; Ok(()) } /// Constructs a list char string without validation. /// /// # Warning: IMAP conformance /// /// The caller must ensure that `inner` is valid according to [`Self::validate`]. Failing to do /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows. /// Do not call this constructor with untrusted data. #[cfg(feature = "unvalidated")] #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))] pub fn unvalidated(inner: C) -> Self where C: Into>, { let inner = inner.into(); #[cfg(debug_assertions)] Self::validate(inner.as_bytes()).unwrap(); Self(inner) } } impl<'a> TryFrom<&'a str> for ListCharString<'a> { type Error = ValidationError; fn try_from(value: &'a str) -> Result { Self::validate(value)?; Ok(Self(Cow::Borrowed(value))) } } impl<'a> TryFrom for ListCharString<'a> { type Error = ValidationError; fn try_from(value: String) -> Result { Self::validate(&value)?; Ok(Self(Cow::Owned(value))) } } impl<'a> AsRef<[u8]> for ListCharString<'a> { fn as_ref(&self) -> &[u8] { self.0.as_bytes() } } #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ListMailbox<'a> { Token(ListCharString<'a>), String(IString<'a>), } impl<'a> TryFrom<&'a str> for ListMailbox<'a> { type Error = ValidationError; fn try_from(s: &'a str) -> Result { if s.is_empty() { // Safety: We know that an empty string can always be converted into a quoted string. return Ok(ListMailbox::String(IString::Quoted(s.try_into().unwrap()))); } if let Ok(lcs) = ListCharString::try_from(s) { return Ok(ListMailbox::Token(lcs)); } Ok(ListMailbox::String(s.try_into()?)) } } impl<'a> TryFrom for ListMailbox<'a> { type Error = ValidationError; fn try_from(s: String) -> Result { if s.is_empty() { // Safety: We know that an empty string can always be converted into a quoted string. return Ok(ListMailbox::String(IString::Quoted(s.try_into().unwrap()))); } // TODO(efficiency) if let Ok(lcs) = ListCharString::try_from(s.clone()) { return Ok(ListMailbox::Token(lcs)); } Ok(ListMailbox::String(s.try_into()?)) } } /// 5.1. Mailbox Naming /// /// Mailbox names are 7-bit. Client implementations MUST NOT attempt to /// create 8-bit mailbox names, and SHOULD interpret any 8-bit mailbox /// names returned by LIST or LSUB as UTF-8. Server implementations /// SHOULD prohibit the creation of 8-bit mailbox names, and SHOULD NOT /// return 8-bit mailbox names in LIST or LSUB. See section 5.1.3 for /// more information on how to represent non-ASCII mailbox names. /// /// Note: 8-bit mailbox names were undefined in earlier /// versions of this protocol. Some sites used a local 8-bit /// character set to represent non-ASCII mailbox names. Such /// usage is not interoperable, and is now formally deprecated. /// /// The case-insensitive mailbox name INBOX is a special name reserved to /// mean "the primary mailbox for this user on this server". The /// interpretation of all other names is implementation-dependent. /// /// In particular, this specification takes no position on case /// sensitivity in non-INBOX mailbox names. Some server implementations /// are fully case-sensitive; others preserve case of a newly-created /// name but otherwise are case-insensitive; and yet others coerce names /// to a particular case. Client implementations MUST interact with any /// of these. If a server implementation interprets non-INBOX mailbox /// names as case-insensitive, it MUST treat names using the /// international naming convention specially as described in section 5.1.3. /// /// There are certain client considerations when creating a new mailbox name: /// /// 1) Any character which is one of the atom-specials (see the Formal Syntax) will require /// that the mailbox name be represented as a quoted string or literal. /// 2) CTL and other non-graphic characters are difficult to represent in a user interface /// and are best avoided. /// 3) Although the list-wildcard characters ("%" and "*") are valid in a mailbox name, it is /// difficult to use such mailbox names with the LIST and LSUB commands due to the conflict /// with wildcard interpretation. /// 4) Usually, a character (determined by the server implementation) is reserved to delimit /// levels of hierarchy. /// 5) Two characters, "#" and "&", have meanings by convention, and should be avoided except /// when used in that convention. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Mailbox<'a> { Inbox, Other(MailboxOther<'a>), } impl_try_from!(AString<'a>, 'a, &'a [u8], Mailbox<'a>); impl_try_from!(AString<'a>, 'a, Vec, Mailbox<'a>); impl_try_from!(AString<'a>, 'a, &'a str, Mailbox<'a>); impl_try_from!(AString<'a>, 'a, String, Mailbox<'a>); impl<'a> From> for Mailbox<'a> { fn from(value: AString<'a>) -> Self { match from_utf8(value.as_ref()) { Ok(value) if value.to_ascii_lowercase() == "inbox" => Self::Inbox, _ => Self::Other(MailboxOther::try_from(value).unwrap()), } } } // We do not implement `AsRef<...>` for `Mailbox` because we want to enforce that a consumer // `match`es on `Mailbox::Inbox`/`Mailbox::Other`. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct MailboxOther<'a>(pub(crate) AString<'a>); impl<'a> MailboxOther<'a> { pub fn validate(value: impl AsRef<[u8]>) -> Result<(), MailboxOtherError> { if value.as_ref().to_ascii_lowercase() == b"inbox" { return Err(MailboxOtherError::Reserved); } Ok(()) } pub fn inner(&self) -> &AString { &self.0 } /// Constructs a mailbox without validation. /// /// # Warning: IMAP conformance /// /// The caller must ensure that `value` is valid according to [`Self::validate`]. Failing to do /// so may create invalid/unparsable IMAP messages, or even produce unintended protocol flows. /// Do not call this constructor with untrusted data. #[cfg(feature = "unvalidated")] #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))] pub fn unvalidated(value: AString<'a>) -> Self { #[cfg(debug_assertions)] Self::validate(&value).unwrap(); Self(value) } } macro_rules! impl_try_from { ($from:ty) => { impl<'a> TryFrom<$from> for MailboxOther<'a> { type Error = MailboxOtherError; fn try_from(value: $from) -> Result { let astring = AString::try_from(value)?; Self::validate(&astring)?; Ok(Self(astring)) } } }; } impl_try_from!(&'a [u8]); impl_try_from!(Vec); impl_try_from!(&'a str); impl_try_from!(String); impl<'a> TryFrom> for MailboxOther<'a> { type Error = MailboxOtherError; fn try_from(value: AString<'a>) -> Result { Self::validate(&value)?; Ok(Self(value)) } } impl<'a> AsRef<[u8]> for MailboxOther<'a> { fn as_ref(&self) -> &[u8] { self.0.as_ref() } } /// Error-related types. pub mod error { use thiserror::Error; use crate::error::ValidationError; #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub enum MailboxOtherError { #[error(transparent)] Literal(#[from] ValidationError), #[error("Reserved: Please use one of the typed variants")] Reserved, } } #[cfg(test)] mod tests { use std::borrow::Cow; use super::*; use crate::core::{AString, IString, Literal, LiteralMode}; #[test] fn test_conversion_mailbox() { let tests = [ ("inbox", Mailbox::Inbox), ("inboX", Mailbox::Inbox), ("Inbox", Mailbox::Inbox), ("InboX", Mailbox::Inbox), ("INBOX", Mailbox::Inbox), ( "INBO²", Mailbox::Other(MailboxOther(AString::String(IString::Literal(Literal { data: Cow::Borrowed("INBO²".as_bytes()), mode: LiteralMode::Sync, })))), ), ]; for (test, expected) in tests { let got = Mailbox::try_from(test).unwrap(); assert_eq!(expected, got); let got = Mailbox::try_from(String::from(test)).unwrap(); assert_eq!(expected, got); } } #[test] fn test_conversion_mailbox_failing() { let tests = ["\x00", "A\x00", "\x00A"]; for test in tests { assert!(Mailbox::try_from(test).is_err()); assert!(Mailbox::try_from(String::from(test)).is_err()); } } } imap-codec-1.0.0/imap-types/src/response.rs000066400000000000000000001171061447115025300206040ustar00rootroot00000000000000//! # 7. Server Responses use std::{ borrow::Cow, fmt::{Debug, Display, Formatter}, num::{NonZeroU32, TryFromIntError}, }; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; use base64::{engine::general_purpose::STANDARD as _base64, Engine}; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{ auth::AuthMechanism, core::{impl_try_from, AString, Atom, Charset, NonEmptyVec, QuotedChar, Tag, Text}, error::ValidationError, extensions::{ compress::CompressionAlgorithm, enable::CapabilityEnable, quota::{QuotaGet, Resource}, }, fetch::MessageDataItem, flag::{Flag, FlagNameAttribute, FlagPerm}, mailbox::Mailbox, response::error::{ContinueError, FetchError}, status::StatusDataItem, }; /// An IMAP greeting. /// /// Note: Don't use `code: None` *and* a `text` that starts with "[" as this would be ambiguous in IMAP. // TODO(301) #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Greeting<'a> { pub kind: GreetingKind, pub code: Option>, pub text: Text<'a>, } impl<'a> Greeting<'a> { pub fn new( kind: GreetingKind, code: Option>, text: &'a str, ) -> Result { Ok(Greeting { kind, code, text: text.try_into()?, }) } pub fn ok(code: Option>, text: &'a str) -> Result { Ok(Greeting { kind: GreetingKind::Ok, code, text: text.try_into()?, }) } pub fn preauth(code: Option>, text: &'a str) -> Result { Ok(Greeting { kind: GreetingKind::PreAuth, code, text: text.try_into()?, }) } pub fn bye(code: Option>, text: &'a str) -> Result { Ok(Greeting { kind: GreetingKind::Bye, code, text: text.try_into()?, }) } } #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] /// IMAP4rev1 defines three possible greetings at connection startup. pub enum GreetingKind { /// The connection is not yet authenticated. /// /// (Advice: A LOGIN command is needed.) Ok, /// The connection has already been authenticated by external means. /// /// (Advice: No LOGIN command is needed.) PreAuth, /// The server is not willing to accept a connection from this client. /// /// (Advice: The server closes the connection immediately.) Bye, } #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Response<'a> { /// Command continuation request responses use the token "+" instead of a /// tag. These responses are sent by the server to indicate acceptance /// of an incomplete client command and readiness for the remainder of /// the command. CommandContinuationRequest(CommandContinuationRequest<'a>), /// All server data is untagged. An untagged response is indicated by the /// token "*" instead of a tag. Untagged status responses indicate server /// greeting, or server status that does not indicate the completion of a /// command (for example, an impending system shutdown alert). Data(Data<'a>), /// Status responses can be tagged or untagged. Tagged status responses /// indicate the completion result (OK, NO, or BAD status) of a client /// command, and have a tag matching the command. Status(Status<'a>), } /// ## 7.1. Server Responses - Status Responses /// /// Status responses are OK, NO, BAD, PREAUTH and BYE. /// OK, NO, and BAD can be tagged or untagged. /// PREAUTH and BYE are always untagged. /// Status responses MAY include an OPTIONAL "response code" (see [`Code`](crate::response::Code).) /// /// Note: Don't use `code: None` *and* a `text` that starts with "[" as this would be ambiguous in IMAP. // TODO(301) #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Status<'a> { /// ### 7.1.1. OK Response /// /// The OK response indicates an information message from the server. Ok { /// When tagged, it indicates successful completion of the associated /// command. The human-readable text MAY be presented to the user as /// an information message. /// /// The untagged form indicates an information-only message; the nature /// of the information MAY be indicated by a response code. /// /// The untagged form is also used as one of three possible greetings /// at connection startup. It indicates that the connection is not /// yet authenticated and that a LOGIN command is needed. tag: Option>, /// Response code (optional) code: Option>, /// Human-readable text (must be at least 1 character!) text: Text<'a>, }, /// ### 7.1.2. NO Response /// /// The NO response indicates an operational error message from the server. No { /// When tagged, it indicates unsuccessful completion of the /// associated command. The untagged form indicates a warning; the /// command can still complete successfully. tag: Option>, /// Response code (optional) code: Option>, /// The human-readable text describes the condition. (must be at least 1 character!) text: Text<'a>, }, /// ### 7.1.3. BAD Response /// /// The BAD response indicates an error message from the server. Bad { /// When tagged, it reports a protocol-level error in the client's command; /// the tag indicates the command that caused the error. The untagged /// form indicates a protocol-level error for which the associated /// command can not be determined; it can also indicate an internal /// server failure. tag: Option>, /// Response code (optional) code: Option>, /// The human-readable text describes the condition. (must be at least 1 character!) text: Text<'a>, }, /// ### 7.1.5. BYE Response /// /// The BYE response is always untagged, and indicates that the server /// is about to close the connection. /// /// The BYE response is sent under one of four conditions: /// /// 1) as part of a normal logout sequence. The server will close /// the connection after sending the tagged OK response to the /// LOGOUT command. /// /// 2) as a panic shutdown announcement. The server closes the /// connection immediately. /// /// 3) as an announcement of an inactivity autologout. The server /// closes the connection immediately. /// /// 4) as one of three possible greetings at connection startup, /// indicating that the server is not willing to accept a /// connection from this client. The server closes the /// connection immediately. /// /// The difference between a BYE that occurs as part of a normal /// LOGOUT sequence (the first case) and a BYE that occurs because of /// a failure (the other three cases) is that the connection closes /// immediately in the failure case. In all cases the client SHOULD /// continue to read response data from the server until the /// connection is closed; this will ensure that any pending untagged /// or completion responses are read and processed. Bye { /// Response code (optional) code: Option>, /// The human-readable text MAY be displayed to the user in a status /// report by the client. (must be at least 1 character!) text: Text<'a>, }, } impl<'a> Status<'a> { // FIXME(API) pub fn ok(tag: Option>, code: Option>, text: T) -> Result where T: TryInto>, { Ok(Status::Ok { tag, code, text: text.try_into()?, }) } // FIXME(API) pub fn no(tag: Option>, code: Option>, text: T) -> Result where T: TryInto>, { Ok(Status::No { tag, code, text: text.try_into()?, }) } // FIXME(API) pub fn bad(tag: Option>, code: Option>, text: T) -> Result where T: TryInto>, { Ok(Status::Bad { tag, code, text: text.try_into()?, }) } pub fn bye(code: Option>, text: T) -> Result where T: TryInto>, { Ok(Status::Bye { code, text: text.try_into()?, }) } // --------------------------------------------------------------------------------------------- pub fn tag(&self) -> Option<&Tag> { match self { Status::Ok { tag, .. } | Status::No { tag, .. } | Status::Bad { tag, .. } => { tag.as_ref() } Status::Bye { .. } => None, } } pub fn code(&self) -> Option<&Code> { match self { Status::Ok { code, .. } | Status::No { code, .. } | Status::Bad { code, .. } | Status::Bye { code, .. } => code.as_ref(), } } pub fn text(&self) -> &Text { match self { Status::Ok { text, .. } | Status::No { text, .. } | Status::Bad { text, .. } | Status::Bye { text, .. } => text, } } } /// ## 7.2 - 7.4 Server and Mailbox Status; Mailbox Size; Message Status #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Data<'a> { // ## 7.2. Server Responses - Server and Mailbox Status // // These responses are always untagged. This is how server and mailbox // status data are transmitted from the server to the client. Many of // these responses typically result from a command with the same name. /// ### 7.2.1. CAPABILITY Response /// /// * Contents: capability listing /// /// The CAPABILITY response occurs as a result of a CAPABILITY /// command. The capability listing contains a space-separated /// listing of capability names that the server supports. The /// capability listing MUST include the atom "IMAP4rev1". /// /// In addition, client and server implementations MUST implement the /// STARTTLS, LOGINDISABLED, and AUTH=PLAIN (described in [IMAP-TLS]) /// capabilities. See the Security Considerations section for /// important information. /// /// A capability name which begins with "AUTH=" indicates that the /// server supports that particular authentication mechanism. /// /// The LOGINDISABLED capability indicates that the LOGIN command is /// disabled, and that the server will respond with a tagged NO /// response to any attempt to use the LOGIN command even if the user /// name and password are valid. An IMAP client MUST NOT issue the /// LOGIN command if the server advertises the LOGINDISABLED /// capability. /// /// Other capability names indicate that the server supports an /// extension, revision, or amendment to the IMAP4rev1 protocol. /// Server responses MUST conform to this document until the client /// issues a command that uses the associated capability. /// /// Capability names MUST either begin with "X" or be standard or /// standards-track IMAP4rev1 extensions, revisions, or amendments /// registered with IANA. A server MUST NOT offer unregistered or /// non-standard capability names, unless such names are prefixed with /// an "X". /// /// Client implementations SHOULD NOT require any capability name /// other than "IMAP4rev1", and MUST ignore any unknown capability /// names. /// /// A server MAY send capabilities automatically, by using the /// CAPABILITY response code in the initial PREAUTH or OK responses, /// and by sending an updated CAPABILITY response code in the tagged /// OK response as part of a successful authentication. It is /// unnecessary for a client to send a separate CAPABILITY command if /// it recognizes these automatic capabilities. Capability(NonEmptyVec>), /// ### 7.2.2. LIST Response /// /// The LIST response occurs as a result of a LIST command. It /// returns a single name that matches the LIST specification. There /// can be multiple LIST responses for a single LIST command. /// /// The hierarchy delimiter is a character used to delimit levels of /// hierarchy in a mailbox name. A client can use it to create child /// mailboxes, and to search higher or lower levels of naming /// hierarchy. All children of a top-level hierarchy node MUST use /// the same separator character. A NIL hierarchy delimiter means /// that no hierarchy exists; the name is a "flat" name. /// /// The name represents an unambiguous left-to-right hierarchy, and /// MUST be valid for use as a reference in LIST and LSUB commands. /// Unless \Noselect is indicated, the name MUST also be valid as an /// argument for commands, such as SELECT, that accept mailbox names. List { /// Name attributes items: Vec>, /// Hierarchy delimiter delimiter: Option, /// Name mailbox: Mailbox<'a>, }, /// ### 7.2.3. LSUB Response /// /// The LSUB response occurs as a result of an LSUB command. It /// returns a single name that matches the LSUB specification. There /// can be multiple LSUB responses for a single LSUB command. The /// data is identical in format to the LIST response. Lsub { /// Name attributes items: Vec>, /// Hierarchy delimiter delimiter: Option, /// Name mailbox: Mailbox<'a>, }, /// ### 7.2.4 STATUS Response /// /// The STATUS response occurs as a result of an STATUS command. It /// returns the mailbox name that matches the STATUS specification and /// the requested mailbox status information. Status { /// Name mailbox: Mailbox<'a>, /// Status parenthesized list items: Cow<'a, [StatusDataItem]>, }, /// ### 7.2.5. SEARCH Response /// /// * Contents: zero or more numbers /// /// The SEARCH response occurs as a result of a SEARCH or UID SEARCH /// command. The number(s) refer to those messages that match the /// search criteria. For SEARCH, these are message sequence numbers; /// for UID SEARCH, these are unique identifiers. Each number is /// delimited by a space. Search(Vec), /// ### 7.2.6. FLAGS Response /// /// * Contents: flag parenthesized list /// /// The FLAGS response occurs as a result of a SELECT or EXAMINE /// command. The flag parenthesized list identifies the flags (at a /// minimum, the system-defined flags) that are applicable for this /// mailbox. Flags other than the system flags can also exist, /// depending on server implementation. /// /// The update from the FLAGS response MUST be recorded by the client. Flags(Vec>), // ## 7.3. Server Responses - Mailbox Size // // These responses are always untagged. This is how changes in the size // of the mailbox are transmitted from the server to the client. // Immediately following the "*" token is a number that represents a // message count. /// ### 7.3.1. EXISTS Response /// /// The EXISTS response reports the number of messages in the mailbox. /// This response occurs as a result of a SELECT or EXAMINE command, /// and if the size of the mailbox changes (e.g., new messages). /// /// The update from the EXISTS response MUST be recorded by the client. Exists(u32), /// ### 7.3.2. RECENT Response /// /// The RECENT response reports the number of messages with the /// \Recent flag set. This response occurs as a result of a SELECT or /// EXAMINE command, and if the size of the mailbox changes (e.g., new /// messages). /// /// Note: It is not guaranteed that the message sequence /// numbers of recent messages will be a contiguous range of /// the highest n messages in the mailbox (where n is the /// value reported by the RECENT response). Examples of /// situations in which this is not the case are: multiple /// clients having the same mailbox open (the first session /// to be notified will see it as recent, others will /// probably see it as non-recent), and when the mailbox is /// re-ordered by a non-IMAP agent. /// /// The only reliable way to identify recent messages is to /// look at message flags to see which have the \Recent flag /// set, or to do a SEARCH RECENT. /// /// The update from the RECENT response MUST be recorded by the client. Recent(u32), // ## 7.4. Server Responses - Message Status // // These responses are always untagged. This is how message data are // transmitted from the server to the client, often as a result of a // command with the same name. Immediately following the "*" token is a // number that represents a message sequence number. /// ### 7.4.1. EXPUNGE Response /// /// The EXPUNGE response reports that the specified message sequence /// number has been permanently removed from the mailbox. The message /// sequence number for each successive message in the mailbox is /// immediately decremented by 1, and this decrement is reflected in /// message sequence numbers in subsequent responses (including other /// untagged EXPUNGE responses). /// /// The EXPUNGE response also decrements the number of messages in the /// mailbox; it is not necessary to send an EXISTS response with the /// new value. /// /// As a result of the immediate decrement rule, message sequence /// numbers that appear in a set of successive EXPUNGE responses /// depend upon whether the messages are removed starting from lower /// numbers to higher numbers, or from higher numbers to lower /// numbers. For example, if the last 5 messages in a 9-message /// mailbox are expunged, a "lower to higher" server will send five /// untagged EXPUNGE responses for message sequence number 5, whereas /// a "higher to lower server" will send successive untagged EXPUNGE /// responses for message sequence numbers 9, 8, 7, 6, and 5. /// /// An EXPUNGE response MUST NOT be sent when no command is in /// progress, nor while responding to a FETCH, STORE, or SEARCH /// command. This rule is necessary to prevent a loss of /// synchronization of message sequence numbers between client and /// server. A command is not "in progress" until the complete command /// has been received; in particular, a command is not "in progress" /// during the negotiation of command continuation. /// /// Note: UID FETCH, UID STORE, and UID SEARCH are different /// commands from FETCH, STORE, and SEARCH. An EXPUNGE /// response MAY be sent during a UID command. /// /// The update from the EXPUNGE response MUST be recorded by the client. Expunge(NonZeroU32), /// ### 7.4.2. FETCH Response /// /// The FETCH response returns data about a message to the client. /// The data are pairs of data item names and their values in /// parentheses. This response occurs as the result of a FETCH or /// STORE command, as well as by unilateral server decision (e.g., /// flag updates). Fetch { /// Sequence number. seq: NonZeroU32, /// Message data items. items: NonEmptyVec>, }, Enabled { capabilities: Vec>, }, Quota { /// Quota root. root: AString<'a>, /// List of quotas. quotas: NonEmptyVec>, }, QuotaRoot { /// Mailbox name. mailbox: Mailbox<'a>, /// List of quota roots. roots: Vec>, }, } impl<'a> Data<'a> { pub fn capability(caps: C) -> Result where C: TryInto>>, { Ok(Self::Capability(caps.try_into()?)) } // TODO // pub fn list() -> Self { // unimplemented!() // } // TODO // pub fn lsub() -> Self { // unimplemented!() // } // TODO // pub fn status() -> Self { // unimplemented!() // } // TODO // pub fn search() -> Self { // unimplemented!() // } // TODO // pub fn flags() -> Self { // unimplemented!() // } pub fn expunge(seq: u32) -> Result { Ok(Self::Expunge(NonZeroU32::try_from(seq)?)) } pub fn fetch(seq: S, items: I) -> Result> where S: TryInto, I: TryInto>>, { let seq = seq.try_into().map_err(FetchError::SeqOrUid)?; let items = items.try_into().map_err(FetchError::InvalidItems)?; Ok(Self::Fetch { seq, items }) } } /// ## 7.5. Server Responses - Command Continuation Request /// /// The command continuation request response is indicated by a "+" token /// instead of a tag. This form of response indicates that the server is /// ready to accept the continuation of a command from the client. The /// remainder of this response is a line of text. /// /// This response is used in the AUTHENTICATE command to transmit server /// data to the client, and request additional client data. This /// response is also used if an argument to any command is a literal. /// /// The client is not permitted to send the octets of the literal unless /// the server indicates that it is expected. This permits the server to /// process commands and reject errors on a line-by-line basis. The /// remainder of the command, including the CRLF that terminates a /// command, follows the octets of the literal. If there are any /// additional command arguments, the literal octets are followed by a /// space and those arguments. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[doc(alias = "Continue")] #[doc(alias = "Continuation")] #[doc(alias = "ContinuationRequest")] pub enum CommandContinuationRequest<'a> { Basic(CommandContinuationRequestBasic<'a>), Base64(Cow<'a, [u8]>), } impl<'a> CommandContinuationRequest<'a> { pub fn basic(code: Option>, text: T) -> Result> where T: TryInto>, { Ok(Self::Basic(CommandContinuationRequestBasic::new( code, text, )?)) } pub fn base64<'data: 'a, D>(data: D) -> Self where D: Into>, { Self::Base64(data.into()) } } #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct CommandContinuationRequestBasic<'a> { code: Option>, text: Text<'a>, } impl<'a> CommandContinuationRequestBasic<'a> { /// Create a basic continuation request. /// /// Note: To avoid ambiguities in the IMAP standard, this constructor ensures that: /// * iff `code` is `None`, `text` must not start with `[`. /// * iff `code` is `None`, `text` must *not* be valid according to base64. /// Otherwise, we could send a `Continue::Basic` that is interpreted as `Continue::Base64`. pub fn new(code: Option>, text: T) -> Result> where T: TryInto>, { let text = text.try_into().map_err(ContinueError::Text)?; // Ambiguity #1 if code.is_none() && text.as_ref().starts_with('[') { return Err(ContinueError::Ambiguity); } // Ambiguity #2 if code.is_none() && _base64.decode(text.inner()).is_ok() { return Err(ContinueError::Ambiguity); } Ok(Self { code, text }) } pub fn code(&self) -> Option<&Code<'a>> { self.code.as_ref() } pub fn text(&self) -> &Text<'a> { &self.text } } /// A response code consists of data inside square brackets in the form of an atom, /// possibly followed by a space and arguments. The response code /// contains additional information or status codes for client software /// beyond the OK/NO/BAD condition, and are defined when there is a /// specific action that a client can take based upon the additional /// information. /// /// The currently defined response codes are: #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Code<'a> { /// `ALERT` /// /// The human-readable text contains a special alert that MUST be /// presented to the user in a fashion that calls the user's /// attention to the message. Alert, /// `BADCHARSET` /// /// Optionally followed by a parenthesized list of charsets. A /// SEARCH failed because the given charset is not supported by /// this implementation. If the optional list of charsets is /// given, this lists the charsets that are supported by this /// implementation. BadCharset { allowed: Vec>, }, /// `CAPABILITY` /// /// Followed by a list of capabilities. This can appear in the /// initial OK or PREAUTH response to transmit an initial /// capabilities list. This makes it unnecessary for a client to /// send a separate CAPABILITY command if it recognizes this /// response. Capability(NonEmptyVec>), // FIXME(misuse): List must contain IMAP4REV1 /// `PARSE` /// /// The human-readable text represents an error in parsing the /// [RFC-2822] header or [MIME-IMB] headers of a message in the /// mailbox. Parse, /// `PERMANENTFLAGS` /// /// Followed by a parenthesized list of flags, indicates which of /// the known flags the client can change permanently. Any flags /// that are in the FLAGS untagged response, but not the /// PERMANENTFLAGS list, can not be set permanently. If the client /// attempts to STORE a flag that is not in the PERMANENTFLAGS /// list, the server will either ignore the change or store the /// state change for the remainder of the current session only. /// The PERMANENTFLAGS list can also include the special flag \*, /// which indicates that it is possible to create new keywords by /// attempting to store those flags in the mailbox. PermanentFlags(Vec>), /// `READ-ONLY` /// /// The mailbox is selected read-only, or its access while selected /// has changed from read-write to read-only. ReadOnly, /// `READ-WRITE` /// /// The mailbox is selected read-write, or its access while /// selected has changed from read-only to read-write. ReadWrite, /// `TRYCREATE` /// /// An APPEND or COPY attempt is failing because the target mailbox /// does not exist (as opposed to some other reason). This is a /// hint to the client that the operation can succeed if the /// mailbox is first created by the CREATE command. TryCreate, /// `UIDNEXT` /// /// Followed by a decimal number, indicates the next unique /// identifier value. Refer to section 2.3.1.1 for more /// information. UidNext(NonZeroU32), /// `UIDVALIDITY` /// /// Followed by a decimal number, indicates the unique identifier /// validity value. Refer to section 2.3.1.1 for more information. UidValidity(NonZeroU32), /// `UNSEEN` /// /// Followed by a decimal number, indicates the number of the first /// message without the \Seen flag set. Unseen(NonZeroU32), /// IMAP4 Login Referrals (RFC 2221) // TODO(misuse): the imap url is more complicated than that... #[cfg(any(feature = "ext_mailbox_referrals", feature = "ext_login_referrals"))] #[cfg_attr( docsrs, doc(cfg(any(feature = "ext_mailbox_referrals", feature = "ext_login_referrals"))) )] Referral(Cow<'a, str>), CompressionActive, /// SHOULD be returned in the tagged NO response to an APPEND/COPY/MOVE when the addition of the /// message(s) puts the target mailbox over any one of its quota limits. OverQuota, /// Server got a non-synchronizing literal larger than 4096 bytes. TooBig, /// Additional response codes defined by particular client or server /// implementations SHOULD be prefixed with an "X" until they are /// added to a revision of this protocol. Client implementations /// SHOULD ignore response codes that they do not recognize. /// /// --- /// /// ```abnf /// atom [SP 1*]` /// ``` /// /// Note: We use this as a fallback for everything that was not recognized as /// `Code`. This includes, e.g., variants with missing parameters, etc. Other(CodeOther<'a>), } impl<'a> Code<'a> { pub fn badcharset(allowed: Vec>) -> Self { Self::BadCharset { allowed } } pub fn capability(caps: C) -> Result where C: TryInto>>, { Ok(Self::Capability(caps.try_into()?)) } pub fn permanentflags(flags: Vec>) -> Self { Self::PermanentFlags(flags) } pub fn uidnext(uidnext: u32) -> Result { Ok(Self::UidNext(NonZeroU32::try_from(uidnext)?)) } pub fn uidvalidity(uidnext: u32) -> Result { Ok(Self::UidValidity(NonZeroU32::try_from(uidnext)?)) } pub fn unseen(uidnext: u32) -> Result { Ok(Self::Unseen(NonZeroU32::try_from(uidnext)?)) } } /// An (unknown) code. /// /// It's guaranteed that this type can't represent any code from [`Code`]. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, PartialEq, Eq, Hash)] pub struct CodeOther<'a>(Cow<'a, [u8]>); // We want a more readable `Debug` implementation. impl<'a> Debug for CodeOther<'a> { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { struct BStr<'a>(&'a Cow<'a, [u8]>); impl<'a> Debug for BStr<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "b\"{}\"", crate::utils::escape_byte_string(self.0.as_ref()) ) } } f.debug_tuple("CodeOther").field(&BStr(&self.0)).finish() } } impl<'a> CodeOther<'a> { /// Constructs an unsupported code without validation. /// /// # Warning: IMAP conformance /// /// The caller must ensure that `data` is valid. Failing to do so may create invalid/unparsable /// IMAP messages, or even produce unintended protocol flows. Do not call this constructor with /// untrusted data. #[cfg(feature = "unvalidated")] #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))] pub fn unvalidated(data: D) -> Self where D: Into>, { Self(data.into()) } pub fn inner(&self) -> &[u8] { self.0.as_ref() } } #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum Capability<'a> { Imap4Rev1, Auth(AuthMechanism<'a>), #[cfg(feature = "starttls")] #[cfg_attr(docsrs, doc(cfg(feature = "starttls")))] LoginDisabled, #[cfg(feature = "starttls")] #[cfg_attr(docsrs, doc(cfg(feature = "starttls")))] StartTls, /// See RFC 2177. Idle, /// See RFC 2193. #[cfg(feature = "ext_mailbox_referrals")] #[cfg_attr(docsrs, doc(cfg(feature = "ext_mailbox_referrals")))] MailboxReferrals, /// See RFC 2221. #[cfg(feature = "ext_login_referrals")] #[cfg_attr(docsrs, doc(cfg(feature = "ext_login_referrals")))] LoginReferrals, SaslIr, /// See RFC 5161. Enable, Compress { algorithm: CompressionAlgorithm, }, /// See RFC 2087 and RFC 9208 Quota, /// See RFC 9208. QuotaRes(Resource<'a>), /// See RFC 9208. QuotaSet, /// See RFC 7888. LiteralPlus, LiteralMinus, /// See RFC 6851. Move, /// Other/Unknown Other(CapabilityOther<'a>), } impl<'a> Display for Capability<'a> { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { match self { Self::Imap4Rev1 => write!(f, "IMAP4REV1"), Self::Auth(mechanism) => write!(f, "AUTH={}", mechanism), #[cfg(feature = "starttls")] Self::LoginDisabled => write!(f, "LOGINDISABLED"), #[cfg(feature = "starttls")] Self::StartTls => write!(f, "STARTTLS"), #[cfg(feature = "ext_mailbox_referrals")] Self::MailboxReferrals => write!(f, "MAILBOX-REFERRALS"), #[cfg(feature = "ext_login_referrals")] Self::LoginReferrals => write!(f, "LOGIN-REFERRALS"), Self::SaslIr => write!(f, "SASL-IR"), Self::Idle => write!(f, "IDLE"), Self::Enable => write!(f, "ENABLE"), Self::Compress { algorithm } => write!(f, "COMPRESS={}", algorithm), Self::Quota => write!(f, "QUOTA"), Self::QuotaRes(resource) => write!(f, "QUOTA=RES-{}", resource), Self::QuotaSet => write!(f, "QUOTASET"), Self::LiteralPlus => write!(f, "LITERAL+"), Self::LiteralMinus => write!(f, "LITERAL-"), Self::Move => write!(f, "MOVE"), Self::Other(other) => write!(f, "{}", other.0), } } } impl_try_from!(Atom<'a>, 'a, &'a [u8], Capability<'a>); impl_try_from!(Atom<'a>, 'a, Vec, Capability<'a>); impl_try_from!(Atom<'a>, 'a, &'a str, Capability<'a>); impl_try_from!(Atom<'a>, 'a, String, Capability<'a>); impl<'a> From> for Capability<'a> { fn from(atom: Atom<'a>) -> Self { fn split_once_cow<'a>( cow: Cow<'a, str>, pattern: &str, ) -> Option<(Cow<'a, str>, Cow<'a, str>)> { match cow { Cow::Borrowed(str) => { if let Some((left, right)) = str.split_once(pattern) { return Some((Cow::Borrowed(left), Cow::Borrowed(right))); } None } Cow::Owned(string) => { // TODO(efficiency) if let Some((left, right)) = string.split_once(pattern) { return Some((Cow::Owned(left.to_owned()), Cow::Owned(right.to_owned()))); } None } } } let cow = atom.into_inner(); match cow.to_ascii_lowercase().as_ref() { "imap4rev1" => Self::Imap4Rev1, #[cfg(feature = "starttls")] "logindisabled" => Self::LoginDisabled, #[cfg(feature = "starttls")] "starttls" => Self::StartTls, "idle" => Self::Idle, #[cfg(feature = "ext_mailbox_referrals")] "mailbox-referrals" => Self::MailboxReferrals, #[cfg(feature = "ext_login_referrals")] "login-referrals" => Self::LoginReferrals, "sasl-ir" => Self::SaslIr, "enable" => Self::Enable, "quota" => Self::Quota, "quotaset" => Self::QuotaSet, "literal+" => Self::LiteralPlus, "literal-" => Self::LiteralMinus, "move" => Self::Move, _ => { // TODO(efficiency) if let Some((left, right)) = split_once_cow(cow.clone(), "=") { match left.as_ref().to_ascii_lowercase().as_ref() { "auth" => { if let Ok(mechanism) = AuthMechanism::try_from(right) { return Self::Auth(mechanism); } } "compress" => { if let Ok(atom) = Atom::try_from(right) { if let Ok(algorithm) = CompressionAlgorithm::try_from(atom) { return Self::Compress { algorithm }; } } } "quota" => { if let Some((_, right)) = right.as_ref().to_ascii_lowercase().split_once("res-") { // TODO(efficiency) if let Ok(resource) = Resource::try_from(right.to_owned()) { return Self::QuotaRes(resource); } } } _ => {} } } Self::Other(CapabilityOther(Atom(cow))) } } } } /// An (unknown) capability. /// /// It's guaranteed that this type can't represent any capability from [`Capability`]. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct CapabilityOther<'a>(Atom<'a>); /// Error-related types. pub mod error { use thiserror::Error; #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub enum ContinueError { #[error("invalid text")] Text(T), #[error("ambiguity detected")] Ambiguity, } #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] pub enum FetchError { #[error("Invalid sequence or UID: {0:?}")] SeqOrUid(S), #[error("Invalid items: {0:?}")] InvalidItems(I), } } #[cfg(test)] mod tests { use super::*; #[test] fn test_conversion_data() { let _ = Data::capability(vec![Capability::Imap4Rev1]).unwrap(); let _ = Data::fetch(1, vec![MessageDataItem::Rfc822Size(123)]).unwrap(); } #[test] fn test_conversion_continue_failing() { let tests = [ CommandContinuationRequest::basic(None, ""), CommandContinuationRequest::basic(Some(Code::ReadWrite), ""), ]; for test in tests { println!("{:?}", test); assert!(test.is_err()); } } } imap-codec-1.0.0/imap-types/src/search.rs000066400000000000000000000121771447115025300202150ustar00rootroot00000000000000//! Search-related types. #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{ core::{AString, Atom, NonEmptyVec}, datetime::NaiveDate, sequence::SequenceSet, }; /// The defined search keys. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum SearchKey<'a> { // // // IMAP doesn't have a dedicated AND operator in its search syntax. // ANDing multiple search keys works by concatenating them with an ascii space. // Introducing this variant makes sense, because // * it may help in understanding the RFC // * and it can be used to distinguish between a single search key // and multiple search keys. // // See also the corresponding `search` parser. And(NonEmptyVec>), /// Messages with message sequence numbers corresponding to the /// specified message sequence number set. SequenceSet(SequenceSet), /// All messages in the mailbox; the default initial key for ANDing. All, /// Messages with the \Answered flag set. Answered, /// Messages that contain the specified string in the envelope /// structure's BCC field. Bcc(AString<'a>), /// Messages whose internal date (disregarding time and timezone) /// is earlier than the specified date. Before(NaiveDate), /// Messages that contain the specified string in the body of the /// message. Body(AString<'a>), /// Messages that contain the specified string in the envelope /// structure's CC field. Cc(AString<'a>), /// Messages with the \Deleted flag set. Deleted, /// Messages with the \Draft flag set. Draft, /// Messages with the \Flagged flag set. Flagged, /// Messages that contain the specified string in the envelope /// structure's FROM field. From(AString<'a>), /// Messages that have a header with the specified field-name (as /// defined in [RFC-2822]) and that contains the specified string /// in the text of the header (what comes after the colon). If the /// string to search is zero-length, this matches all messages that /// have a header line with the specified field-name regardless of /// the contents. Header(AString<'a>, AString<'a>), /// Messages with the specified keyword flag set. Keyword(Atom<'a>), /// Messages with an [RFC-2822] size larger than the specified /// number of octets. Larger(u32), /// Messages that have the \Recent flag set but not the \Seen flag. /// This is functionally equivalent to "(RECENT UNSEEN)". New, /// Messages that do not match the specified search key. Not(Box>), /// Messages that do not have the \Recent flag set. This is /// functionally equivalent to "NOT RECENT" (as opposed to "NOT /// NEW"). Old, /// Messages whose internal date (disregarding time and timezone) /// is within the specified date. On(NaiveDate), /// Messages that match either search key. Or(Box>, Box>), /// Messages that have the \Recent flag set. Recent, /// Messages that have the \Seen flag set. Seen, /// Messages whose [RFC-2822] Date: header (disregarding time and /// timezone) is earlier than the specified date. SentBefore(NaiveDate), /// Messages whose [RFC-2822] Date: header (disregarding time and /// timezone) is within the specified date. SentOn(NaiveDate), /// Messages whose [RFC-2822] Date: header (disregarding time and /// timezone) is within or later than the specified date. SentSince(NaiveDate), /// Messages whose internal date (disregarding time and timezone) /// is within or later than the specified date. Since(NaiveDate), /// Messages with an [RFC-2822] size smaller than the specified /// number of octets. Smaller(u32), /// Messages that contain the specified string in the envelope /// structure's SUBJECT field. Subject(AString<'a>), /// Messages that contain the specified string in the header or /// body of the message. Text(AString<'a>), /// Messages that contain the specified string in the envelope /// structure's TO field. To(AString<'a>), /// Messages with unique identifiers corresponding to the specified /// unique identifier set. Sequence set ranges are permitted. Uid(SequenceSet), /// Messages that do not have the \Answered flag set. Unanswered, /// Messages that do not have the \Deleted flag set. Undeleted, /// Messages that do not have the \Draft flag set. Undraft, /// Messages that do not have the \Flagged flag set. Unflagged, /// Messages that do not have the specified keyword flag set. Unkeyword(Atom<'a>), /// Messages that do not have the \Seen flag set. Unseen, } impl<'a> SearchKey<'a> { pub fn uid(sequence_set: S) -> Self where S: Into, { Self::Uid(sequence_set.into()) } } imap-codec-1.0.0/imap-types/src/secret.rs000066400000000000000000000102421447115025300202240ustar00rootroot00000000000000//! Handling of secret values. //! //! This module provides a `Secret` ensuring that sensitive values are not //! `Debug`-printed by accident. use std::fmt::{Debug, Formatter}; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; /// A wrapper to ensure that secrets are redacted during `Debug`-printing. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] // Note: The implementation of these traits does agree: // `PartialEq` is just a thin wrapper that ensures constant-time comparison. #[allow(clippy::derived_hash_with_manual_eq)] #[derive(Clone, Eq, Hash, PartialEq)] pub struct Secret(T); impl Secret { /// Crate a new secret. pub fn new(inner: T) -> Self { Self(inner) } /// Expose the inner secret. pub fn declassify(&self) -> &T { &self.0 } } impl From for Secret { fn from(value: T) -> Self { Self::new(value) } } impl Debug for Secret where T: Debug, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { #[cfg(not(debug_assertions))] return write!(f, "/* REDACTED */"); #[cfg(debug_assertions)] return self.0.fmt(f); } } #[cfg(test)] mod tests { use crate::{ command::{Command, CommandBody}, core::{AString, Atom, Literal, Quoted}, }; #[test] #[cfg(not(debug_assertions))] #[allow(clippy::redundant_clone)] fn test_that_secret_is_redacted() { use super::Secret; use crate::auth::{AuthMechanism, AuthenticateData}; let secret = Secret("xyz123"); let got = format!("{:?}", secret); println!("{}", got); assert!(!got.contains("xyz123")); println!("-----"); let tests = vec![ CommandBody::login("alice", "xyz123") .unwrap() .tag("A") .unwrap(), CommandBody::authenticate_with_ir(AuthMechanism::Plain, b"xyz123".as_ref()) .tag("A") .unwrap(), ]; for test in tests.into_iter() { let got = format!("{:?}", test); println!("Debug: {}", got); assert!(got.contains("/* REDACTED */")); assert!(!got.contains("xyz123")); assert!(!got.contains("eHl6MTIz")); println!(); } println!("-----"); let tests = [ AuthenticateData(Secret::new(b"xyz123".to_vec())), AuthenticateData(Secret::from(b"xyz123".to_vec())), ]; for test in tests { let got = format!("{:?}", test); println!("Debug: {}", got); assert!(got.contains("/* REDACTED */")); assert!(!got.contains("xyz123")); assert!(!got.contains("eHl6MTIz")); } } #[test] fn test_that_secret_has_no_side_effects_on_eq() { assert_ne!( Command::new( "A", CommandBody::login( AString::from(Atom::try_from("user").unwrap()), AString::from(Atom::try_from("pass").unwrap()), ) .unwrap(), ), Command::new( "A", CommandBody::login( AString::from(Atom::try_from("user").unwrap()), AString::from(Quoted::try_from("pass").unwrap()), ) .unwrap(), ) ); assert_ne!( Command::new( "A", CommandBody::login( Literal::try_from("").unwrap(), Literal::try_from("A").unwrap(), ) .unwrap(), ), Command::new( "A", CommandBody::login( Literal::try_from("").unwrap(), Literal::try_from("A").unwrap().into_non_sync(), ) .unwrap(), ) ); } } imap-codec-1.0.0/imap-types/src/sequence.rs000066400000000000000000000517571447115025300205670ustar00rootroot00000000000000use std::{ num::NonZeroU32, ops::{Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive}, str::FromStr, }; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{ core::NonEmptyVec, error::{ValidationError, ValidationErrorKind}, }; pub const ONE: NonZeroU32 = match NonZeroU32::new(1) { Some(one) => one, None => panic!(), }; pub const MIN: NonZeroU32 = ONE; pub const MAX: NonZeroU32 = match NonZeroU32::new(u32::MAX) { Some(max) => max, None => panic!(), }; #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct SequenceSet(pub NonEmptyVec); impl From for SequenceSet { fn from(sequence: Sequence) -> Self { Self(NonEmptyVec::from(sequence)) } } macro_rules! impl_from_t_for_sequence_set { ($thing:ty) => { impl From<$thing> for SequenceSet { fn from(value: $thing) -> Self { Self::from(Sequence::from(value)) } } }; } macro_rules! impl_try_from_t_for_sequence_set { ($thing:ty) => { impl TryFrom<$thing> for SequenceSet { type Error = ValidationError; fn try_from(value: $thing) -> Result { Ok(Self::from(Sequence::try_from(value)?)) } } }; } impl_from_t_for_sequence_set!(SeqOrUid); impl_from_t_for_sequence_set!(NonZeroU32); impl_from_t_for_sequence_set!(RangeFull); impl_from_t_for_sequence_set!(RangeFrom); impl_try_from_t_for_sequence_set!(RangeTo); impl_from_t_for_sequence_set!(RangeToInclusive); impl_try_from_t_for_sequence_set!(Range); impl_from_t_for_sequence_set!(RangeInclusive); // `SequenceSet::try_from` implementations. impl TryFrom> for SequenceSet { type Error = ValidationError; fn try_from(sequences: Vec) -> Result { Ok(Self(NonEmptyVec::try_from(sequences).map_err(|_| { ValidationError::new(ValidationErrorKind::Empty) })?)) } } impl TryFrom> for SequenceSet { type Error = ValidationError; fn try_from(sequences: Vec) -> Result { Ok(Self( NonEmptyVec::try_from( sequences .into_iter() .map(Sequence::from) .collect::>(), ) .map_err(|_| ValidationError::new(ValidationErrorKind::Empty))?, )) } } impl TryFrom<&str> for SequenceSet { type Error = ValidationError; fn try_from(value: &str) -> Result { value.parse() } } impl FromStr for SequenceSet { type Err = ValidationError; fn from_str(value: &str) -> Result { let mut results = vec![]; for seq in value.split(',') { results.push(Sequence::try_from(seq)?); } Ok(SequenceSet(NonEmptyVec::try_from(results).map_err( |_| ValidationError::new(ValidationErrorKind::Empty), )?)) } } #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Sequence { Single(SeqOrUid), Range(SeqOrUid, SeqOrUid), } impl From for Sequence { fn from(value: SeqOrUid) -> Self { Self::Single(value) } } impl From for Sequence { fn from(value: NonZeroU32) -> Self { Self::Single(SeqOrUid::from(value)) } } impl TryFrom<&str> for Sequence { type Error = ValidationError; fn try_from(value: &str) -> Result { value.parse() } } impl FromStr for Sequence { type Err = ValidationError; fn from_str(value: &str) -> Result { match value.split(':').count() { 0 => Err(ValidationError::new(ValidationErrorKind::Empty)), 1 => Ok(Sequence::Single(SeqOrUid::try_from(value)?)), 2 => { let mut split = value.split(':'); let start = split.next().unwrap(); let end = split.next().unwrap(); Ok(Sequence::Range( SeqOrUid::try_from(start)?, SeqOrUid::try_from(end)?, )) } _ => Err(ValidationError::new(ValidationErrorKind::Invalid)), } } } #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)] pub enum SeqOrUid { Value(NonZeroU32), Asterisk, } impl From for SeqOrUid { fn from(value: NonZeroU32) -> Self { Self::Value(value) } } macro_rules! impl_try_from_num { ($num:ty) => { impl TryFrom<&[$num]> for SequenceSet { type Error = ValidationError; fn try_from(values: &[$num]) -> Result { let mut checked = Vec::new(); for value in values { checked.push(Sequence::try_from(*value)?); } Self::try_from(checked) } } impl TryFrom<$num> for SequenceSet { type Error = ValidationError; fn try_from(value: $num) -> Result { Ok(Self::from(Sequence::try_from(value)?)) } } impl TryFrom<$num> for Sequence { type Error = ValidationError; fn try_from(value: $num) -> Result { Ok(Self::from(SeqOrUid::try_from(value)?)) } } impl TryFrom<$num> for SeqOrUid { type Error = ValidationError; fn try_from(value: $num) -> Result { if let Ok(value) = u32::try_from(value) { if let Ok(value) = NonZeroU32::try_from(value) { return Ok(Self::Value(value)); } } Err(ValidationError::new(ValidationErrorKind::Invalid)) } } }; } impl_try_from_num!(i8); impl_try_from_num!(i16); impl_try_from_num!(i32); impl_try_from_num!(i64); impl_try_from_num!(isize); impl_try_from_num!(u8); impl_try_from_num!(u16); impl_try_from_num!(u32); impl_try_from_num!(u64); impl_try_from_num!(usize); impl TryFrom<&str> for SeqOrUid { type Error = ValidationError; fn try_from(value: &str) -> Result { value.parse() } } impl FromStr for SeqOrUid { type Err = ValidationError; fn from_str(value: &str) -> Result { if value == "*" { Ok(SeqOrUid::Asterisk) } else { // This is to align parsing here with the IMAP grammar: // Rust's `parse::` function accepts numbers that start with 0. // For example, 00001, is interpreted as 1. But this is not allowed in IMAP. if value.starts_with('0') { Err(ValidationError::new(ValidationErrorKind::Invalid)) } else { Ok(SeqOrUid::Value(NonZeroU32::from_str(value).map_err( |_| ValidationError::new(ValidationErrorKind::Invalid), )?)) } } } } // ------------------------------------------------------------------------------------------------- macro_rules! impl_try_from_num_range { ($num:ty) => { impl TryFrom> for SequenceSet { type Error = ValidationError; fn try_from(range: RangeFrom<$num>) -> Result { Ok(Self::from(Sequence::try_from(range)?)) } } impl TryFrom> for SequenceSet { type Error = ValidationError; fn try_from(range: RangeTo<$num>) -> Result { Ok(Self::from(Sequence::try_from(range)?)) } } impl TryFrom> for SequenceSet { type Error = ValidationError; fn try_from(range: RangeToInclusive<$num>) -> Result { Ok(Self::from(Sequence::try_from(range)?)) } } impl TryFrom> for SequenceSet { type Error = ValidationError; fn try_from(range: Range<$num>) -> Result { Ok(Self::from(Sequence::try_from(range)?)) } } impl TryFrom> for SequenceSet { type Error = ValidationError; fn try_from(range: RangeInclusive<$num>) -> Result { Ok(Self::from(Sequence::try_from(range)?)) } } // ----------------------------------------------------------------------------------------- impl TryFrom> for Sequence { type Error = ValidationError; fn try_from(range: RangeFrom<$num>) -> Result { Ok(Self::Range( SeqOrUid::try_from(range.start)?, SeqOrUid::Asterisk, )) } } impl TryFrom> for Sequence { type Error = ValidationError; fn try_from(range: RangeTo<$num>) -> Result { Ok(Self::Range( SeqOrUid::from(ONE), SeqOrUid::try_from(range.end.saturating_sub(1))?, )) } } impl TryFrom> for Sequence { type Error = ValidationError; fn try_from(range: RangeToInclusive<$num>) -> Result { Ok(Self::Range( SeqOrUid::from(ONE), SeqOrUid::try_from(range.end)?, )) } } impl TryFrom> for Sequence { type Error = ValidationError; fn try_from(range: Range<$num>) -> Result { Ok(Self::Range( SeqOrUid::try_from(range.start)?, SeqOrUid::try_from(range.end.saturating_sub(1))?, )) } } impl TryFrom> for Sequence { type Error = ValidationError; fn try_from(range: RangeInclusive<$num>) -> Result { Ok(Self::Range( SeqOrUid::try_from(*range.start())?, SeqOrUid::try_from(*range.end())?, )) } } }; } impl_try_from_num_range!(i8); impl_try_from_num_range!(i16); impl_try_from_num_range!(i32); impl_try_from_num_range!(i64); impl_try_from_num_range!(isize); impl_try_from_num_range!(u8); impl_try_from_num_range!(u16); impl_try_from_num_range!(u32); impl_try_from_num_range!(u64); impl_try_from_num_range!(usize); impl From for Sequence { fn from(_: RangeFull) -> Self { Self::from(MIN..) } } impl From> for Sequence { fn from(range: RangeFrom) -> Self { Self::Range(SeqOrUid::from(range.start), SeqOrUid::Asterisk) } } impl TryFrom> for Sequence { type Error = ValidationError; fn try_from(range: RangeTo) -> Result { Self::try_from(MIN..range.end) } } impl From> for Sequence { fn from(range: RangeToInclusive) -> Self { Self::from(MIN..=range.end) } } impl TryFrom> for Sequence { type Error = ValidationError; fn try_from(range: Range) -> Result { Ok(Self::Range( SeqOrUid::from(MIN), SeqOrUid::try_from(range.end.get().saturating_sub(1))?, )) } } impl From> for Sequence { fn from(range: RangeInclusive) -> Self { Self::Range(SeqOrUid::from(*range.start()), SeqOrUid::from(*range.end())) } } // ------------------------------------------------------------------------------------------------- impl<'a> SequenceSet { pub fn iter(&'a self, strategy: Strategy) -> impl Iterator + 'a { match strategy { Strategy::Naive { largest } => SequenceSetIterNaive { iter: self.0.as_ref().iter(), active_range: None, largest, }, } } } impl SeqOrUid { pub fn expand(&self, largest: NonZeroU32) -> NonZeroU32 { match self { SeqOrUid::Value(value) => *value, SeqOrUid::Asterisk => largest, } } } // ------------------------------------------------------------------------------------------------- #[derive(Debug)] #[non_exhaustive] pub enum Strategy { Naive { largest: NonZeroU32 }, } #[derive(Debug)] pub struct SequenceSetIterNaive<'a> { iter: core::slice::Iter<'a, Sequence>, active_range: Option>, largest: NonZeroU32, } impl<'a> Iterator for SequenceSetIterNaive<'a> { type Item = NonZeroU32; fn next(&mut self) -> Option { loop { if let Some(ref mut range) = self.active_range { if let Some(seq_or_uid) = range.next() { return Some(NonZeroU32::try_from(seq_or_uid).unwrap()); } else { self.active_range = None; } } match self.iter.next() { Some(seq) => match seq { Sequence::Single(seq_no) => { return Some(seq_no.expand(self.largest)); } Sequence::Range(from, to) => { let from = from.expand(self.largest); let to = to.expand(self.largest); self.active_range = Some(u32::from(from)..=u32::from(to)); } }, None => return None, } } } } #[cfg(test)] mod tests { use std::num::NonZeroU32; use super::*; use crate::core::NonEmptyVec; #[test] fn test_creation_of_sequence_from_u32() { assert_eq!( SequenceSet::try_from(1), Ok(SequenceSet(NonEmptyVec::from(Sequence::Single( SeqOrUid::Value(NonZeroU32::new(1).unwrap()) )))) ); assert_eq!( SequenceSet::try_from(0), Err(ValidationError::new(ValidationErrorKind::Invalid)) ); } #[test] fn test_creation_of_sequence_from_range() { // 1:* let range = ..; let seq = Sequence::from(range); assert_eq!( seq, Sequence::Range( SeqOrUid::Value(NonZeroU32::new(1).unwrap()), SeqOrUid::Asterisk ) ); // 1:* let range = 1..; let seq = Sequence::try_from(range).unwrap(); assert_eq!( seq, Sequence::Range( SeqOrUid::Value(NonZeroU32::new(1).unwrap()), SeqOrUid::Asterisk ) ); // 1337:* let range = 1337..; let seq = Sequence::try_from(range).unwrap(); assert_eq!( seq, Sequence::Range( SeqOrUid::Value(NonZeroU32::new(1337).unwrap()), SeqOrUid::Asterisk ) ); // 1:1336 let range = 1..1337; let seq = Sequence::try_from(range).unwrap(); assert_eq!( seq, Sequence::Range( SeqOrUid::Value(NonZeroU32::new(1).unwrap()), SeqOrUid::Value(NonZeroU32::new(1336).unwrap()) ) ); // 1:1337 let range = 1..=1337; let seq = Sequence::try_from(range).unwrap(); assert_eq!( seq, Sequence::Range( SeqOrUid::Value(NonZeroU32::new(1).unwrap()), SeqOrUid::Value(NonZeroU32::new(1337).unwrap()) ) ); // 1:1336 let range = ..1337; let seq = Sequence::try_from(range).unwrap(); assert_eq!( seq, Sequence::Range( SeqOrUid::Value(NonZeroU32::new(1).unwrap()), SeqOrUid::Value(NonZeroU32::new(1336).unwrap()) ) ); // 1:1337 let range = ..=1337; let seq = Sequence::try_from(range).unwrap(); assert_eq!( seq, Sequence::Range( SeqOrUid::Value(NonZeroU32::new(1).unwrap()), SeqOrUid::Value(NonZeroU32::new(1337).unwrap()) ) ); } #[test] fn test_creation_of_sequence_set_from_str_positive() { let tests = &[ ( "1", SequenceSet( vec![Sequence::Single(SeqOrUid::Value(1.try_into().unwrap()))] .try_into() .unwrap(), ), ), ( "1,2,3", SequenceSet( vec![ Sequence::Single(SeqOrUid::Value(1.try_into().unwrap())), Sequence::Single(SeqOrUid::Value(2.try_into().unwrap())), Sequence::Single(SeqOrUid::Value(3.try_into().unwrap())), ] .try_into() .unwrap(), ), ), ( "*", SequenceSet( vec![Sequence::Single(SeqOrUid::Asterisk)] .try_into() .unwrap(), ), ), ( "1:2", SequenceSet( vec![Sequence::Range( SeqOrUid::Value(1.try_into().unwrap()), SeqOrUid::Value(2.try_into().unwrap()), )] .try_into() .unwrap(), ), ), ( "1:2,3", SequenceSet( vec![ Sequence::Range( SeqOrUid::Value(1.try_into().unwrap()), SeqOrUid::Value(2.try_into().unwrap()), ), Sequence::Single(SeqOrUid::Value(3.try_into().unwrap())), ] .try_into() .unwrap(), ), ), ( "1:2,3,*", SequenceSet( vec![ Sequence::Range( SeqOrUid::Value(1.try_into().unwrap()), SeqOrUid::Value(2.try_into().unwrap()), ), Sequence::Single(SeqOrUid::Value(3.try_into().unwrap())), Sequence::Single(SeqOrUid::Asterisk), ] .try_into() .unwrap(), ), ), ]; for (test, expected) in tests.iter() { let got = SequenceSet::try_from(*test).unwrap(); assert_eq!(*expected, got); } } #[test] fn test_creation_of_sequence_set_from_str_negative() { let tests = &[ "", "* ", " *", " * ", "1 ", " 1", " 1 ", "01", " 01", "01 ", " 01 ", "*1", ":", ":*", "*:", "*: ", "1:2:3", ]; for test in tests { let got = SequenceSet::try_from(*test); print!("\"{}\" | {:?} | ", test, got.clone().unwrap_err()); println!("{}", got.unwrap_err()); } } #[test] fn test_iteration_over_some_sequence_sets() { let tests = vec![ ("*", vec![3]), ("1:*", vec![1, 2, 3]), ("5,1:*,2:*", vec![5, 1, 2, 3, 2, 3]), ("*:2", vec![]), ("*:*", vec![3]), ("4:6,*", vec![4, 5, 6, 3]), ] .into_iter() .map(|(raw, vec)| { ( raw, vec.into_iter() .map(|num| num.try_into().unwrap()) .collect::>(), ) }) .collect::)>>(); for (test, expected) in tests { let seq_set = SequenceSet::try_from(test).unwrap(); let got: Vec = seq_set .iter(Strategy::Naive { largest: 3.try_into().unwrap(), }) .collect(); assert_eq!(*expected, got); } } } imap-codec-1.0.0/imap-types/src/state.rs000066400000000000000000000126171447115025300200670ustar00rootroot00000000000000//! IMAP protocol state. //! //! "Once the connection between client and server is established, an IMAP4rev1 connection is in one of four states. //! The initial state is identified in the server greeting. //! Most commands are only valid in certain states. //! It is a protocol error for the client to attempt a command while the connection is in an inappropriate state, //! and the server will respond with a BAD or NO (depending upon server implementation) command completion result." ([RFC 3501](https://www.rfc-editor.org/rfc/rfc3501.html)) //! //! ```text //! +----------------------+ //! |connection established| //! +----------------------+ //! || //! \/ //! +--------------------------------------+ //! | server greeting | //! +--------------------------------------+ //! || (1) || (2) || (3) //! \/ || || //! +-----------------+ || || //! |Not Authenticated| || || //! +-----------------+ || || //! || (7) || (4) || || //! || \/ \/ || //! || +----------------+ || //! || | Authenticated |<=++ || //! || +----------------+ || || //! || || (7) || (5) || (6) || //! || || \/ || || //! || || +--------+ || || //! || || |Selected|==++ || //! || || +--------+ || //! || || || (7) || //! \/ \/ \/ \/ //! +--------------------------------------+ //! | Logout | //! +--------------------------------------+ //! || //! \/ //! +-------------------------------+ //! |both sides close the connection| //! +-------------------------------+ //! //! (1) connection without pre-authentication (OK greeting) //! (2) pre-authenticated connection (PREAUTH greeting) //! (3) rejected connection (BYE greeting) //! (4) successful LOGIN or AUTHENTICATE command //! (5) successful SELECT or EXAMINE command //! (6) CLOSE command, or failed SELECT or EXAMINE command //! (7) LOGOUT command, server shutdown, or connection closed //! ``` #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{core::Tag, mailbox::Mailbox}; /// State of the IMAP4rev1 connection. #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Clone, Debug, Eq, PartialEq)] pub enum State<'a> { Greeting, /// The client MUST supply authentication credentials before most commands will be permitted. /// This state is entered when a connection starts unless the connection has been pre-authenticated. NotAuthenticated, /// The client is authenticated and MUST select a mailbox to access before commands that affect messages will be permitted. /// This state is entered when a pre-authenticated connection starts, when acceptable authentication credentials have been provided, /// after an error in selecting a mailbox, or after a successful CLOSE command. Authenticated, /// A mailbox has been selected to access. /// This state is entered when a mailbox has been successfully selected. Selected(Mailbox<'a>), /// The connection is being terminated. /// This state can be entered as a result of a client request (via the LOGOUT command) or by unilateral action on the part of either the client or server. /// /// If the client requests the logout state, the server MUST send an untagged BYE response and a tagged OK response to the LOGOUT command before the server closes the connection; /// and the client MUST read the tagged OK response to the LOGOUT command before the client closes the connection. /// /// A server MUST NOT unilaterally close the connection without sending an untagged BYE response that contains the reason for having done so. /// A client SHOULD NOT unilaterally close the connection, and instead SHOULD issue a LOGOUT command. /// If the server detects that the client has unilaterally closed the connection, the server MAY omit the untagged BYE response and simply close its connection. Logout, IdleAuthenticated(Tag<'a>), IdleSelected(Tag<'a>, Mailbox<'a>), } #[cfg(test)] mod tests { #[cfg(feature = "bounded-static")] use bounded_static::{IntoBoundedStatic, ToBoundedStatic}; use super::*; use crate::{core::Tag, mailbox::Mailbox}; #[test] fn test_conversion() { let tests = [ State::Greeting, State::NotAuthenticated, State::Authenticated, State::Selected(Mailbox::Inbox), State::Logout, State::IdleAuthenticated(Tag::try_from("A").unwrap()), State::IdleSelected(Tag::try_from("A").unwrap(), Mailbox::Inbox), ]; for _test in tests { #[cfg(feature = "bounded-static")] { let test_to_static = _test.to_static(); assert_eq!(_test, test_to_static); let test_into_static = _test.into_static(); assert_eq!(test_to_static, test_into_static); } } } } imap-codec-1.0.0/imap-types/src/status.rs000066400000000000000000000043421447115025300202660ustar00rootroot00000000000000use std::num::NonZeroU32; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "bounded-static")] use bounded_static::ToStatic; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; /// Status data item name used to request a status data item. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[doc(alias = "StatusAttribute")] pub enum StatusDataItemName { /// The number of messages in the mailbox. Messages, /// The number of messages with the \Recent flag set. Recent, /// The next unique identifier value of the mailbox. UidNext, /// The unique identifier validity value of the mailbox. UidValidity, /// The number of messages which do not have the \Seen flag set. Unseen, /// The number of messages with the \Deleted flag set. Deleted, /// The amount of storage space that can be reclaimed by performing EXPUNGE on the mailbox. DeletedStorage, #[cfg(feature = "ext_condstore_qresync")] #[cfg_attr(docsrs, doc(cfg(feature = "ext_condstore_qresync")))] HighestModSeq, } /// Status data item. #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr(feature = "bounded-static", derive(ToStatic))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[doc(alias = "StatusAttributeValue")] pub enum StatusDataItem { /// The number of messages in the mailbox. Messages(u32), /// The number of messages with the \Recent flag set. Recent(u32), /// The next unique identifier value of the mailbox. Refer to /// section 2.3.1.1 for more information. UidNext(NonZeroU32), /// The unique identifier validity value of the mailbox. Refer to /// section 2.3.1.1 for more information. UidValidity(NonZeroU32), /// The number of messages which do not have the \Seen flag set. Unseen(u32), /// The number of messages with the \Deleted flag set. Deleted(u32), /// The amount of storage space that can be reclaimed by performing EXPUNGE on the mailbox. DeletedStorage(u64), } imap-codec-1.0.0/imap-types/src/utils.rs000066400000000000000000000140231447115025300201000ustar00rootroot00000000000000//! Functions that may come in handy. use std::borrow::Cow; /// Converts bytes into a ready-to-be-printed form. pub fn escape_byte_string(bytes: B) -> String where B: AsRef<[u8]>, { let bytes = bytes.as_ref(); bytes .iter() .map(|byte| match byte { 0x00..=0x08 => format!("\\x{:02x}", byte), 0x09 => String::from("\\t"), 0x0A => String::from("\\n"), 0x0B => format!("\\x{:02x}", byte), 0x0C => format!("\\x{:02x}", byte), 0x0D => String::from("\\r"), 0x0e..=0x1f => format!("\\x{:02x}", byte), 0x20..=0x21 => format!("{}", *byte as char), 0x22 => String::from("\\\""), 0x23..=0x5B => format!("{}", *byte as char), 0x5C => String::from("\\\\"), 0x5D..=0x7E => format!("{}", *byte as char), 0x7f => format!("\\x{:02x}", byte), 0x80..=0xff => format!("\\x{:02x}", byte), }) .collect::>() .join("") } pub mod indicators { /// Any 7-bit US-ASCII character, excluding NUL /// /// CHAR = %x01-7F pub fn is_char(byte: u8) -> bool { matches!(byte, 0x01..=0x7f) } /// Controls /// /// CTL = %x00-1F / %x7F pub fn is_ctl(byte: u8) -> bool { matches!(byte, 0x00..=0x1f | 0x7f) } pub(crate) fn is_any_text_char_except_quoted_specials(byte: u8) -> bool { is_text_char(byte) && !is_quoted_specials(byte) } /// `quoted-specials = DQUOTE / "\"` pub fn is_quoted_specials(byte: u8) -> bool { byte == b'"' || byte == b'\\' } /// `ASTRING-CHAR = ATOM-CHAR / resp-specials` pub fn is_astring_char(i: u8) -> bool { is_atom_char(i) || is_resp_specials(i) } /// `ATOM-CHAR = ` pub fn is_atom_char(b: u8) -> bool { is_char(b) && !is_atom_specials(b) } /// `atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards / quoted-specials / resp-specials` pub fn is_atom_specials(i: u8) -> bool { match i { b'(' | b')' | b'{' | b' ' => true, c if is_ctl(c) => true, c if is_list_wildcards(c) => true, c if is_quoted_specials(c) => true, c if is_resp_specials(c) => true, _ => false, } } /// `list-wildcards = "%" / "*"` pub fn is_list_wildcards(i: u8) -> bool { i == b'%' || i == b'*' } #[inline] /// `resp-specials = "]"` pub fn is_resp_specials(i: u8) -> bool { i == b']' } #[inline] /// `CHAR8 = %x01-ff` /// /// Any OCTET except NUL, %x00 pub fn is_char8(i: u8) -> bool { i != 0 } /// `TEXT-CHAR = %x01-09 / %x0B-0C / %x0E-7F` /// /// Note: This was `` before. pub fn is_text_char(c: u8) -> bool { matches!(c, 0x01..=0x09 | 0x0b..=0x0c | 0x0e..=0x7f) } /// `list-char = ATOM-CHAR / list-wildcards / resp-specials` pub fn is_list_char(i: u8) -> bool { is_atom_char(i) || is_list_wildcards(i) || is_resp_specials(i) } } pub fn escape_quoted(unescaped: &str) -> Cow { let mut escaped = Cow::Borrowed(unescaped); if escaped.contains('\\') { escaped = Cow::Owned(escaped.replace('\\', "\\\\")); } if escaped.contains('\"') { escaped = Cow::Owned(escaped.replace('"', "\\\"")); } escaped } pub fn unescape_quoted(escaped: &str) -> Cow { let mut unescaped = Cow::Borrowed(escaped); if unescaped.contains("\\\\") { unescaped = Cow::Owned(unescaped.replace("\\\\", "\\")); } if unescaped.contains("\\\"") { unescaped = Cow::Owned(unescaped.replace("\\\"", "\"")); } unescaped } #[cfg(test)] mod tests { use super::*; #[test] fn test_escape_quoted() { let tests = [ ("", ""), ("\\", "\\\\"), ("\"", "\\\""), ("alice", "alice"), ("\\alice\\", "\\\\alice\\\\"), ("alice\"", "alice\\\""), (r#"\alice\ ""#, r#"\\alice\\ \""#), ]; for (test, expected) in tests { let got = escape_quoted(test); assert_eq!(expected, got); } } #[test] fn test_unescape_quoted() { let tests = [ ("", ""), ("\\\\", "\\"), ("\\\"", "\""), ("alice", "alice"), ("\\\\alice\\\\", "\\alice\\"), ("alice\\\"", "alice\""), (r#"\\alice\\ \""#, r#"\alice\ ""#), ]; for (test, expected) in tests { let got = unescape_quoted(test); assert_eq!(expected, got); } } #[test] fn test_that_unescape_is_inverse_of_escape() { let input = "\\\"\\¹²³abc_*:;059^$%§!\""; assert_eq!(input, unescape_quoted(escape_quoted(input).as_ref())); } #[test] fn test_escape_byte_string() { for byte in 0u8..=255 { let got = escape_byte_string([byte]); if byte.is_ascii_alphanumeric() { assert_eq!((byte as char).to_string(), got.to_string()); } else if byte.is_ascii_whitespace() { if byte == b'\t' { assert_eq!(String::from("\\t"), got); } else if byte == b'\n' { assert_eq!(String::from("\\n"), got); } } else if byte.is_ascii_punctuation() { if byte == b'\\' { assert_eq!(String::from("\\\\"), got); } else if byte == b'"' { assert_eq!(String::from("\\\""), got); } else { assert_eq!((byte as char).to_string(), got); } } else { assert_eq!(format!("\\x{:02x}", byte), got); } } let tests = [(b"Hallo \"\\\x00", String::from(r#"Hallo \"\\\x00"#))]; for (test, expected) in tests { let got = escape_byte_string(test); assert_eq!(expected, got); } } } imap-codec-1.0.0/imap-types/tests/000077500000000000000000000000001447115025300167455ustar00rootroot00000000000000imap-codec-1.0.0/imap-types/tests/api.rs000066400000000000000000000210701447115025300200640ustar00rootroot00000000000000use std::fmt::{Debug, Display}; use imap_types::{ command::{error::LoginError, Command, CommandBody}, core::{AString, Atom, AtomExt, Charset, IString, Literal, NString, Quoted, Tag, Text}, mailbox::{Mailbox, MailboxOther}, response::Data, sequence::{SeqOrUid, Sequence, SequenceSet, MAX, MIN}, }; macro_rules! test_conversions { // Unvalidated (y, $try_from:tt, $from:tt, $as_ref:tt, $object:ty, $sample:expr) => {{ #[cfg(feature = "unvalidated")] { let object = <$object>::unvalidated($sample); let _ = object.as_ref(); } test_conversions!($try_from, $from, $as_ref, $object, $sample); }}; (n, $try_from:tt, $from:tt, $as_ref:tt, $object:ty, $sample:expr) => {{ test_conversions!($try_from, $from, $as_ref, $object, $sample); }}; // TryFrom (y, $from:tt, $as_ref:tt, $object:ty, $sample:expr) => {{ let _ = <$object>::try_from($sample).unwrap(); let _ = <$object>::try_from($sample.to_owned()).unwrap(); let _ = <$object>::try_from($sample.as_bytes()).unwrap(); let _ = <$object>::try_from($sample.as_bytes().to_vec()).unwrap(); test_conversions!($from, $as_ref, $object, $sample); }}; (n, $from:tt, $as_ref:tt, $object:ty, $sample:expr) => {{ test_conversions!($from, $as_ref, $object, $sample); }}; // From (y, $as_ref:tt, $object:ty, $sample:expr) => {{ let _ = <$object>::from($sample); test_conversions!($as_ref, $object, $sample); }}; (n, $as_ref:tt, $object:ty, $sample:expr) => {{ test_conversions!($as_ref, $object, $sample); }}; // AsRef (y, $object:ty, $sample:expr) => {{ let object = <$object>::try_from($sample).unwrap(); let _ = object.as_ref(); // ... }}; (n, $object:ty, $sample:expr) => {{ // ... }}; } #[test] fn test_constructions() { // Unvalidated | TryFrom | From | AsRef | Type | Sample test_conversions!(y, y, n, y, Tag, "tag"); test_conversions!(y, y, n, y, Text, "text"); // -------------------------------------------- test_conversions!(n, y, n, y, AString, "astring"); test_conversions!(y, y, n, y, Atom, "atom"); test_conversions!(y, y, n, y, AtomExt, "atomext"); test_conversions!(n, y, n, y, IString, "istring"); test_conversions!(y, y, n, y, Quoted, "quoted"); test_conversions!(n, y, n, y, Literal, "literal"); test_conversions!(n, y, n, n, NString, "nstring"); // -------------------------------------------- test_conversions!(n, y, n, n, Mailbox, "mailbox"); test_conversions!(n, y, n, y, MailboxOther, "mailbox"); // -------------------------------------------- test_conversions!(n, y, n, y, Charset, "charset"); } #[test] fn test_construction_of_command() { trait DisplayDebug: Display + Debug {} impl DisplayDebug for T where T: Display + Debug {} match CommandBody::login("\x00", "") { Err(LoginError::Username(e)) => println!("Oops, bad username: {}", e), Err(LoginError::Password(e)) => println!("Oops, bad password: {:?}", e), _ => {} } let tests: Vec> = vec![ Box::new(Command::new(b"".as_ref(), CommandBody::Noop).unwrap_err()), Box::new(Command::new(b"A ".as_ref(), CommandBody::Noop).unwrap_err()), Box::new(Command::new(b"\xff".as_ref(), CommandBody::Noop).unwrap_err()), Box::new("---"), Box::new(Command::new("", CommandBody::Noop).unwrap_err()), Box::new(Command::new("A ", CommandBody::Noop).unwrap_err()), Box::new("---"), Box::new(Command::new(String::from(""), CommandBody::Noop).unwrap_err()), Box::new(Command::new(String::from("A "), CommandBody::Noop).unwrap_err()), Box::new("---"), Box::new(Command::new(Vec::from(b"".as_ref()), CommandBody::Noop).unwrap_err()), Box::new(Command::new(Vec::from(b"\xff".as_ref()), CommandBody::Noop).unwrap_err()), Box::new("---"), Box::new(Atom::try_from("").unwrap_err()), Box::new(Atom::try_from("²").unwrap_err()), Box::new("---"), Box::new(AtomExt::try_from("").unwrap_err()), Box::new(AtomExt::try_from("²").unwrap_err()), Box::new("---"), Box::new(CommandBody::login("\x00", "").unwrap_err()), Box::new(CommandBody::login("", b"\x00".as_ref()).unwrap_err()), Box::new("---"), Box::new(Data::capability(vec![]).unwrap_err()), ]; for test in tests.into_iter() { println!("{test:?} // {test}"); } } #[test] fn test_construction_of_sequence_etc() { // # From // ## SequenceSet let _ = SequenceSet::from(MIN); let _ = SequenceSet::from(MAX); let _ = SequenceSet::from(..); let _ = SequenceSet::from(MIN..); let _ = SequenceSet::try_from(MIN..MAX).unwrap(); let _ = SequenceSet::from(MIN..=MAX); let _ = SequenceSet::try_from(..MAX).unwrap(); let _ = SequenceSet::from(MIN..=MAX); // ## Sequence let _ = Sequence::from(MIN); let _ = Sequence::from(MAX); let _ = Sequence::from(..); let _ = Sequence::from(MIN..); let _ = Sequence::try_from(MIN..MAX).unwrap(); let _ = Sequence::from(MIN..=MAX); let _ = Sequence::try_from(..MAX).unwrap(); let _ = Sequence::from(MIN..=MAX); // ## SeqOrUid let _ = SeqOrUid::from(MIN); let _ = SeqOrUid::from(MAX); macro_rules! try_from { ($min:literal, $max:literal) => { let _ = SequenceSet::try_from($min).unwrap(); let _ = SequenceSet::try_from($max).unwrap(); let _ = SequenceSet::try_from(..).unwrap(); let _ = SequenceSet::try_from($min..).unwrap(); let _ = SequenceSet::try_from($min..$max).unwrap(); let _ = SequenceSet::try_from(..$max).unwrap(); let _ = SequenceSet::try_from($min..$max).unwrap(); let _ = Sequence::try_from($min).unwrap(); let _ = Sequence::try_from($max).unwrap(); let _ = Sequence::try_from(..).unwrap(); let _ = Sequence::try_from($min..).unwrap(); let _ = Sequence::try_from($min..$max).unwrap(); let _ = Sequence::try_from(..$max).unwrap(); let _ = Sequence::try_from($min..$max).unwrap(); let _ = SeqOrUid::try_from($min).unwrap(); let _ = SeqOrUid::try_from($max).unwrap(); }; } try_from!(1i8, 127i8); try_from!(1i16, 32_767i16); try_from!(1i32, 2_147_483_647i32); try_from!(1i64, 2_147_483_647i64); try_from!(1isize, 2_147_483_647isize); try_from!(1u8, 255u8); try_from!(1u16, 65_535u16); try_from!(1u32, 4_294_967_295u32); try_from!(1u64, 4_294_967_295u64); try_from!(1usize, 4_294_967_295usize); macro_rules! try_from_fail_zero { ($min:literal, $max:literal) => { let _ = SequenceSet::try_from($min).unwrap_err(); let _ = SequenceSet::try_from($min..).unwrap_err(); let _ = SequenceSet::try_from($min..$max).unwrap_err(); let _ = SequenceSet::try_from($min..$max).unwrap_err(); let _ = Sequence::try_from($min).unwrap_err(); let _ = Sequence::try_from($min..).unwrap_err(); let _ = Sequence::try_from($min..$max).unwrap_err(); let _ = Sequence::try_from($min..$max).unwrap_err(); let _ = SeqOrUid::try_from($min).unwrap_err(); }; } try_from_fail_zero!(0i8, 127i8); try_from_fail_zero!(0i16, 32_767i16); try_from_fail_zero!(0i32, 2_147_483_647i32); try_from_fail_zero!(0i64, 2_147_483_647i64); try_from_fail_zero!(0isize, 2_147_483_647isize); try_from_fail_zero!(0u8, 255u8); try_from_fail_zero!(0u16, 65_535u16); try_from_fail_zero!(0u32, 4_294_967_295u32); try_from_fail_zero!(0u64, 4_294_967_295u64); try_from_fail_zero!(0usize, 4_294_967_295usize); macro_rules! try_from_fail_max { ($min:literal, $max:literal) => { let _ = SequenceSet::try_from($max).unwrap_err(); let _ = SequenceSet::try_from($min..$max).unwrap_err(); let _ = SequenceSet::try_from(..$max).unwrap_err(); let _ = SequenceSet::try_from($min..$max).unwrap_err(); let _ = Sequence::try_from($max).unwrap_err(); let _ = Sequence::try_from($min..$max).unwrap_err(); let _ = Sequence::try_from(..$max).unwrap_err(); let _ = Sequence::try_from($min..$max).unwrap_err(); let _ = SeqOrUid::try_from($max).unwrap_err(); }; } try_from_fail_max!(1i64, 9_223_372_036_854_775_807i64); try_from_fail_max!(1u64, 18_446_744_073_709_551_615u64); } imap-codec-1.0.0/imap-types/tests/readme.rs000066400000000000000000000020421447115025300205460ustar00rootroot00000000000000use imap_types::{ command::{Command, CommandBody}, core::Literal, }; #[cfg(feature = "unvalidated")] use imap_types::{ core::{AString, Atom, Tag}, secret::Secret, }; #[test] fn test_readme() { Command::new("A1", CommandBody::login("alice", "password").unwrap()).unwrap(); Command::new( "A1", CommandBody::login("alice\"", b"\xCA\xFE".as_ref()).unwrap(), ) .unwrap(); Command::new( "A1", CommandBody::login(Literal::try_from("alice").unwrap(), "password").unwrap(), ) .unwrap(); #[cfg(feature = "unvalidated")] { let tag = Tag::try_from("A1").unwrap(); let _ = Command { tag, body: CommandBody::Login { username: AString::from(Atom::unvalidated("alice")), password: Secret::new(AString::from(Atom::unvalidated("password"))), }, }; } } #[test] #[should_panic] fn test_readme_failing() { Command::new("A1", CommandBody::login("alice\x00", "password").unwrap()).unwrap(); } imap-codec-1.0.0/rustfmt.toml000066400000000000000000000001361447115025300161140ustar00rootroot00000000000000format_code_in_doc_comments=true group_imports="StdExternalCrate" imports_granularity="Crate"