pax_global_header00006660000000000000000000000064147241353530014521gustar00rootroot0000000000000052 comment=7bd8e3fd7cad0afab15b41944184e6523d80f23f imap-next-0.3.1/000077500000000000000000000000001472413535300134245ustar00rootroot00000000000000imap-next-0.3.1/.github/000077500000000000000000000000001472413535300147645ustar00rootroot00000000000000imap-next-0.3.1/.github/FUNDING.yml000066400000000000000000000000211472413535300165720ustar00rootroot00000000000000github: [duesee] imap-next-0.3.1/.github/actions/000077500000000000000000000000001472413535300164245ustar00rootroot00000000000000imap-next-0.3.1/.github/actions/cache_restore/000077500000000000000000000000001472413535300212325ustar00rootroot00000000000000imap-next-0.3.1/.github/actions/cache_restore/action.yml000066400000000000000000000012271472413535300232340ustar00rootroot00000000000000name: cache_restore runs: using: composite steps: - uses: actions/cache/restore@v4 with: path: | # See https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci ~/.cargo/.crates.toml ~/.cargo/.crates2.json ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ # See https://doc.rust-lang.org/cargo/guide/build-cache.html target key: ${{ runner.os }}|${{ github.job }}|${{ github.run_attempt }} restore-keys: | ${{ runner.os }}|${{ github.job }} ${{ runner.os }} imap-next-0.3.1/.github/actions/cache_save/000077500000000000000000000000001472413535300205055ustar00rootroot00000000000000imap-next-0.3.1/.github/actions/cache_save/action.yml000066400000000000000000000010611472413535300225030ustar00rootroot00000000000000name: cache_save runs: using: composite steps: - uses: actions/cache/save@v4 with: path: | # See https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci ~/.cargo/.crates.toml ~/.cargo/.crates2.json ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ # See https://doc.rust-lang.org/cargo/guide/build-cache.html target key: ${{ runner.os }}|${{ github.job }}|${{ github.run_attempt }} imap-next-0.3.1/.github/dependabot.yml000066400000000000000000000004211472413535300176110ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" groups: dependencies: patterns: - "*" imap-next-0.3.1/.github/workflows/000077500000000000000000000000001472413535300170215ustar00rootroot00000000000000imap-next-0.3.1/.github/workflows/audit.yml000066400000000000000000000010261472413535300206510ustar00rootroot00000000000000name: audit on: push: branches: [ main ] pull_request: branches: [ main ] schedule: # 21:43 on Wednesday and Sunday. (Thanks, crontab.guru) - cron: '43 21 * * 3,0' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/cache_restore - run: cargo install just - run: just audit - uses: ./.github/actions/cache_save imap-next-0.3.1/.github/workflows/main.yml000066400000000000000000000043341472413535300204740ustar00rootroot00000000000000name: main on: push: branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/cache_restore - run: cargo install just - run: just check - uses: ./.github/actions/cache_save test: strategy: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Setup | Install NASM (Windows) uses: ilammy/setup-nasm@v1 if: matrix.os == 'windows-latest' - uses: ./.github/actions/cache_restore - run: cargo install just - run: just test - uses: ./.github/actions/cache_save # benchmark: # runs-on: ubuntu-latest # steps: # - uses: actions/checkout@v4 # - uses: ./.github/actions/cache_restore # - run: cargo install just # - run: just bench_against_main # - uses: ./.github/actions/cache_save coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/cache_restore - run: cargo install just - run: just coverage - uses: ./.github/actions/cache_save - uses: coverallsapp/github-action@cfd0633edbd2411b532b808ba7a8b5e04f76d2c8 with: format: lcov file: target/coverage/coverage.lcov # fuzz: # runs-on: ubuntu-latest # steps: # - uses: actions/checkout@v4 # - uses: ./.github/actions/cache_restore # - run: cargo install just # - run: just fuzz # - uses: ./.github/actions/cache_save check_msrv: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/cache_restore - run: cargo install just - run: just check_msrv - uses: ./.github/actions/cache_save check_minimal_dependency_versions: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/cache_restore - run: cargo install just - run: just check_minimal_dependency_versions - uses: ./.github/actions/cache_save imap-next-0.3.1/.github/workflows/release.yml000066400000000000000000000014671472413535300211740ustar00rootroot00000000000000name: release on: push: tags: - 'v*' jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Assert release version matches crate version run: | set -euo pipefail # Get release version from Git tag tag_version=${GITHUB_REF#refs/tags/v} # Get crate version from Cargo.toml crate_version=$(cargo read-manifest | jq -r .version) if [ "$tag_version" != "$crate_version" ]; then echo "Error: Release version ${tag_version} from Git tag does not match crate version ${crate_version} from Cargo.toml" exit 1 fi - name: Publish crate to crates.io env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: cargo publish imap-next-0.3.1/.gitignore000066400000000000000000000001401472413535300154070ustar00rootroot00000000000000/target # IntelliJ, CLion, RustRover, ... .idea # direnv (https://direnv.net/) .envrc .direnv imap-next-0.3.1/CHANGELOG.md000066400000000000000000000023471472413535300152430ustar00rootroot00000000000000# 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). ## [Unreleased] - YYYY-MM-DD ### Added * Created README, CHANGELOG, badges, rustfmt.toml, ... * Created project board * Setup CI: Check, Build, Lint, Audit, Coverage, ... * Licensed everything as "APACHE OR MIT" * `imap-next` * Implemented literal handling, handles, events, and examples * Implemented AUTHENTICATE and IDLE * Implemented a self-test, and tested against a few providers * `proxy` * Implemented argument processing and configuration * Smoke tested against a few providers (and a few MUAs) * Provided a README * Supported capabilities are ... * AUTH={PLAIN,LOGIN,XOAUTH2,ScramSha1,ScramSha256} * SASL-IR * QUOTA* * MOVE * LITERAL+/LITERAL- * UNSELECT * ID * IDLE * Use ALPN==imap * `imap-tasks` prototype * Designed `Task`s trait * Implemented `Task` for a few commands * Implemented a task scheduler/manager * `tag-generator` [Unreleased]: https://github.com/duesee/imap-next/compare/0a89b5e180ad7dfd3d67d1184370fa1028ea92b4...HEAD imap-next-0.3.1/Cargo.lock000066400000000000000000000550101472413535300153320ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "abnf-core" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec182d1f071b906a9f59269c89af101515a5cbe58f723eb6717e7fe7445c0dea" dependencies = [ "nom", ] [[package]] name = "addr2line" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "arbitrary" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" dependencies = [ "derive_arbitrary", ] [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bounded-static" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0beb903daa49b43bcafb5d5eebe633f9ad638d8b16cd08f95fb05ee7bd099321" [[package]] name = "bounded-static-derive" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0af050e27e5d57aa14975f97fe47a134c46a390f91819f23a625319a7111bfa" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "bstr" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" dependencies = [ "memchr", ] [[package]] name = "bytes" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cc" version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "arbitrary", "num-traits", "serde", ] [[package]] name = "derive_arbitrary" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "imap-codec" version = "2.0.0-alpha.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f584310addd1fb8fe288e4f07c279fec9264ac1ea68b018241ae4dcd4fb28557" dependencies = [ "abnf-core", "base64", "chrono", "imap-types", "log", "nom", ] [[package]] name = "imap-next" version = "0.3.1" dependencies = [ "bytes", "imap-codec", "imap-next", "rand", "thiserror 2.0.3", "tokio", "tokio-rustls", "tracing", ] [[package]] name = "imap-types" version = "2.0.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d601d81f11962a649acc2d535ad7311770e30364b4a978a762de291829c9ef53" dependencies = [ "arbitrary", "base64", "bounded-static", "bounded-static-derive", "chrono", "rand", "serde", "thiserror 1.0.69", ] [[package]] name = "integration-test" version = "0.1.0" dependencies = [ "bstr", "bytes", "imap-codec", "imap-next", "lazy_static", "tokio", "tracing", "tracing-subscriber", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "matchers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ "regex-automata 0.1.10", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "mio" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ "hermit-abi", "libc", "wasi", "windows-sys", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", "winapi", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "object" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets", ] [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.7", "regex-syntax 0.8.4", ] [[package]] name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ "regex-syntax 0.6.29", ] [[package]] name = "regex-automata" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", "regex-syntax 0.8.4", ] [[package]] name = "regex-syntax" version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "ring" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", "getrandom", "libc", "spin", "untrusted", "windows-sys", ] [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustls" version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ "once_cell", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pki-types" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" version = "0.102.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" dependencies = [ "thiserror-impl 2.0.3", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thiserror-impl" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thread_local" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "tokio" version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys", ] [[package]] name = "tokio-macros" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-rustls" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ "rustls", "rustls-pki-types", "tokio", ] [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" imap-next-0.3.1/Cargo.toml000066400000000000000000000030561472413535300153600ustar00rootroot00000000000000[package] name = "imap-next" description = "Thin sans I/O abstraction over IMAP's distinct protocol flows" keywords = ["email", "imap", "protocol", "network"] categories = ["email", "network-programming"] version = "0.3.1" repository = "https://github.com/duesee/imap-next" edition = "2021" license = "MIT OR Apache-2.0" exclude = [".github"] [features] default = ["stream"] expose_stream = [] stream = ["dep:bytes", "dep:tokio", "dep:tokio-rustls"] # arbitrary = ["imap-codec/arbitrary"] arbitrary_simplified = ["imap-codec/arbitrary_simplified"] serde = ["imap-codec/serde"] tag_generator = ["imap-codec/tag_generator"] # IMAP starttls = ["imap-codec/starttls"] ext_condstore_qresync = ["imap-codec/ext_condstore_qresync"] ext_id = ["imap-codec/ext_id"] ext_login_referrals = ["imap-codec/ext_login_referrals"] ext_mailbox_referrals = ["imap-codec/ext_mailbox_referrals"] ext_metadata = ["imap-codec/ext_metadata"] # [dependencies] bytes = { version = "1.8.0", optional = true } imap-codec = { version = "2.0.0-alpha.5", features = ["quirk_crlf_relaxed"] } thiserror = "2.0.3" tokio = { version = "1.41.1", optional = true, features = ["io-util", "macros", "net"] } tokio-rustls = { version = "0.26.0", optional = true, default-features = false } tracing = "0.1.40" [dev-dependencies] # We want to enable `tag_generator` for examples. imap-next = { path = ".", features = ["tag_generator"] } rand = "0.8.5" tokio = { version = "1.41.1", features = ["full"] } [workspace] resolver = "2" members = [ "integration-test", ] imap-next-0.3.1/LICENSE-APACHE000066400000000000000000000261351472413535300153570ustar00rootroot00000000000000 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-next-0.3.1/LICENSE-MIT000066400000000000000000000020621472413535300150600ustar00rootroot00000000000000MIT 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-next-0.3.1/README.md000066400000000000000000000073561472413535300147160ustar00rootroot00000000000000[![main](https://github.com/duesee/imap-next/actions/workflows/main.yml/badge.svg)](https://github.com/duesee/imap-next/actions/workflows/main.yml) [![audit](https://github.com/duesee/imap-next/actions/workflows/audit.yml/badge.svg)](https://github.com/duesee/imap-next/actions/workflows/audit.yml) [![Coverage](https://coveralls.io/repos/github/duesee/imap-next/badge.svg?branch=main)](https://coveralls.io/github/duesee/imap-next?branch=main) # imap-next 𓅟 ```mermaid %%{init: {'theme': 'neutral' } }%% flowchart LR imap-types --> imap-codec imap-codec --> imap-next imap-next -.-> imap-proxy imap-next -.-> imap-client style imap-codec stroke-dasharray: 10 5 style imap-next stroke-width:4px click imap-types href "https://github.com/duesee/imap-codec/tree/main/imap-types" click imap-codec href "https://github.com/duesee/imap-codec" click imap-next href "https://github.com/duesee/imap-next" click imap-proxy href "https://github.com/duesee/imap-proxy" click imap-client href "https://github.com/soywod/imap-client" ``` `imap-next` is a thin sans I/O abstraction over IMAP's distinct protocol flows. These are literal handling, AUTHENTICATE, and IDLE. The way these protocol flows were defined in IMAP couples networking, parsing, and business logic. `imap-next` untangles them, providing a minimal interface allowing sending and receiving coherent messages. It's a thin layer paving the ground for higher-level client or server implementations. And it's sans I/O enabling the integration in any existing I/O runtime. ## Lower-level Libraries `imap-next` uses [`imap-codec`](https://github.com/duesee/imap-codec) internally for parsing and serialization, and re-exposes [`imap-types`](https://github.com/duesee/imap-codec/tree/main/imap-types). ## Higher-level Libraries * [`imap-proxy`](https://github.com/duesee/imap-proxy) is an IMAP proxy that gracefully forwards unsolicited responses, abstracts over literals, and `Debug`-prints messages. * [`imap-client`](https://github.com/soywod/imap-client) is a methods-based client library with a `client.capability()`, `client.login()`, ... interface. ## Usage ```rust,no_run use std::error::Error; use imap_next::{ client::{Client, Event, Options}, imap_types::{ command::{Command, CommandBody}, core::Tag, }, stream::Stream, }; use tokio::net::TcpStream; #[tokio::main] async fn main() -> Result<(), Box> { let mut stream = Stream::insecure(TcpStream::connect("127.0.0.1:1143").await?); let mut client = Client::new(Options::default()); loop { match stream.next(&mut client).await? { event => { println!("{event:?}"); if matches!(event, Event::GreetingReceived { .. }) { break; } } } } let handle = client.enqueue_command(Command::new("A1", CommandBody::login("Al¹cE", "pa²²w0rd")?)?); loop { match stream.next(&mut client).await? { event => println!("{event:?}"), } } } ``` # 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-next` through their [NGI Assure](https://nlnet.nl/assure/) program!
NLnet logo Whitespace NGI Assure logo
imap-next-0.3.1/deny.toml000066400000000000000000000004401472413535300152560ustar00rootroot00000000000000[sources] unknown-registry = "deny" unknown-git = "deny" [licenses] allow = [ "Apache-2.0", "BSD-3-Clause", "MIT", "Unicode-DFS-2016", "ISC" ] [[licenses.clarify]] name = "ring" expression = "MIT AND ISC AND OpenSSL" license-files = [ { path = "LICENSE", hash = 0xbd0eed23 } ] imap-next-0.3.1/examples/000077500000000000000000000000001472413535300152425ustar00rootroot00000000000000imap-next-0.3.1/examples/client.rs000066400000000000000000000037441472413535300170760ustar00rootroot00000000000000use imap_next::{ client::{Client, Event, Options}, imap_types::{ command::{Command, CommandBody}, core::Tag, }, stream::Stream, }; use tokio::net::TcpStream; #[tokio::main(flavor = "current_thread")] async fn main() { let stream = TcpStream::connect("127.0.0.1:12345").await.unwrap(); let mut stream = Stream::insecure(stream); let mut client = Client::new(Options::default()); let greeting = loop { match stream.next(&mut client).await.unwrap() { Event::GreetingReceived { greeting } => break greeting, event => println!("unexpected event: {event:?}"), } }; println!("received greeting: {greeting:?}"); let handle = client.enqueue_command(Command { tag: Tag::try_from("A1").unwrap(), body: CommandBody::login("Al¹cE", "pa²²w0rd").unwrap(), }); loop { match stream.next(&mut client).await.unwrap() { Event::CommandSent { handle: got_handle, command, } => { println!("command sent: {got_handle:?}, {command:?}"); assert_eq!(handle, got_handle); } Event::CommandRejected { handle: got_handle, command, status, } => { println!("command rejected: {got_handle:?}, {command:?}, {status:?}"); assert_eq!(handle, got_handle); } Event::DataReceived { data } => { println!("data received: {data:?}"); } Event::StatusReceived { status } => { println!("status received: {status:?}"); } Event::ContinuationRequestReceived { continuation_request, } => { println!("unexpected continuation request received: {continuation_request:?}"); } event => { println!("{event:?}"); } } } } imap-next-0.3.1/examples/client_authenticate.rs000066400000000000000000000034031472413535300216240ustar00rootroot00000000000000use std::collections::VecDeque; use imap_next::{ client::{Client, Event, Options}, imap_types::{ auth::{AuthMechanism, AuthenticateData}, command::{Command, CommandBody}, core::TagGenerator, }, stream::Stream, }; use tokio::net::TcpStream; #[tokio::main(flavor = "current_thread")] async fn main() { let stream = TcpStream::connect("127.0.0.1:12345").await.unwrap(); let mut stream = Stream::insecure(stream); let mut client = Client::new(Options::default()); loop { match stream.next(&mut client).await.unwrap() { Event::GreetingReceived { .. } => break, event => println!("unexpected event: {event:?}"), } } let mut tag_generator = TagGenerator::new(); let tag = tag_generator.generate(); client.enqueue_command(Command { tag: tag.clone(), body: CommandBody::authenticate(AuthMechanism::Login), }); let mut authenticate_data = VecDeque::from([ AuthenticateData::r#continue(b"alice".to_vec()), AuthenticateData::r#continue(b"password".to_vec()), ]); loop { let event = stream.next(&mut client).await.unwrap(); println!("{event:?}"); match event { Event::AuthenticateContinuationRequestReceived { .. } => { if let Some(authenticate_data) = authenticate_data.pop_front() { client.set_authenticate_data(authenticate_data).unwrap(); } else { client .set_authenticate_data(AuthenticateData::Cancel) .unwrap(); } } Event::AuthenticateStatusReceived { .. } => { break; } _ => {} } } } imap-next-0.3.1/examples/client_idle.rs000066400000000000000000000065251472413535300200730ustar00rootroot00000000000000use std::io::BufRead; use imap_next::{ client::{Client, Event, Options}, imap_types::{ command::{Command, CommandBody}, core::Tag, response::{Status, Tagged}, }, stream::Stream, }; use tokio::{net::TcpStream, sync::mpsc::Receiver}; #[tokio::main(flavor = "current_thread")] async fn main() { let stream = TcpStream::connect("127.0.0.1:12345").await.unwrap(); let mut stream = Stream::insecure(stream); let mut client = Client::new(Options::default()); loop { match stream.next(&mut client).await.unwrap() { Event::GreetingReceived { .. } => break, event => println!("unexpected event: {event:?}"), } } println!("Press ENTER to stop IDLE"); let mut lines = Lines::new(); let tag = Tag::unvalidated("A1"); let _handle = client.enqueue_command(Command { tag: tag.clone(), body: CommandBody::Idle, }); loop { tokio::select! { event = stream.next(&mut client) => { match event.unwrap() { Event::IdleCommandSent { .. } => { println!("IDLE command sent") }, Event::IdleAccepted { continuation_request, .. } => { println!("IDLE accepted: {continuation_request:?}"); }, Event::IdleRejected { status, .. } => { println!("IDLE rejected: {status:?}"); break; }, Event::IdleDoneSent { .. } => { println!("IDLE DONE sent"); break; }, Event::DataReceived { data } => { println!("Data received: {data:?}") }, Event::StatusReceived { status } => { println!("Status received: {status:?}") }, event => { println!("Unknown event received: {event:?}"); } } } _ = lines.next() => { if client.set_idle_done().is_some() { println!("Triggered IDLE DONE"); } else { println!("Can't trigger IDLE DONE now"); } } } } loop { match stream.next(&mut client).await.unwrap() { ref event @ Event::StatusReceived { status: Status::Tagged(Tagged { tag: ref got_tag, .. }), } if *got_tag == tag => { println!("Status for IDLE received: {event:?}"); break; } event => { println!("Unknown event received: {event:?}"); } } } } struct Lines { receiver: Receiver, } impl Lines { pub fn new() -> Self { let (sender, receiver) = tokio::sync::mpsc::channel(1); tokio::task::spawn_blocking(move || loop { for line in std::io::stdin().lock().lines() { sender.blocking_send(line.unwrap()).unwrap(); } }); Self { receiver } } pub async fn next(&mut self) -> String { self.receiver.recv().await.unwrap() } } imap-next-0.3.1/examples/client_std.rs000066400000000000000000000057471472413535300177550ustar00rootroot00000000000000use std::{ io::{Read, Write}, net::TcpStream, }; use imap_next::{ client::{Client, Event, Options}, imap_types::{ command::{Command, CommandBody}, core::Tag, }, Interrupt, Io, State, }; fn main() { let mut stream = TcpStream::connect("127.0.0.1:12345").unwrap(); let mut read_buffer = [0; 128]; let mut client = Client::new(Options::default()); let greeting = loop { match client.next() { Err(interrupt) => match interrupt { Interrupt::Io(Io::NeedMoreInput) => { let count = stream.read(&mut read_buffer).unwrap(); client.enqueue_input(&read_buffer[0..count]); } interrupt => panic!("unexpected interrupt: {interrupt:?}"), }, Ok(event) => match event { Event::GreetingReceived { greeting } => break greeting, event => println!("unexpected event: {event:?}"), }, } }; println!("received greeting: {greeting:?}"); let handle = client.enqueue_command(Command { tag: Tag::try_from("A1").unwrap(), body: CommandBody::login("Al¹cE", "pa²²w0rd").unwrap(), }); loop { match client.next() { Err(interrupt) => match interrupt { Interrupt::Io(Io::NeedMoreInput) => { let count = stream.read(&mut read_buffer).unwrap(); client.enqueue_input(&read_buffer[0..count]); } Interrupt::Io(Io::Output(bytes)) => { stream.write_all(&bytes).unwrap(); } Interrupt::Error(error) => { panic!("unexpected error: {error:?}"); } }, Ok(event) => match event { Event::CommandSent { handle: got_handle, command, } => { println!("command sent: {got_handle:?}, {command:?}"); assert_eq!(handle, got_handle); } Event::CommandRejected { handle: got_handle, command, status, } => { println!("command rejected: {got_handle:?}, {command:?}, {status:?}"); assert_eq!(handle, got_handle); } Event::DataReceived { data } => { println!("data received: {data:?}"); } Event::StatusReceived { status } => { println!("status received: {status:?}"); } Event::ContinuationRequestReceived { continuation_request, } => { println!("unexpected continuation request received: {continuation_request:?}"); } event => { println!("{event:?}"); } }, } } } imap-next-0.3.1/examples/server.rs000066400000000000000000000030401472413535300171130ustar00rootroot00000000000000use std::collections::VecDeque; use imap_next::{ imap_types::response::{Greeting, Status}, server::{Event, Options, Server}, stream::Stream, }; use tokio::net::TcpListener; #[tokio::main(flavor = "current_thread")] async fn main() { let listener = TcpListener::bind("127.0.0.1:12345").await.unwrap(); let (stream, _) = listener.accept().await.unwrap(); let mut stream = Stream::insecure(stream); let mut server = Server::new( Options::default(), Greeting::ok(None, "server (example)").unwrap(), ); loop { match stream.next(&mut server).await.unwrap() { Event::GreetingSent { greeting } => { println!("greeting sent: {greeting:?}"); break; } event => println!("unexpected event: {event:?}"), } } let mut handles = VecDeque::new(); loop { match stream.next(&mut server).await.unwrap() { Event::CommandReceived { command } => { println!("command received: {command:?}"); handles.push_back( server.enqueue_status(Status::no(Some(command.tag), None, "...").unwrap()), ); } Event::ResponseSent { handle: got_handle, response, } => { println!("response sent: {response:?}"); assert_eq!(handles.pop_front(), Some(got_handle)); } event => { println!("{event:?}"); } } } } imap-next-0.3.1/examples/server_authenticate.rs000066400000000000000000000052411472413535300216560ustar00rootroot00000000000000use imap_next::{ imap_types::response::{CommandContinuationRequest, Greeting, Status}, server::{Event, Options, Server}, stream::Stream, types::CommandAuthenticate, }; use tokio::net::TcpListener; #[tokio::main(flavor = "current_thread")] async fn main() { let listener = TcpListener::bind("127.0.0.1:12345").await.unwrap(); let (stream, _) = listener.accept().await.unwrap(); let mut stream = Stream::insecure(stream); let mut server = Server::new( Options::default(), Greeting::ok(None, "server_idle (example)").unwrap(), ); loop { match stream.next(&mut server).await.unwrap() { Event::GreetingSent { .. } => break, event => println!("unexpected event: {event:?}"), } } let mut current_authenticate_tag = None; loop { let event = stream.next(&mut server).await.unwrap(); println!("{event:?}"); // We don't implement any real SASL mechanism in this example. let pretend_to_need_more_data = rand::random(); match event { Event::CommandAuthenticateReceived { command_authenticate: CommandAuthenticate { tag, .. }, } => { if pretend_to_need_more_data { server .authenticate_continue( CommandContinuationRequest::basic(None, "I need more data...").unwrap(), ) .unwrap(); current_authenticate_tag = Some(tag); } else { server .authenticate_finish( Status::ok(Some(tag), None, "Thanks, that's already enough!").unwrap(), ) .unwrap(); } } Event::AuthenticateDataReceived { .. } => { if pretend_to_need_more_data { server .authenticate_continue( CommandContinuationRequest::basic(None, "...more...").unwrap(), ) .unwrap(); } else { let tag = current_authenticate_tag.take().unwrap(); server .authenticate_finish(Status::ok(Some(tag), None, "Thanks!").unwrap()) .unwrap(); } } Event::CommandReceived { command } => { server.enqueue_status( Status::no(Some(command.tag), None, "Please use AUTHENTICATE").unwrap(), ); } _ => {} } } } imap-next-0.3.1/examples/server_idle.rs000066400000000000000000000104071472413535300201150ustar00rootroot00000000000000use std::{io::BufRead, num::NonZeroU32}; use imap_next::{ imap_types::{ core::Text, response::{ CommandContinuationRequest, Data, Greeting, Status, StatusBody, StatusKind, Tagged, }, }, server::{Event, Options, Server}, stream::Stream, }; use tokio::{net::TcpListener, sync::mpsc::Receiver}; #[tokio::main(flavor = "current_thread")] async fn main() { let listener = TcpListener::bind("127.0.0.1:12345").await.unwrap(); let (stream, _) = listener.accept().await.unwrap(); let mut stream = Stream::insecure(stream); let mut server = Server::new( Options::default(), Greeting::ok(None, "server_idle (example)").unwrap(), ); loop { match stream.next(&mut server).await.unwrap() { Event::GreetingSent { .. } => break, event => println!("unexpected event: {event:?}"), } } println!("Please enter 'ok', 'no', or 'expunge'"); let mut lines = Lines::new(); let mut current_idle_tag = None; loop { tokio::select! { event = stream.next(&mut server) => { match event.unwrap() { Event::IdleCommandReceived { tag } => { println!("IDLE received"); current_idle_tag = Some(tag); }, Event::IdleDoneReceived => { println!("IDLE DONE received"); if let Some(tag) = current_idle_tag.take() { let status = Status::Tagged(Tagged { tag, body: StatusBody { kind: StatusKind::Ok, code: None, text: Text::try_from("...").unwrap() }, }); server.enqueue_status(status); } }, event => { println!("Event received: {event:?}"); } } } line = lines.next() => { match line.as_ref() { "ok" => { let cont = CommandContinuationRequest::basic(None, "...").unwrap(); if server.idle_accept(cont).is_ok() { println!("IDLE accepted"); } else { println!("IDLE can't be accepted now"); } } "no" => { let Some(tag) = current_idle_tag.clone() else { println!("IDLE can't be rejected now"); continue; }; let status = Status::Tagged(Tagged { tag, body: StatusBody { kind: StatusKind::No, code: None, text: Text::try_from("...").unwrap() }, }); if server.idle_reject(status).is_ok() { println!("IDLE rejected"); } else { println!("IDLE can't be rejected now"); } } "expunge" => { let data = Data::Expunge(NonZeroU32::new(1).unwrap()); server.enqueue_data(data); println!("Send EXPUNGE"); } _ => println!("Please enter 'ok', 'no', or 'expunge'"), } } } } } struct Lines { receiver: Receiver, } impl Lines { pub fn new() -> Self { let (sender, receiver) = tokio::sync::mpsc::channel(1); tokio::task::spawn_blocking(move || loop { for line in std::io::stdin().lock().lines() { sender.blocking_send(line.unwrap()).unwrap(); } }); Self { receiver } } pub async fn next(&mut self) -> String { self.receiver.recv().await.unwrap() } } imap-next-0.3.1/flake.lock000066400000000000000000000040651472413535300153650ustar00rootroot00000000000000{ "nodes": { "fenix": { "inputs": { "nixpkgs": [ "nixpkgs" ], "rust-analyzer-src": "rust-analyzer-src" }, "locked": { "lastModified": 1715063087, "narHash": "sha256-cktPkcCmJ2sR0V/FaWEuCWmKuGPbwoMltih/EfF0mXg=", "owner": "nix-community", "repo": "fenix", "rev": "f8f16c1f2c83bea4e51e6522d988ec8bfcc8420e", "type": "github" }, "original": { "owner": "nix-community", "repo": "fenix", "type": "github" } }, "flake-compat": { "flake": false, "locked": { "lastModified": 1696426674, "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", "owner": "edolstra", "repo": "flake-compat", "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", "type": "github" }, "original": { "owner": "edolstra", "repo": "flake-compat", "type": "github" } }, "nixpkgs": { "locked": { "lastModified": 1714971268, "narHash": "sha256-IKwMSwHj9+ec660l+I4tki/1NRoeGpyA2GdtdYpAgEw=", "owner": "nixos", "repo": "nixpkgs", "rev": "27c13997bf450a01219899f5a83bd6ffbfc70d3c", "type": "github" }, "original": { "owner": "nixos", "ref": "nixos-23.11", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "fenix": "fenix", "flake-compat": "flake-compat", "nixpkgs": "nixpkgs" } }, "rust-analyzer-src": { "flake": false, "locked": { "lastModified": 1714936835, "narHash": "sha256-M+PpgfRMBfHo8Jb2ou/s3maAZbps0XnuHXQU9Hv9vL0=", "owner": "rust-lang", "repo": "rust-analyzer", "rev": "c4618fe14d39992fbbb85c2d6cad028a232c13d2", "type": "github" }, "original": { "owner": "rust-lang", "ref": "nightly", "repo": "rust-analyzer", "type": "github" } } }, "root": "root", "version": 7 } imap-next-0.3.1/flake.nix000066400000000000000000000024221472413535300152260ustar00rootroot00000000000000{ inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11"; # The rustup equivalent for Nix. fenix = { url = "github:nix-community/fenix"; inputs.nixpkgs.follows = "nixpkgs"; }; # Allows non-flakes users to still be able to `nix-shell` based on # `shell.nix` instead of this `flake.nix`. flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; }; outputs = { self, nixpkgs, fenix, ... }: let inherit (nixpkgs) lib; eachSupportedSystem = lib.genAttrs supportedSystems; supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; mkDevShells = system: let pkgs = import nixpkgs { inherit system; }; # get the rust toolchain from the rustup # `rust-toolchain.toml` configuration file rust-toolchain = fenix.packages.${system}.fromToolchainFile { file = ./rust-toolchain.toml; sha256 = "opUgs6ckUQCyDxcB9Wy51pqhd0MPGHUVbwRKKPGiwZU="; }; in { default = pkgs.mkShell { buildInputs = [ rust-toolchain ]; }; }; in { devShells = eachSupportedSystem mkDevShells; }; } imap-next-0.3.1/integration-test/000077500000000000000000000000001472413535300167245ustar00rootroot00000000000000imap-next-0.3.1/integration-test/Cargo.toml000066400000000000000000000007471472413535300206640ustar00rootroot00000000000000[package] name = "integration-test" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" publish = false [dependencies] bstr = { version = "1.11.0", default-features = false } bytes = "1.8.0" imap-codec = { version = "2.0.0-alpha.4" } imap-next = { path = ".." } tokio = { version = "1.41.1", features = ["macros", "net", "rt", "time"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } # Fix minimal versions lazy_static = "1.5.0" imap-next-0.3.1/integration-test/README.md000066400000000000000000000001341472413535300202010ustar00rootroot00000000000000# integration-test Test harness for writing lightweight integration tests for `imap-next`. imap-next-0.3.1/integration-test/src/000077500000000000000000000000001472413535300175135ustar00rootroot00000000000000imap-next-0.3.1/integration-test/src/client_tester.rs000066400000000000000000000330241472413535300227270ustar00rootroot00000000000000use std::net::SocketAddr; use bstr::ByteSlice; use imap_next::{ client::{self, Client, CommandHandle}, imap_types::{command::Command, ToStatic}, stream::{self, Stream}, }; use tokio::net::TcpStream; use tracing::trace; use crate::codecs::Codecs; /// A wrapper for `ClientFlow` suitable for testing. pub struct ClientTester { codecs: Codecs, connection_state: ConnectionState, } impl ClientTester { pub async fn new( codecs: Codecs, client_options: client::Options, server_address: SocketAddr, ) -> Self { let stream = TcpStream::connect(server_address).await.unwrap(); trace!(?server_address, "Client is connected"); let stream = Stream::insecure(stream); let client = Client::new(client_options); Self { codecs, connection_state: ConnectionState::Connected { stream, client }, } } pub async fn receive_greeting(&mut self, expected_bytes: &[u8]) { let expected_greeting = self.codecs.decode_greeting(expected_bytes); let (stream, client) = self.connection_state.connected(); let event = stream.next(client).await.unwrap(); match event { client::Event::GreetingReceived { greeting } => { assert_eq!(expected_greeting, greeting); } event => panic!("Client emitted unexpected event: {event:?}"), } } pub fn enqueue_command(&mut self, bytes: &[u8]) -> EnqueuedCommand { let command = self.codecs.decode_command_normalized(bytes).to_static(); let (_, client) = self.connection_state.connected(); let handle = client.enqueue_command(command.to_static()); EnqueuedCommand { command, handle } } pub fn set_idle_done(&mut self, idle_handle: CommandHandle) { let (_, client) = self.connection_state.connected(); let Some(handle) = client.set_idle_done() else { panic!("Client is in unexpected state"); }; assert_eq!(idle_handle, handle); } pub fn set_authenticate_data(&mut self, authenticate_handle: CommandHandle, bytes: &[u8]) { let authenticate_data = self .codecs .decode_authenticate_data_normalized(bytes) .to_static(); let (_, client) = self.connection_state.connected(); let Ok(handle) = client.set_authenticate_data(authenticate_data.to_static()) else { panic!("Client is in unexpected state"); }; assert_eq!(authenticate_handle, handle); } pub async fn progress_command(&mut self, enqueued_command: EnqueuedCommand) { let (stream, client) = self.connection_state.connected(); let event = stream.next(client).await.unwrap(); match event { client::Event::CommandSent { handle, command } => { assert_eq!(enqueued_command.handle, handle); assert_eq!(enqueued_command.command, command); } event => panic!("Client emitted unexpected event: {event:?}"), } } pub async fn progress_idle(&mut self, idle_handle: CommandHandle) { let (stream, client) = self.connection_state.connected(); let event = stream.next(client).await.unwrap(); match event { client::Event::IdleCommandSent { handle } => { assert_eq!(idle_handle, handle); } event => panic!("Client emitted unexpected event: {event:?}"), } } pub async fn progress_idle_done(&mut self, idle_handle: CommandHandle) { let (stream, client) = self.connection_state.connected(); let event = stream.next(client).await.unwrap(); match event { client::Event::IdleDoneSent { handle } => { assert_eq!(idle_handle, handle); } event => panic!("Client emitted unexpected event: {event:?}"), } } pub async fn progress_authenticate(&mut self, authenticate_handle: CommandHandle) { let (stream, client) = self.connection_state.connected(); let event = stream.next(client).await.unwrap(); match event { client::Event::AuthenticateStarted { handle } => { assert_eq!(authenticate_handle, handle); } event => panic!("Client emitted unexpected event: {event:?}"), } } pub async fn progress_rejected_command( &mut self, enqueued_command: EnqueuedCommand, status_bytes: &[u8], ) { let expected_status = self.codecs.decode_status(status_bytes); let (stream, client) = self.connection_state.connected(); let event = stream.next(client).await.unwrap(); match event { client::Event::CommandRejected { handle, command, status, } => { assert_eq!(enqueued_command.handle, handle); assert_eq!(enqueued_command.command, command); assert_eq!(expected_status, status); } event => panic!("Client emitted unexpected event: {event:?}"), } } /// Progresses internal commands without expecting any results. pub async fn progress_internal_commands(&mut self) -> T { let (stream, client) = self.connection_state.connected(); let result = stream.next(client).await; panic!("Client emitted unexpected result: {result:?}"); } pub async fn send_command(&mut self, bytes: &[u8]) { let enqueued_command = self.enqueue_command(bytes); self.progress_command(enqueued_command).await; } pub async fn send_idle(&mut self, bytes: &[u8]) -> CommandHandle { let enqueued_command = self.enqueue_command(bytes); self.progress_idle(enqueued_command.handle).await; enqueued_command.handle } pub async fn send_idle_done(&mut self, idle_handle: CommandHandle) { self.set_idle_done(idle_handle); self.progress_idle_done(idle_handle).await; } pub async fn send_authenticate(&mut self, bytes: &[u8]) -> CommandHandle { let enqueued_command = self.enqueue_command(bytes); self.progress_authenticate(enqueued_command.handle).await; enqueued_command.handle } pub async fn send_rejected_command(&mut self, command_bytes: &[u8], status_bytes: &[u8]) { let enqueued_command = self.enqueue_command(command_bytes); self.progress_rejected_command(enqueued_command, status_bytes) .await; } pub async fn receive_data(&mut self, expected_bytes: &[u8]) { let expected_data = self.codecs.decode_data(expected_bytes); let (stream, client) = self.connection_state.connected(); let event = stream.next(client).await.unwrap(); match event { client::Event::DataReceived { data } => { assert_eq!(expected_data, data); } event => panic!("Client emitted unexpected event: {event:?}"), } } pub async fn receive_status(&mut self, expected_bytes: &[u8]) { let expected_status = self.codecs.decode_status(expected_bytes); let (stream, client) = self.connection_state.connected(); let event = stream.next(client).await.unwrap(); match event { client::Event::StatusReceived { status } => { assert_eq!(expected_status, status); } event => panic!("Client emitted unexpected event: {event:?}"), } } pub async fn receive_idle_accepted( &mut self, idle_handle: CommandHandle, expected_bytes: &[u8], ) { let expected_continuation_request = self.codecs.decode_continuation_request(expected_bytes); let (stream, client) = self.connection_state.connected(); let event = stream.next(client).await.unwrap(); match event { client::Event::IdleAccepted { handle, continuation_request, } => { assert_eq!(handle, idle_handle); assert_eq!(expected_continuation_request, continuation_request); } event => panic!("Client emitted unexpected event: {event:?}"), } } pub async fn receive_idle_rejected( &mut self, idle_handle: CommandHandle, expected_bytes: &[u8], ) { let expected_status = self.codecs.decode_status(expected_bytes); let (stream, client) = self.connection_state.connected(); let event = stream.next(client).await.unwrap(); match event { client::Event::IdleRejected { handle, status } => { assert_eq!(handle, idle_handle); assert_eq!(status, expected_status); } event => panic!("Client emitted unexpected event: {event:?}"), } } pub async fn receive_authenticate_continuation_request( &mut self, authenticate_request: CommandHandle, expected_bytes: &[u8], ) { let expected_continuation_request = self.codecs.decode_continuation_request(expected_bytes); let (stream, client) = self.connection_state.connected(); let event = stream.next(client).await.unwrap(); match event { client::Event::AuthenticateContinuationRequestReceived { handle, continuation_request, } => { assert_eq!(handle, authenticate_request); assert_eq!(expected_continuation_request, continuation_request); } event => panic!("Client emitted unexpected event: {event:?}"), } } pub async fn receive_authenticate_status( &mut self, authenticate_request: CommandHandle, expected_authenticate_bytes: &[u8], expected_status_bytes: &[u8], ) { let expected_command = self .codecs .decode_command_normalized(expected_authenticate_bytes); let expected_status = self.codecs.decode_status_normalized(expected_status_bytes); let (stream, client) = self.connection_state.connected(); let event = stream.next(client).await.unwrap(); match event { client::Event::AuthenticateStatusReceived { handle, command_authenticate, status, } => { assert_eq!(handle, authenticate_request); assert_eq!(expected_command, command_authenticate.into()); assert_eq!(expected_status, status); } event => panic!("Client emitted unexpected event: {event:?}"), } } async fn receive_error(&mut self) -> stream::Error { let result = match &mut self.connection_state { ConnectionState::Connected { stream, client } => stream.next(client).await, ConnectionState::Disconnected => panic!("Client is already disconnected"), }; match result { Ok(event) => panic!("Client emitted unexpected event: {event:?}"), Err(err) => err, } } pub async fn receive_error_because_expected_crlf_got_lf(&mut self, expected_bytes: &[u8]) { let error = self.receive_error().await; match error { stream::Error::State(client::Error::ExpectedCrlfGotLf { discarded_bytes }) => { assert_eq!( expected_bytes.as_bstr(), discarded_bytes.declassify().as_bstr() ); } error => panic!("Client emitted unexpected error: {error:?}"), } } pub async fn receive_error_because_malformed_message(&mut self, expected_bytes: &[u8]) { let error = self.receive_error().await; match error { stream::Error::State(client::Error::MalformedMessage { discarded_bytes }) => { assert_eq!( expected_bytes.as_bstr(), discarded_bytes.declassify().as_bstr() ); } error => panic!("Client emitted unexpected error: {error:?}"), } } pub async fn receive_error_because_response_too_long(&mut self, expected_bytes: &[u8]) { let error = self.receive_error().await; match error { stream::Error::State(client::Error::ResponseTooLong { discarded_bytes }) => { assert_eq!( expected_bytes.as_bstr(), discarded_bytes.declassify().as_bstr() ); } error => panic!("Client emitted unexpected error: {error:?}"), } } pub async fn receive_error_because_stream_closed(&mut self) { let error = self.receive_error().await; match error { stream::Error::Closed => (), error => panic!("Client emitted unexpected error: {error:?}"), } } } /// Connection state between client and server. #[allow(clippy::large_enum_variant)] enum ConnectionState { /// Connection to server established. Connected { stream: Stream, client: Client }, /// Connection dropped. Disconnected, } impl ConnectionState { fn connected(&mut self) -> (&mut Stream, &mut Client) { match self { ConnectionState::Connected { stream, client } => (stream, client), ConnectionState::Disconnected => panic!("Client is already disconnected"), } } #[allow(unused)] fn take(&mut self) -> ConnectionState { std::mem::replace(self, ConnectionState::Disconnected) } } /// Enqueued command that can be used for assertions. pub struct EnqueuedCommand { handle: CommandHandle, command: Command<'static>, } imap-next-0.3.1/integration-test/src/codecs.rs000066400000000000000000000200451472413535300213220ustar00rootroot00000000000000use bstr::ByteSlice; use imap_codec::{ decode::Decoder, encode::Encoder, AuthenticateDataCodec, CommandCodec, GreetingCodec, ResponseCodec, }; use imap_next::imap_types::{ auth::AuthenticateData, command::Command, response::{CommandContinuationRequest, Data, Greeting, Response, Status}, }; /// Contains all codecs from `imap-codec`. #[derive(Clone, Debug, Default, PartialEq)] #[non_exhaustive] pub struct Codecs { pub greeting_codec: GreetingCodec, pub command_codec: CommandCodec, pub response_codec: ResponseCodec, pub authenticate_data_codec: AuthenticateDataCodec, } impl Codecs { pub fn encode_greeting(&self, greeting: &Greeting) -> Vec { self.greeting_codec.encode(greeting).dump() } pub fn encode_command(&self, command: &Command) -> Vec { self.command_codec.encode(command).dump() } pub fn encode_response(&self, response: &Response) -> Vec { self.response_codec.encode(response).dump() } pub fn encode_continuation_request( &self, continuation_request: &CommandContinuationRequest, ) -> Vec { self.response_codec .encode(&Response::CommandContinuationRequest( continuation_request.clone(), )) .dump() } pub fn encode_data(&self, data: &Data) -> Vec { self.response_codec .encode(&Response::Data(data.clone())) .dump() } pub fn encode_status(&self, status: &Status) -> Vec { self.response_codec .encode(&Response::Status(status.clone())) .dump() } pub fn encode_authenticate_data(&self, authenticate_data: &AuthenticateData) -> Vec { self.authenticate_data_codec .encode(authenticate_data) .dump() } pub fn decode_greeting<'a>(&self, bytes: &'a [u8]) -> Greeting<'a> { match self.greeting_codec.decode(bytes) { Ok((rem, greeting)) => { if !rem.is_empty() { panic!( "Expected single greeting but there are remaining bytes {:?}", rem.as_bstr() ) } greeting } Err(err) => { panic!( "Got error {:?} when parsing greeting from bytes {:?}", err, bytes.as_bstr() ) } } } pub fn decode_command<'a>(&self, bytes: &'a [u8]) -> Command<'a> { match self.command_codec.decode(bytes) { Ok((rem, command)) => { if !rem.is_empty() { panic!( "Expected single command but there are remaining bytes {:?}", rem.as_bstr() ) } command } Err(err) => { panic!( "Got error {:?} when parsing command from bytes {:?}", err, bytes.as_bstr() ) } } } pub fn decode_response<'a>(&self, bytes: &'a [u8]) -> Response<'a> { match self.response_codec.decode(bytes) { Ok((rem, response)) => { if !rem.is_empty() { panic!( "Expected single response but there are remaining bytes {:?}", rem.as_bstr() ) } response } Err(err) => { panic!( "Got error {:?} when parsing response bytes {:?}", err, bytes.as_bstr() ) } } } pub fn decode_continuation_request<'a>( &self, bytes: &'a [u8], ) -> CommandContinuationRequest<'a> { let Response::CommandContinuationRequest(expected_data) = self.decode_response(bytes) else { panic!("Got wrong response type when parsing continuation request from {bytes:?}") }; expected_data } pub fn decode_data<'a>(&self, bytes: &'a [u8]) -> Data<'a> { let Response::Data(expected_data) = self.decode_response(bytes) else { panic!("Got wrong response type when parsing data from {bytes:?}") }; expected_data } pub fn decode_status<'a>(&self, bytes: &'a [u8]) -> Status<'a> { let Response::Status(expected_status) = self.decode_response(bytes) else { panic!("Got wrong response type when parsing status from {bytes:?}") }; expected_status } pub fn decode_authenticate_data<'a>(&self, bytes: &'a [u8]) -> AuthenticateData<'a> { match self.authenticate_data_codec.decode(bytes) { Ok((rem, response)) => { if !rem.is_empty() { panic!( "Expected single authenticate data but there are remaining bytes {:?}", rem.as_bstr() ) } response } Err(err) => { panic!( "Got error {:?} when parsing authenticate data bytes {:?}", err, bytes.as_bstr() ) } } } pub fn decode_greeting_normalized<'a>(&self, bytes: &'a [u8]) -> Greeting<'a> { let greeting = self.decode_greeting(bytes); let normalized_bytes = self.encode_greeting(&greeting); assert_eq!( normalized_bytes.as_bstr(), bytes.as_bstr(), "Bytes must contain a normalized greeting" ); greeting } pub fn decode_command_normalized<'a>(&self, bytes: &'a [u8]) -> Command<'a> { let command = self.decode_command(bytes); let normalized_bytes = self.encode_command(&command); assert_eq!( normalized_bytes.as_bstr(), bytes.as_bstr(), "Bytes must contain a normalized command" ); command } pub fn decode_response_normalized<'a>(&self, bytes: &'a [u8]) -> Response<'a> { let response = self.decode_response(bytes); let normalized_bytes = self.encode_response(&response); assert_eq!( normalized_bytes.as_bstr(), bytes.as_bstr(), "Bytes must contain a normalized response" ); response } pub fn decode_continuation_request_normalized<'a>( &self, bytes: &'a [u8], ) -> CommandContinuationRequest<'a> { let continuation_request = self.decode_continuation_request(bytes); let normalized_bytes = self.encode_continuation_request(&continuation_request); assert_eq!( normalized_bytes.as_bstr(), bytes.as_bstr(), "Bytes must contain a normalized continuation request" ); continuation_request } pub fn decode_data_normalized<'a>(&self, bytes: &'a [u8]) -> Data<'a> { let data = self.decode_data(bytes); let normalized_bytes = self.encode_data(&data); assert_eq!( normalized_bytes.as_bstr(), bytes.as_bstr(), "Bytes must contain a normalized data" ); data } pub fn decode_status_normalized<'a>(&self, bytes: &'a [u8]) -> Status<'a> { let status = self.decode_status(bytes); let normalized_bytes = self.encode_status(&status); assert_eq!( normalized_bytes.as_bstr(), bytes.as_bstr(), "Bytes must contain a normalized status" ); status } pub fn decode_authenticate_data_normalized<'a>(&self, bytes: &'a [u8]) -> AuthenticateData<'a> { let authenticate_data = self.decode_authenticate_data(bytes); let normalized_bytes = self.encode_authenticate_data(&authenticate_data); assert_eq!( normalized_bytes.as_bstr(), bytes.as_bstr(), "Bytes must contain a normalized authenticate data" ); authenticate_data } } imap-next-0.3.1/integration-test/src/lib.rs000066400000000000000000000001611472413535300206250ustar00rootroot00000000000000pub mod client_tester; pub mod codecs; pub mod mock; pub mod runtime; pub mod server_tester; pub mod test_setup; imap-next-0.3.1/integration-test/src/mock.rs000066400000000000000000000044141472413535300210150ustar00rootroot00000000000000use std::net::SocketAddr; use bstr::{BStr, ByteSlice}; use bytes::{Buf, BytesMut}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::{TcpListener, TcpStream}, }; use tracing::trace; /// Mocks either the server or client. /// /// This mock doesn't know any IMAP semantics. Instead it provides direct access to the /// TCP connection. Therefore the correctness of the test depends on the correctness /// of the test data. pub struct Mock { role: Role, stream: TcpStream, read_buffer: BytesMut, } impl Mock { pub async fn server(server_listener: TcpListener) -> Self { let role = Role::Server; let (stream, client_address) = server_listener.accept().await.unwrap(); trace!(?role, ?client_address, "Mock accepts connection"); Self { role, stream, read_buffer: BytesMut::default(), } } pub async fn client(server_address: SocketAddr) -> Self { let role = Role::Client; let stream = TcpStream::connect(server_address).await.unwrap(); trace!(?role, ?server_address, "Mock is connected"); Self { role, stream, read_buffer: BytesMut::default(), } } pub async fn send(&mut self, bytes: &[u8]) { trace!( role = ?self.role, bytes = ?BStr::new(bytes), "Mock writes bytes" ); self.stream.write_all(bytes).await.unwrap(); } pub async fn receive(&mut self, expected_bytes: &[u8]) { loop { let bytes = &self.read_buffer[..]; trace!( role = ?self.role, read_bytes = ?BStr::new(bytes), "Mock reads bytes" ); if bytes.len() < expected_bytes.len() { assert_eq!(expected_bytes[..bytes.len()].as_bstr(), bytes.as_bstr()); self.stream.read_buf(&mut self.read_buffer).await.unwrap(); } else { assert_eq!( expected_bytes.as_bstr(), bytes[..expected_bytes.len()].as_bstr() ); self.read_buffer.advance(expected_bytes.len()); break; } } } } #[derive(Debug)] enum Role { Server, Client, } imap-next-0.3.1/integration-test/src/runtime.rs000066400000000000000000000037411472413535300215510ustar00rootroot00000000000000use std::{future::Future, time::Duration}; use tokio::{join, runtime, select, time::sleep}; /// Options for creating an instance of `Runtime`. #[derive(Clone, Debug, PartialEq)] #[non_exhaustive] pub struct RuntimeOptions { pub timeout: Option, } impl Default for RuntimeOptions { fn default() -> Self { Self { timeout: Some(Duration::from_secs(1)), } } } /// Allows to execute one or more `Future`s by blocking the current thread. /// /// We prefer to have single-threaded unit tests because it makes debugging easier. /// This runtime allows us to execute server and client tasks in parallel on the same /// thread the test is executed on. pub struct Runtime { timeout: Option, rt: runtime::Runtime, } impl Runtime { pub fn new(runtime_options: RuntimeOptions) -> Self { let rt = runtime::Builder::new_current_thread() .enable_time() .enable_io() .build() .unwrap(); Runtime { timeout: runtime_options.timeout, rt, } } pub fn run(&self, future: impl Future) -> T { match self.timeout { None => self.rt.block_on(future), Some(timeout) => self.rt.block_on(async { select! { output = future => output, () = sleep(timeout) => panic!("exceeded {timeout:?} timeout"), } }), } } pub fn run2( &self, future1: impl Future, future2: impl Future, ) -> (T1, T2) { self.run(async { join!(future1, future2) }) } pub fn run2_and_select( &self, future1: impl Future, future2: impl Future, ) -> T { self.run(async { select! { output = future1 => output, output = future2 => output, } }) } } imap-next-0.3.1/integration-test/src/server_tester.rs000066400000000000000000000311351472413535300227600ustar00rootroot00000000000000use bstr::ByteSlice; use imap_next::{ imap_types::{response::Response, ToStatic}, server::{self, ResponseHandle, Server}, stream::{self, Stream}, }; use tokio::net::TcpListener; use tracing::trace; use crate::codecs::Codecs; /// Wrapper for `ServerFlow` suitable for testing. pub struct ServerTester { codecs: Codecs, server_options: server::Options, connection_state: ConnectionState, } impl ServerTester { pub async fn new( codecs: Codecs, server_options: server::Options, server_listener: TcpListener, ) -> Self { let (stream, client_address) = server_listener.accept().await.unwrap(); trace!(?client_address, "Server accepts connection"); let stream = Stream::insecure(stream); Self { codecs, server_options, connection_state: ConnectionState::Connected { stream }, } } pub async fn send_greeting(&mut self, bytes: &[u8]) { let enqueued_greeting = self.codecs.decode_greeting_normalized(bytes); match self.connection_state.take() { ConnectionState::Connected { mut stream } => { let mut server = Server::new(self.server_options.clone(), enqueued_greeting.to_static()); let event = stream.next(&mut server).await.unwrap(); match event { server::Event::GreetingSent { greeting } => { assert_eq!(enqueued_greeting, greeting); } event => panic!("Server emitted unexpected event: {event:?}"), } self.connection_state = ConnectionState::Greeted { stream, server }; } ConnectionState::Greeted { .. } => panic!("Server has already greeted"), ConnectionState::Disconnected => panic!("Server is already disconnected"), } } pub fn enqueue_data(&mut self, bytes: &[u8]) -> EnqueuedResponse { let data = self.codecs.decode_data_normalized(bytes).to_static(); let (_, server) = self.connection_state.greeted(); let handle = server.enqueue_data(data.to_static()); EnqueuedResponse { response: Response::Data(data), handle, } } pub fn enqueue_status(&mut self, bytes: &[u8]) -> EnqueuedResponse { let status = self.codecs.decode_status_normalized(bytes).to_static(); let (_, server) = self.connection_state.greeted(); let handle = server.enqueue_status(status.to_static()); EnqueuedResponse { response: Response::Status(status), handle, } } pub fn set_idle_accept(&mut self, bytes: &[u8]) -> EnqueuedResponse { let continuation_request = self .codecs .decode_continuation_request_normalized(bytes) .to_static(); let (_, server) = self.connection_state.greeted(); let Ok(handle) = server.idle_accept(continuation_request.to_static()) else { panic!("Server is in unexpected state"); }; EnqueuedResponse { response: Response::CommandContinuationRequest(continuation_request), handle, } } pub fn set_idle_reject(&mut self, bytes: &[u8]) -> EnqueuedResponse { let status = self.codecs.decode_status_normalized(bytes).to_static(); let (_, server) = self.connection_state.greeted(); let Ok(handle) = server.idle_reject(status.to_static()) else { panic!("Server is in unexpected state"); }; EnqueuedResponse { response: Response::Status(status), handle, } } pub fn set_authenticate_continue(&mut self, bytes: &[u8]) -> EnqueuedResponse { let authenticate_data = self .codecs .decode_continuation_request_normalized(bytes) .to_static(); let (_, server) = self.connection_state.greeted(); let Ok(handle) = server.authenticate_continue(authenticate_data.to_static()) else { panic!("Server is in unexpected state"); }; EnqueuedResponse { response: Response::CommandContinuationRequest(authenticate_data), handle, } } pub fn set_authenticate_finish(&mut self, bytes: &[u8]) -> EnqueuedResponse { let authenticate_data = self.codecs.decode_status_normalized(bytes).to_static(); let (_, server) = self.connection_state.greeted(); let Ok(handle) = server.authenticate_finish(authenticate_data.to_static()) else { panic!("Server is in unexpected state"); }; EnqueuedResponse { response: Response::Status(authenticate_data), handle, } } pub async fn progress_response(&mut self, enqueued_response: EnqueuedResponse) { let (stream, server) = self.connection_state.greeted(); let event = stream.next(server).await.unwrap(); match event { server::Event::ResponseSent { handle, response } => { assert_eq!(enqueued_response.handle, handle); assert_eq!(enqueued_response.response, response); } event => panic!("Server emitted unexpected event: {event:?}"), } } /// Progresses internal responses without expecting any results. pub async fn progress_internal_responses(&mut self) -> T { let (stream, server) = self.connection_state.greeted(); let result = stream.next(server).await; panic!("Server emitted unexpected result: {result:?}"); } pub async fn send_data(&mut self, bytes: &[u8]) { let enqueued_response = self.enqueue_data(bytes); self.progress_response(enqueued_response).await; } pub async fn send_status(&mut self, bytes: &[u8]) { let enqueued_response = self.enqueue_status(bytes); self.progress_response(enqueued_response).await; } pub async fn send_idle_accepted(&mut self, bytes: &[u8]) { let enqueued_response = self.set_idle_accept(bytes); self.progress_response(enqueued_response).await; } pub async fn send_idle_rejected(&mut self, bytes: &[u8]) { let enqueued_response = self.set_idle_reject(bytes); self.progress_response(enqueued_response).await; } pub async fn send_authenticate_continue(&mut self, bytes: &[u8]) { let enqueued_response = self.set_authenticate_continue(bytes); self.progress_response(enqueued_response).await; } pub async fn send_authenticate_finish(&mut self, bytes: &[u8]) { let enqueued_response = self.set_authenticate_finish(bytes); self.progress_response(enqueued_response).await; } async fn receive_error(&mut self) -> stream::Error { let (stream, server) = self.connection_state.greeted(); let result = stream.next(server).await; match result { Ok(event) => panic!("Server emitted unexpected event: {event:?}"), Err(err) => err, } } pub async fn receive_error_because_expected_crlf_got_lf(&mut self, expected_bytes: &[u8]) { let error = self.receive_error().await; match error { stream::Error::State(server::Error::ExpectedCrlfGotLf { discarded_bytes }) => { assert_eq!( expected_bytes.as_bstr(), discarded_bytes.declassify().as_bstr() ); } error => panic!("Server emitted unexpected error: {error:?}"), } } pub async fn receive_error_because_malformed_message(&mut self, expected_bytes: &[u8]) { let error = self.receive_error().await; match error { stream::Error::State(server::Error::MalformedMessage { discarded_bytes }) => { assert_eq!( expected_bytes.as_bstr(), discarded_bytes.declassify().as_bstr() ); } error => panic!("Server emitted unexpected error: {error:?}"), } } pub async fn receive_error_because_literal_too_long(&mut self, expected_bytes: &[u8]) { let error = self.receive_error().await; match error { stream::Error::State(server::Error::LiteralTooLong { discarded_bytes }) => { assert_eq!( expected_bytes.as_bstr(), discarded_bytes.declassify().as_bstr() ); } error => panic!("Server emitted unexpected error: {error:?}"), } } pub async fn receive_error_because_command_too_long(&mut self, expected_bytes: &[u8]) { let error = self.receive_error().await; match error { stream::Error::State(server::Error::CommandTooLong { discarded_bytes }) => { assert_eq!( expected_bytes.as_bstr(), discarded_bytes.declassify().as_bstr() ); } error => panic!("Server emitted unexpected error: {error:?}"), } } pub async fn receive_error_because_stream_closed(&mut self) { let error = self.receive_error().await; match error { stream::Error::Closed => (), error => panic!("Server emitted unexpected error: {error:?}"), } } pub async fn receive_command(&mut self, expected_bytes: &[u8]) { let expected_command = self.codecs.decode_command(expected_bytes); let (stream, server) = self.connection_state.greeted(); let event = stream.next(server).await.unwrap(); match event { server::Event::CommandReceived { command } => { assert_eq!(expected_command, command); } event => panic!("Server emitted unexpected event: {event:?}"), } } pub async fn receive_idle(&mut self, expected_bytes: &[u8]) { let expected_command = self.codecs.decode_command(expected_bytes); let (stream, server) = self.connection_state.greeted(); let event = stream.next(server).await.unwrap(); match event { server::Event::IdleCommandReceived { tag } => { assert_eq!(expected_command.tag, tag); } event => panic!("Server emitted unexpected event: {event:?}"), } } pub async fn receive_idle_done(&mut self) { let (stream, server) = self.connection_state.greeted(); let event = stream.next(server).await.unwrap(); match event { server::Event::IdleDoneReceived => (), event => panic!("Server emitted unexpected event: {event:?}"), } } pub async fn receive_authenticate_command(&mut self, expected_bytes: &[u8]) { let expected_command = self.codecs.decode_command(expected_bytes); let (stream, server) = self.connection_state.greeted(); let event = stream.next(server).await.unwrap(); match event { server::Event::CommandAuthenticateReceived { command_authenticate, } => { assert_eq!(expected_command, command_authenticate.into()); } event => panic!("Server emitted unexpected event: {event:?}"), } } pub async fn receive_authenticate_data(&mut self, expected_bytes: &[u8]) { let expected_authenticate_data = self.codecs.decode_authenticate_data(expected_bytes); let (stream, server) = self.connection_state.greeted(); let event = stream.next(server).await.unwrap(); match event { server::Event::AuthenticateDataReceived { authenticate_data } => { assert_eq!(expected_authenticate_data, authenticate_data); } event => panic!("Server emitted unexpected event: {event:?}"), } } } /// Connection state between server and client. #[allow(clippy::large_enum_variant)] enum ConnectionState { // Connection to client established. Connected { stream: Stream }, // Server greeted client. Greeted { stream: Stream, server: Server }, // Connection dropped. Disconnected, } impl ConnectionState { fn greeted(&mut self) -> (&mut Stream, &mut Server) { match self { ConnectionState::Connected { .. } => panic!("Server has not greeted yet"), ConnectionState::Greeted { stream, server } => (stream, server), ConnectionState::Disconnected => panic!("Server is already disconnected"), } } fn take(&mut self) -> ConnectionState { std::mem::replace(self, ConnectionState::Disconnected) } } /// Enqueued response that can be used for assertions. pub struct EnqueuedResponse { handle: ResponseHandle, response: Response<'static>, } imap-next-0.3.1/integration-test/src/test_setup.rs000066400000000000000000000067011472413535300222640ustar00rootroot00000000000000use std::net::SocketAddr; use imap_next::{client, server}; use tokio::net::TcpListener; use tracing::trace; use tracing_subscriber::EnvFilter; use crate::{ client_tester::ClientTester, codecs::Codecs, mock::Mock, runtime::{Runtime, RuntimeOptions}, server_tester::ServerTester, }; /// Contains all parameters for creating a test setup for the server or client side /// of `imap-next`. #[derive(Clone, Debug, PartialEq)] #[non_exhaustive] pub struct TestSetup { pub codecs: Codecs, pub server_options: server::Options, pub client_options: client::Options, pub runtime_options: RuntimeOptions, pub init_logging: bool, } impl TestSetup { /// Create a test setup to test the client side (mocking the server side). pub fn setup_client(self) -> (Runtime, Mock, ClientTester) { if self.init_logging { init_logging(); } let rt = Runtime::new(self.runtime_options); let (server_listener, server_address) = rt.run(bind_address()); let (server, client) = rt.run2( Mock::server(server_listener), ClientTester::new(self.codecs, self.client_options, server_address), ); (rt, server, client) } /// Create a test setup to test the server side (mocking the client side). pub fn setup_server(self) -> (Runtime, ServerTester, Mock) { if self.init_logging { init_logging(); } let rt = Runtime::new(self.runtime_options); let (server_listener, server_address) = rt.run(bind_address()); let (server, client) = rt.run2( ServerTester::new(self.codecs, self.server_options, server_listener), Mock::client(server_address), ); (rt, server, client) } /// Create a test setup to test the server side and the client side. pub fn setup(self) -> (Runtime, ServerTester, ClientTester) { if self.init_logging { init_logging(); } let rt = Runtime::new(self.runtime_options); let (server_listener, server_address) = rt.run(bind_address()); let (server, client) = rt.run2( ServerTester::new(self.codecs.clone(), self.server_options, server_listener), ClientTester::new(self.codecs, self.client_options, server_address), ); (rt, server, client) } } impl Default for TestSetup { fn default() -> Self { Self { codecs: Codecs::default(), server_options: server::Options::default(), client_options: client::Options::default(), runtime_options: RuntimeOptions::default(), init_logging: true, } } } fn init_logging() { let builder = tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env()) .with_target(false) .with_file(false) .with_line_number(false) .without_time(); // We use `try_init` because multiple tests might try to initialize the logging let _result = builder.try_init(); } async fn bind_address() -> (TcpListener, SocketAddr) { // If we use port 0 the OS will assign us a free port. This is useful because // we want to run many tests in parallel and two tests must not use the same port. let server_listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let server_address = server_listener.local_addr().unwrap(); trace!(?server_address, "Bound to address"); (server_listener, server_address) } imap-next-0.3.1/integration-test/tests/000077500000000000000000000000001472413535300200665ustar00rootroot00000000000000imap-next-0.3.1/integration-test/tests/client.rs000066400000000000000000000361621472413535300217220ustar00rootroot00000000000000use std::time::Duration; use integration_test::test_setup::TestSetup; #[test] fn noop() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); let noop = b"A1 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive(noop)); let status = b"A1 OK ...\r\n"; rt.run2(server.send(status), client.receive_status(status)); } #[test] fn noop_with_large_lines() { let mut setup = TestSetup::default(); // Sending large messages takes some time, especially when running on a slow CI. setup.runtime_options.timeout = Some(Duration::from_secs(10)); let (rt, mut server, mut client) = setup.setup_client(); // This number seems to be larger than the TCP buffer, so server/client must // send/receive in parallel to prevent a deadlock. const LARGE: usize = 10 * 1024 * 1024; let greeting = &mut b"* OK ".to_vec(); greeting.extend(vec![b'.'; LARGE]); greeting.extend(b"\r\n"); rt.run2(server.send(greeting), client.receive_greeting(greeting)); let noop = b"A1 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive(noop)); let status = &mut b"A1 OK ".to_vec(); status.extend(vec![b'.'; LARGE]); status.extend(b"\r\n"); rt.run2(server.send(status), client.receive_status(status)); } #[test] fn gibberish_instead_of_greeting() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let gibberish = b"I like bananas\r\n"; rt.run2( server.send(gibberish), client.receive_error_because_malformed_message(gibberish), ); } #[test] fn gibberish_instead_of_response() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); let noop = b"A1 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive(noop)); let gibberish = b"I like bananas\r\n"; rt.run2( server.send(gibberish), client.receive_error_because_malformed_message(gibberish), ); } #[test] fn greeting_with_missing_cr() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); // Greeting with missing \r let greeting = b"* OK ...\n"; rt.run2( server.send(greeting), client.receive_error_because_expected_crlf_got_lf(greeting), ); } #[test] fn response_with_missing_cr() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); let noop = b"A1 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive(noop)); // Response with missing \r let status = b"A1 OK ...\n"; rt.run2( server.send(status), client.receive_error_because_expected_crlf_got_lf(status), ); } #[test] fn crlf_relaxed() { let mut setup = TestSetup::default(); setup.client_options.crlf_relaxed = true; let (rt, mut server, mut client) = setup.setup_client(); // Greeting with missing \r let greeting = b"* OK ...\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); let noop = b"A1 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive(noop)); // Response with missing \r let status = b"A1 OK ...\n"; rt.run2(server.send(status), client.receive_status(status)); let noop = b"A2 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive(noop)); // Response with \r still works let status = b"A2 OK ...\r\n"; rt.run2(server.send(status), client.receive_status(status)); } #[test] fn login_with_literal() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); let login = b"A1 LOGIN {5}\r\nABCDE {5}\r\nFGHIJ\r\n"; let continuation_request = b"+ ...\r\n"; rt.run2(client.send_command(login), async { server.receive(&login[..14]).await; server.send(continuation_request).await; server.receive(&login[14..25]).await; server.send(continuation_request).await; server.receive(&login[25..]).await; }); let status = b"A1 NO ...\r\n"; rt.run2(server.send(status), client.receive_status(status)); } #[test] fn login_with_rejected_literal() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); let login = b"A1 LOGIN {5}\r\nABCDE {5}\r\nFGHIJ\r\n"; let status = b"A1 BAD ...\r\n"; rt.run2(client.send_rejected_command(login, status), async { server.receive(&login[..14]).await; server.send(status).await; }); } #[test] fn login_with_literal_and_unexpected_status() { // According to the specification, OK and NO will not affect the literal let unexpected_status_tests = [b"A1 OK ...\r\n", b"A1 NO ...\r\n"]; for unexpected_status in unexpected_status_tests { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); let login = b"A1 LOGIN {5}\r\nABCDE {5}\r\nFGHIJ\r\n"; let continuation_request = b"+ ...\r\n"; rt.run2( async { // Client starts sending the command let command = client.enqueue_command(login); // Client receives unexpected status client.receive_status(unexpected_status).await; // Client is able to continue sending the command client.progress_command(command).await; }, async { // Server starts receiving the command server.receive(&login[..14]).await; // Server sends unexpected status server.send(unexpected_status).await; // Server continues receiving the command server.send(continuation_request).await; server.receive(&login[14..25]).await; server.send(continuation_request).await; server.receive(&login[25..]).await; }, ); } } #[test] fn login_with_non_sync_literal() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); let login = b"A1 LOGIN {5+}\r\nABCDE {5+}\r\nFGHIJ\r\n"; rt.run2(client.send_command(login), server.receive(login)); let status = b"A1 NO ...\r\n"; rt.run2(server.send(status), client.receive_status(status)); } #[test] fn response_larger_than_max_response_size() { // The client will reject the response because it's larger than the max size let max_response_size_tests = [11, 20, 100, 10 * 1024 * 1024]; for max_response_size in max_response_size_tests { let mut setup = TestSetup::default(); setup.client_options.max_response_size = max_response_size as u32; // Sending large messages takes some time, especially when running on a slow CI. setup.runtime_options.timeout = Some(Duration::from_secs(10)); let (rt, mut server, mut client) = setup.setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); let noop = b"A1 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive(noop)); // Response smaller than the max size can be received let small_status = b"A1 OK ...\r\n"; rt.run2( server.send(small_status), client.receive_status(small_status), ); // Response larger than the max size triggers an error let large_status = format!( "{}\r\n", String::from_utf8(vec![b'.'; max_response_size + 1]).unwrap(), ) .into_bytes(); rt.run2( server.send(&large_status), client.receive_error_because_response_too_long(&large_status[..max_response_size]), ); } } #[test] fn idle_accepted() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); // Client starts IDLE let idle = b"A1 IDLE\r\n"; let (idle_handle, _) = rt.run2(client.send_idle(idle), server.receive(idle)); // Server accepts IDLE let continuation_request = b"+ idling\r\n"; rt.run2( server.send(continuation_request), client.receive_idle_accepted(idle_handle, continuation_request), ); // Client ends IDLE let idle_done = b"DONE\r\n"; rt.run2( client.send_idle_done(idle_handle), server.receive(idle_done), ); // Client is able to send commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive(noop)); // Client is able to receive responses let status = b"A2 OK ...\r\n"; rt.run2(server.send(status), client.receive_status(status)); } #[test] fn idle_rejected() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); // Client starts IDLE let idle = b"A1 IDLE\r\n"; let (idle_handle, _) = rt.run2(client.send_idle(idle), server.receive(idle)); // Server rejects IDLE let status = b"A1 NO rise and shine\r\n"; rt.run2( server.send(status), client.receive_idle_rejected(idle_handle, status), ); // Client is able to send commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive(noop)); // Client is able to receive responses let status = b"A2 OK ...\r\n"; rt.run2(server.send(status), client.receive_status(status)); } #[test] fn authenticate_accepted() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); // Client initiates AUTHENTICATE let authenticate = b"A1 AUTHENTICATE PLAIN dGVzdAB0ZXN0AHRlc3Q=\r\n"; let (authenticate_handle, _) = rt.run2( client.send_authenticate(authenticate), server.receive(authenticate), ); // Server accepts AUTHENTICATE let status = b"A1 OK success\r\n"; rt.run2( server.send(status), client.receive_authenticate_status(authenticate_handle, authenticate, status), ); // Client is able to send commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive(noop)); // Client is able to receive responses let status = b"A2 OK ...\r\n"; rt.run2(server.send(status), client.receive_status(status)); } #[test] fn authenticate_with_more_data_accepted() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); // Client initiates AUTHENTICATE let authenticate = b"A1 AUTHENTICATE PLAIN\r\n"; let (authenticate_handle, _) = rt.run2( client.send_authenticate(authenticate), server.receive(authenticate), ); // Server requests more data let continuation_request = b"+ \r\n"; rt.run2( server.send(continuation_request), client.receive_authenticate_continuation_request(authenticate_handle, continuation_request), ); // Client sends more data let authenticate_data = b"dGVzdAB0ZXN0AHRlc3Q=\r\n"; rt.run2_and_select( async { client.set_authenticate_data(authenticate_handle, authenticate_data); client.progress_internal_commands().await }, server.receive(authenticate_data), ); // Server accepts AUTHENTICATE let status = b"A1 OK success\r\n"; rt.run2( server.send(status), client.receive_authenticate_status(authenticate_handle, authenticate, status), ); // Client is able to send commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive(noop)); // Client is able to receive responses let status = b"A2 OK ...\r\n"; rt.run2(server.send(status), client.receive_status(status)); } #[test] fn authenticate_rejected() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); // Client initiates AUTHENTICATE let authenticate = b"A1 AUTHENTICATE PLAIN dGVzdAB0ZXN0AHRlc3Q=\r\n"; let (authenticate_handle, _) = rt.run2( client.send_authenticate(authenticate), server.receive(authenticate), ); // Server rejects AUTHENTICATE let status = b"A1 NO abort\r\n"; rt.run2( server.send(status), client.receive_authenticate_status(authenticate_handle, authenticate, status), ); // Client is able to send commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive(noop)); // Client is able to receive responses let status = b"A2 OK ...\r\n"; rt.run2(server.send(status), client.receive_status(status)); } #[test] fn authenticate_with_more_data_rejected() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); // Client initiates AUTHENTICATE let authenticate = b"A1 AUTHENTICATE PLAIN\r\n"; let (authenticate_handle, _) = rt.run2( client.send_authenticate(authenticate), server.receive(authenticate), ); // Server requests more data let continuation_request = b"+ \r\n"; rt.run2( server.send(continuation_request), client.receive_authenticate_continuation_request(authenticate_handle, continuation_request), ); // Client sends more data let authenticate_data = b"dGVzdAB0ZXN0AHRlc3Q=\r\n"; rt.run2_and_select( async { client.set_authenticate_data(authenticate_handle, authenticate_data); client.progress_internal_commands().await }, server.receive(authenticate_data), ); // Server rejects AUTHENTICATE let status = b"A1 NO abort\r\n"; rt.run2( server.send(status), client.receive_authenticate_status(authenticate_handle, authenticate, status), ); // Client is able to send commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive(noop)); // Client is able to receive responses let status = b"A2 OK ...\r\n"; rt.run2(server.send(status), client.receive_status(status)); } #[test] fn stream_closed() { let (rt, mut server, mut client) = TestSetup::default().setup_client(); let greeting = b"* OK ...\r\n"; rt.run2(server.send(greeting), client.receive_greeting(greeting)); // Close stream drop(server); rt.run(client.receive_error_because_stream_closed()); } imap-next-0.3.1/integration-test/tests/client_server.rs000066400000000000000000000270071472413535300233060ustar00rootroot00000000000000use std::time::Duration; use integration_test::test_setup::TestSetup; #[test] fn noop() { let (rt, mut server, mut client) = TestSetup::default().setup(); let greeting = b"* OK ...\r\n"; rt.run2( server.send_greeting(greeting), client.receive_greeting(greeting), ); let noop = b"A1 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive_command(noop)); let status = b"A1 OK ...\r\n"; rt.run2(server.send_status(status), client.receive_status(status)); } #[test] fn send_large_messages_in_parallel() { let mut setup = TestSetup::default(); // Sending large messages takes some time, especially when running on a slow CI. setup.runtime_options.timeout = Some(Duration::from_secs(10)); // Disable the limits because we want to send and receive large messages setup.server_options.max_literal_size = u32::MAX; setup.server_options.max_command_size = u32::MAX; let (rt, mut server, mut client) = setup.setup(); // This number seems to be larger than the TCP buffer, so server/client must // send/receive in parallel to prevent a deadlock. const LARGE: usize = 10 * 1024 * 1024; let greeting = b"* OK ...\r\n"; rt.run2( server.send_greeting(greeting), client.receive_greeting(greeting), ); let noop = b"A1 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive_command(noop)); // Create a large command let login = &mut b"A1 LOGIN ABCDE ".to_vec(); login.extend(vec![b'x'; LARGE]); login.extend(b"\r\n"); // Create a large response let status = &mut b"A1 OK ".to_vec(); status.extend(vec![b'.'; LARGE]); status.extend(b"\r\n"); // Client and server send large messages in parallel without actively // receiving messages. This should not lead to a deadlock. rt.run2(client.send_command(login), server.send_status(status)); // Client and server receive the messages rt.run2(client.receive_status(status), server.receive_command(login)); } #[test] fn login_with_literal() { // The server will accept the literal ABCDE because it's smaller than the max size let max_literal_size_tests = [5, 6, 10, 100]; for max_literal_size in max_literal_size_tests { let mut setup = TestSetup::default(); setup .server_options .set_literal_accept_text("You shall pass".to_owned()) .unwrap(); setup.server_options.max_literal_size = max_literal_size; let (rt, mut server, mut client) = TestSetup::default().setup(); let greeting = b"* OK ...\r\n"; rt.run2( server.send_greeting(greeting), client.receive_greeting(greeting), ); let login = b"A1 LOGIN {5}\r\nABCDE {5}\r\nFGHIJ\r\n"; rt.run2(client.send_command(login), server.receive_command(login)); let status = b"A1 NO ...\r\n"; rt.run2(server.send_status(status), client.receive_status(status)); } } #[test] fn login_with_rejected_literal() { // The server will reject the literal ABCDE because it's larger than the max size let max_literal_size_tests = [0, 1, 4]; for max_literal_size in max_literal_size_tests { let mut setup = TestSetup::default(); setup .server_options .set_literal_reject_text("You shall not pass".to_owned()) .unwrap(); setup.server_options.max_literal_size = max_literal_size; let (rt, mut server, mut client) = setup.setup(); let greeting = b"* OK ...\r\n"; rt.run2( server.send_greeting(greeting), client.receive_greeting(greeting), ); let login = b"A1 LOGIN {5}\r\nABCDE {5}\r\nFGHIJ\r\n"; let status = b"A1 BAD You shall not pass\r\n"; rt.run2_and_select(client.send_rejected_command(login, status), async { server .receive_error_because_literal_too_long(&login[..14]) .await; server.progress_internal_responses().await }); } } #[test] fn login_with_non_sync_literal() { let (rt, mut server, mut client) = TestSetup::default().setup(); let greeting = b"* OK ...\r\n"; rt.run2( server.send_greeting(greeting), client.receive_greeting(greeting), ); let login = b"A1 LOGIN {5+}\r\nABCDE {5+}\r\nFGHIJ\r\n"; rt.run2(client.send_command(login), server.receive_command(login)); let status = b"A1 NO ...\r\n"; rt.run2(server.send_status(status), client.receive_status(status)); } #[test] fn idle_accepted() { let (rt, mut server, mut client) = TestSetup::default().setup(); let greeting = b"* OK ...\r\n"; rt.run2( server.send_greeting(greeting), client.receive_greeting(greeting), ); // Client starts IDLE let idle = b"A1 IDLE\r\n"; let (idle_handle, _) = rt.run2(client.send_idle(idle), server.receive_idle(idle)); // Server accepts IDLE let continuation_request = b"+ idling\r\n"; rt.run2( server.send_idle_accepted(continuation_request), client.receive_idle_accepted(idle_handle, continuation_request), ); // Client ends IDLE rt.run2( client.send_idle_done(idle_handle), server.receive_idle_done(), ); // Client is able to send commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive_command(noop)); // Server is able to send responses let status = b"A2 OK ...\r\n"; rt.run2(server.send_status(status), client.receive_status(status)); } #[test] fn idle_rejected() { let (rt, mut server, mut client) = TestSetup::default().setup(); let greeting = b"* OK ...\r\n"; rt.run2( server.send_greeting(greeting), client.receive_greeting(greeting), ); // Client starts IDLE let idle = b"A1 IDLE\r\n"; let (idle_handle, _) = rt.run2(client.send_idle(idle), server.receive_idle(idle)); // Server rejects IDLE let status = b"A1 NO rise and shine\r\n"; rt.run2( server.send_idle_rejected(status), client.receive_idle_rejected(idle_handle, status), ); // Client is able to send commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive_command(noop)); // Server is able to send responses let status = b"A2 OK ...\r\n"; rt.run2(server.send_status(status), client.receive_status(status)); } #[test] fn authenticate_accepted() { let (rt, mut server, mut client) = TestSetup::default().setup(); let greeting = b"* OK ...\r\n"; rt.run2( server.send_greeting(greeting), client.receive_greeting(greeting), ); // Client initiates AUTHENTICATE let authenticate = b"A1 AUTHENTICATE PLAIN\r\n"; let (authenticate_handle, _) = rt.run2( client.send_authenticate(authenticate), server.receive_authenticate_command(authenticate), ); // Server accepts AUTHENTICATE let status = b"A1 OK success\r\n"; rt.run2( server.send_authenticate_finish(status), client.receive_authenticate_status(authenticate_handle, authenticate, status), ); // Client is able to send commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive_command(noop)); // Server is able to send responses let status = b"A2 OK ...\r\n"; rt.run2(server.send_status(status), client.receive_status(status)); } #[test] fn authenticate_with_more_data_accepted() { let (rt, mut server, mut client) = TestSetup::default().setup(); let greeting = b"* OK ...\r\n"; rt.run2( server.send_greeting(greeting), client.receive_greeting(greeting), ); // Client initiates AUTHENTICATE let authenticate = b"A1 AUTHENTICATE PLAIN\r\n"; let (authenticate_handle, _) = rt.run2( client.send_authenticate(authenticate), server.receive_authenticate_command(authenticate), ); // Server requests more data let continuation_request = b"+ \r\n"; rt.run2( server.send_authenticate_continue(continuation_request), client.receive_authenticate_continuation_request(authenticate_handle, continuation_request), ); // Client sends more data let authenticate_data = b"dGVzdAB0ZXN0AHRlc3Q=\r\n"; rt.run2_and_select( async { client.set_authenticate_data(authenticate_handle, authenticate_data); client.progress_internal_commands().await }, server.receive_authenticate_data(authenticate_data), ); // Server accepts AUTHENTICATE let status = b"A1 OK success\r\n"; rt.run2( server.send_authenticate_finish(status), client.receive_authenticate_status(authenticate_handle, authenticate, status), ); // Client is able to send commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive_command(noop)); // Server is able to send responses let status = b"A2 OK ...\r\n"; rt.run2(server.send_status(status), client.receive_status(status)); } #[test] fn authenticate_rejected() { let (rt, mut server, mut client) = TestSetup::default().setup(); let greeting = b"* OK ...\r\n"; rt.run2( server.send_greeting(greeting), client.receive_greeting(greeting), ); // Client initiates AUTHENTICATE let authenticate = b"A1 AUTHENTICATE PLAIN\r\n"; let (authenticate_handle, _) = rt.run2( client.send_authenticate(authenticate), server.receive_authenticate_command(authenticate), ); // Server rejects AUTHENTICATE let status = b"A1 NO abort\r\n"; rt.run2( server.send_authenticate_finish(status), client.receive_authenticate_status(authenticate_handle, authenticate, status), ); // Client is able to send commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive_command(noop)); // Server is able to send responses let status = b"A2 OK ...\r\n"; rt.run2(server.send_status(status), client.receive_status(status)); } #[test] fn authenticate_with_more_data_rejected() { let (rt, mut server, mut client) = TestSetup::default().setup(); let greeting = b"* OK ...\r\n"; rt.run2( server.send_greeting(greeting), client.receive_greeting(greeting), ); // Client initiates AUTHENTICATE let authenticate = b"A1 AUTHENTICATE PLAIN\r\n"; let (authenticate_handle, _) = rt.run2( client.send_authenticate(authenticate), server.receive_authenticate_command(authenticate), ); // Server requests more data let continuation_request = b"+ \r\n"; rt.run2( server.send_authenticate_continue(continuation_request), client.receive_authenticate_continuation_request(authenticate_handle, continuation_request), ); // Client sends more data let authenticate_data = b"dGVzdAB0ZXN0AHRlc3Q=\r\n"; rt.run2_and_select( async { client.set_authenticate_data(authenticate_handle, authenticate_data); client.progress_internal_commands().await }, server.receive_authenticate_data(authenticate_data), ); // Server rejects AUTHENTICATE let status = b"A1 NO abort\r\n"; rt.run2( server.send_authenticate_finish(status), client.receive_authenticate_status(authenticate_handle, authenticate, status), ); // Client is able to send commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send_command(noop), server.receive_command(noop)); // Server is able to send responses let status = b"A2 OK ...\r\n"; rt.run2(server.send_status(status), client.receive_status(status)); } imap-next-0.3.1/integration-test/tests/server.rs000066400000000000000000000365671472413535300217630ustar00rootroot00000000000000use std::time::Duration; use integration_test::test_setup::TestSetup; #[test] fn noop() { let (rt, mut server, mut client) = TestSetup::default().setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); let noop = b"A1 NOOP\r\n"; rt.run2(client.send(noop), server.receive_command(noop)); let status = b"A1 OK ...\r\n"; rt.run2(server.send_status(status), client.receive(status)); } #[test] fn noop_with_large_lines() { let mut setup = TestSetup::default(); // Sending large messages takes some time, especially when running on a slow CI. setup.runtime_options.timeout = Some(Duration::from_secs(10)); let (rt, mut server, mut client) = setup.setup_server(); // This number seems to be larger than the TCP buffer, so server/client must // send/receive in parallel to prevent a deadlock. const LARGE: usize = 10 * 1024 * 1024; let greeting = &mut b"* OK ".to_vec(); greeting.extend(vec![b'.'; LARGE]); greeting.extend(b"\r\n"); rt.run2(server.send_greeting(greeting), client.receive(greeting)); let noop = b"A1 NOOP\r\n"; rt.run2(client.send(noop), server.receive_command(noop)); let status = &mut b"A1 OK ".to_vec(); status.extend(vec![b'.'; LARGE]); status.extend(b"\r\n"); rt.run2(server.send_status(status), client.receive(status)); } #[test] fn gibberish_instead_of_command() { let (rt, mut server, mut client) = TestSetup::default().setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); let gibberish = b"I like bananas\r\n"; rt.run2( client.send(gibberish), server.receive_error_because_malformed_message(gibberish), ); } #[test] fn command_with_missing_cr() { let (rt, mut server, mut client) = TestSetup::default().setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); // Command with missing \r let noop = b"A1 NOOP\n"; rt.run2( client.send(noop), server.receive_error_because_expected_crlf_got_lf(noop), ); } #[test] fn crlf_relaxed() { let mut setup = TestSetup::default(); setup.server_options.crlf_relaxed = true; let (rt, mut server, mut client) = setup.setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); // Command with missing \r let noop = b"A1 NOOP\n"; rt.run2(client.send(noop), server.receive_command(noop)); let status = b"A1 OK ...\r\n"; rt.run2(server.send_status(status), client.receive(status)); // Command with \r still works let noop = b"A2 NOOP\r\n"; rt.run2(client.send(noop), server.receive_command(noop)); let status = b"A2 OK ...\r\n"; rt.run2(server.send_status(status), client.receive(status)); } #[test] fn login_with_literal() { // The server will accept the literal ABCDE because it's smaller than the max size let max_literal_size_tests = [5, 6, 10, 100]; for max_literal_size in max_literal_size_tests { let mut setup = TestSetup::default(); setup .server_options .set_literal_accept_text("You shall pass".to_string()) .unwrap(); setup.server_options.max_literal_size = max_literal_size; let (rt, mut server, mut client) = setup.setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); let login = b"A1 LOGIN {5}\r\nABCDE {5}\r\nFGHIJ\r\n"; let continuation_request = b"+ You shall pass\r\n"; rt.run2( async { client.send(&login[..14]).await; client.receive(continuation_request).await; client.send(&login[14..25]).await; client.receive(continuation_request).await; client.send(&login[25..]).await; }, server.receive_command(login), ); let status = b"A1 NO ...\r\n"; rt.run2(server.send_status(status), client.receive(status)); } } #[test] fn login_with_rejected_literal() { // The server will reject the literal ABCDE because it's larger than the max size let max_literal_size_tests = [0, 1, 4]; for max_literal_size in max_literal_size_tests { let mut setup = TestSetup::default(); setup .server_options .set_literal_reject_text("You shall not pass".to_owned()) .unwrap(); setup.server_options.max_literal_size = max_literal_size; let (rt, mut server, mut client) = setup.setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); let login = b"A1 LOGIN {5}\r\nABCDE {5}\r\nFGHIJ\r\n"; rt.run2( client.send(&login[..14]), server.receive_error_because_literal_too_long(&login[..14]), ); let status = b"A1 BAD You shall not pass\r\n"; rt.run2_and_select(client.receive(status), server.progress_internal_responses()); } } #[test] fn login_with_non_sync_literal() { let (rt, mut server, mut client) = TestSetup::default().setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); let login = b"A1 LOGIN {5+}\r\nABCDE {5+}\r\nFGHIJ\r\n"; rt.run2(client.send(login), server.receive_command(login)); let status = b"A1 NO ...\r\n"; rt.run2(server.send_status(status), client.receive(status)); } #[test] fn command_larger_than_max_command_size() { // The server will reject the command because it's larger than the max size let max_command_size_tests = [9, 10, 20, 100, 10 * 1024 * 1024]; for max_command_size in max_command_size_tests { let mut setup = TestSetup::default(); setup.server_options.max_command_size = max_command_size as u32; // Sending large messages takes some time, especially when running on a slow CI. setup.runtime_options.timeout = Some(Duration::from_secs(10)); let (rt, mut server, mut client) = setup.setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); // Command smaller than the max size can be received let small_command = b"A1 NOOP\r\n"; rt.run2( client.send(small_command), server.receive_command(small_command), ); // Command larger than the max size triggers an error let large_command = format!( "{}\r\n", String::from_utf8(vec![b'.'; max_command_size + 1]).unwrap(), ) .into_bytes(); rt.run2( client.send(&large_command), server.receive_error_because_command_too_long(&large_command[..max_command_size]), ); } } #[test] fn command_with_literals_larger_than_max_command_size() { // The server will reject the login command because it's larger than the max size. // We use only single digit sizes for the password literal because otherwise the // size of the non-literal part would also change. let password_size_tests = [4, 5, 6, 7, 8, 9]; for password_size in password_size_tests { let max_command_size = 28; let mut setup = TestSetup::default(); setup .server_options .set_literal_accept_text("more data".to_owned()) .unwrap(); // Max literal size must be smaller than max command size setup.server_options.max_literal_size = password_size as u32; setup.server_options.max_command_size = max_command_size as u32; let (rt, mut server, mut client) = setup.setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); // Login command smaller than the max size can be received let login = b"A1 LOGIN {3}\r\nABC {3}\r\n...\r\n"; let continuation_request = b"+ more data\r\n"; rt.run2( async { client.send(&login[..14]).await; client.receive(continuation_request).await; client.send(&login[14..25]).await; client.receive(continuation_request).await; client.send(&login[25..]).await; }, server.receive_command(login), ); // Login command larger than the max size triggers an error let large_login = format!( "A1 LOGIN {{3}}\r\nABC {{{}}}\r\n{}\r\n", password_size, String::from_utf8(vec![b'.'; password_size]).unwrap(), ) .into_bytes(); rt.run2( async { client.send(&large_login[..14]).await; client.receive(continuation_request).await; client.send(&large_login[14..25]).await; client.receive(continuation_request).await; client.send(&large_login[25..]).await; }, server.receive_error_because_command_too_long(&large_login[..max_command_size]), ); } } #[test] fn idle_accepted() { let (rt, mut server, mut client) = TestSetup::default().setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); // Client starts IDLE let idle = b"A1 IDLE\r\n"; rt.run2(client.send(idle), server.receive_idle(idle)); // Server accepts IDLE let continuation_request = b"+ idling\r\n"; rt.run2( server.send_idle_accepted(continuation_request), client.receive(continuation_request), ); // Client ends IDLE let idle_done = b"DONE\r\n"; rt.run2(client.send(idle_done), server.receive_idle_done()); // Server is able to receive commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send(noop), server.receive_command(noop)); // Server is able to send responses let status = b"A2 OK ...\r\n"; rt.run2(server.send_status(status), client.receive(status)); } #[test] fn idle_rejected() { let (rt, mut server, mut client) = TestSetup::default().setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); // Client starts IDLE let idle = b"A1 IDLE\r\n"; rt.run2(client.send(idle), server.receive_idle(idle)); // Server rejects IDLE let status = b"A1 NO rise and shine\r\n"; rt.run2(server.send_idle_rejected(status), client.receive(status)); // Server is able to receive commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send(noop), server.receive_command(noop)); // Server is able to send responses let status = b"A2 OK ...\r\n"; rt.run2(server.send_status(status), client.receive(status)); } #[test] fn authenticate_accepted() { let (rt, mut server, mut client) = TestSetup::default().setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); // Client initiates AUTHENTICATE let authenticate = b"A1 AUTHENTICATE PLAIN dGVzdAB0ZXN0AHRlc3Q=\r\n"; rt.run2( client.send(authenticate), server.receive_authenticate_command(authenticate), ); // Server accepts AUTHENTICATE let status = b"A1 OK success\r\n"; rt.run2( server.send_authenticate_finish(status), client.receive(status), ); // Server is able to receive commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send(noop), server.receive_command(noop)); // Server is able to send responses let status = b"A2 OK ...\r\n"; rt.run2(server.send_status(status), client.receive(status)); } #[test] fn authenticate_with_more_data_accepted() { let (rt, mut server, mut client) = TestSetup::default().setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); // Client initiates AUTHENTICATE let authenticate = b"A1 AUTHENTICATE PLAIN\r\n"; rt.run2( client.send(authenticate), server.receive_authenticate_command(authenticate), ); // Server requests more data let continuation_request = b"+ \r\n"; rt.run2( server.send_authenticate_continue(continuation_request), client.receive(continuation_request), ); // Client sends more data let authenticate_data = b"dGVzdAB0ZXN0AHRlc3Q=\r\n"; rt.run2( client.send(authenticate_data), server.receive_authenticate_data(authenticate_data), ); // Server accepts AUTHENTICATE let status = b"A1 OK success\r\n"; rt.run2( server.send_authenticate_finish(status), client.receive(status), ); // Server is able to receive commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send(noop), server.receive_command(noop)); // Server is able to send responses let status = b"A2 OK ...\r\n"; rt.run2(server.send_status(status), client.receive(status)); } #[test] fn authenticate_rejected() { let (rt, mut server, mut client) = TestSetup::default().setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); // Client initiates AUTHENTICATE let authenticate = b"A1 AUTHENTICATE PLAIN dGVzdAB0ZXN0AHRlc3Q=\r\n"; rt.run2( client.send(authenticate), server.receive_authenticate_command(authenticate), ); // Server rejects AUTHENTICATE let status = b"A1 NO abort\r\n"; rt.run2( server.send_authenticate_finish(status), client.receive(status), ); // Server is able to receive commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send(noop), server.receive_command(noop)); // Server is able to send responses let status = b"A2 OK ...\r\n"; rt.run2(server.send_status(status), client.receive(status)); } #[test] fn authenticate_with_more_data_rejected() { let (rt, mut server, mut client) = TestSetup::default().setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); // Client initiates AUTHENTICATE let authenticate = b"A1 AUTHENTICATE PLAIN\r\n"; rt.run2( client.send(authenticate), server.receive_authenticate_command(authenticate), ); // Server requests more data let continuation_request = b"+ \r\n"; rt.run2( server.send_authenticate_continue(continuation_request), client.receive(continuation_request), ); // Client sends more data let authenticate_data = b"dGVzdAB0ZXN0AHRlc3Q=\r\n"; rt.run2( client.send(authenticate_data), server.receive_authenticate_data(authenticate_data), ); // Server rejects AUTHENTICATE let status = b"A1 NO abort\r\n"; rt.run2( server.send_authenticate_finish(status), client.receive(status), ); // Server is able to receive commands let noop = b"A2 NOOP\r\n"; rt.run2(client.send(noop), server.receive_command(noop)); // Server is able to send responses let status = b"A2 OK ...\r\n"; rt.run2(server.send_status(status), client.receive(status)); } #[test] fn stream_closed() { let (rt, mut server, mut client) = TestSetup::default().setup_server(); let greeting = b"* OK ...\r\n"; rt.run2(server.send_greeting(greeting), client.receive(greeting)); // Close stream drop(client); rt.run(server.receive_error_because_stream_closed()); } imap-next-0.3.1/justfile000066400000000000000000000106761472413535300152060ustar00rootroot00000000000000export RUSTFLAGS := "-D warnings" export RUSTDOCFLAGS := "-D warnings" [private] default: just -l --unsorted ########### ### RUN ### ########### # Run (local) CI ci: (ci_impl "" "" ) \ (ci_impl "" " --all-features") \ (ci_impl " --release" "" ) \ (ci_impl " --release" " --all-features") [private] ci_impl mode features: (check_impl mode features) (test_impl mode features) # Check syntax, formatting, clippy, deny, semver, ... check: (check_impl "" "" ) \ (check_impl "" " --all-features") \ (check_impl " --release" "" ) \ (check_impl " --release" " --all-features") [private] check_impl mode features: (cargo_check mode features) \ (cargo_hack mode) \ cargo_fmt \ (cargo_clippy mode features) \ cargo_deny \ cargo_semver [private] cargo_check mode features: cargo check --workspace --all-targets{{ mode }}{{ features }} cargo doc --no-deps --document-private-items --keep-going{{ mode }}{{ features }} [private] cargo_hack mode: install_cargo_hack cargo hack check --workspace --all-targets{{ mode }} [private] cargo_fmt: install_rust_nightly install_rust_nightly_fmt cargo +nightly fmt --check [private] cargo_clippy features mode: install_cargo_clippy cargo clippy --workspace --all-targets{{ features }}{{ mode }} [private] cargo_deny: install_cargo_deny cargo deny check [private] cargo_semver: install_cargo_semver_checks cargo semver-checks check-release --only-explicit-features -p imap-next # Test multiple configurations test: (test_impl "" "" ) \ (test_impl "" " --all-features") \ (test_impl " --release" "" ) \ (test_impl " --release" " --all-features") [private] test_impl mode features: (cargo_test mode features) [private] cargo_test features mode: cargo test --workspace --all-targets{{ features }}{{ mode }} # Audit advisories, bans, licenses, and sources audit: cargo_deny # Measure test coverage coverage: install_rust_llvm_tools_preview install_cargo_grcov mkdir -p target/coverage RUSTFLAGS="-Cinstrument-coverage" LLVM_PROFILE_FILE="coverage-%m-%p.profraw" cargo test -p imap-next -p integration-test --all-features grcov . \ --source-dir . \ --binary-path target/debug \ --branch \ --keep-only 'src/**' \ --output-types "lcov" \ --llvm > target/coverage/coverage.lcov # TODO: Create files in `target/coverage` only. rm *.profraw rm integration-test/*.profraw # Check MSRV check_msrv: install_rust_1_74 cargo +1.74 check --workspace --all-targets --all-features cargo +1.74 test --workspace --all-targets --all-features # Check minimal dependency versions check_minimal_dependency_versions: install_rust_nightly cargo +nightly update -Z minimal-versions cargo check --workspace --all-targets --all-features cargo test --workspace --all-targets --all-features cargo update ############### ### INSTALL ### ############### # Install required tooling (ahead of time) install: install_rust_1_74 \ install_rust_nightly \ install_rust_nightly_fmt \ install_rust_llvm_tools_preview \ install_cargo_clippy \ install_cargo_deny \ install_cargo_fuzz \ install_cargo_grcov \ install_cargo_hack \ install_cargo_semver_checks [private] install_rust_1_74: # Fix issue rustup update --no-self-update 1.74 rustup set profile minimal # rustup toolchain install 1.74 --profile minimal [private] install_rust_nightly: # Fix issue rustup update --no-self-update nightly rustup set profile minimal # rustup toolchain install nightly --profile minimal [private] install_rust_nightly_fmt: rustup component add --toolchain nightly rustfmt [private] install_rust_llvm_tools_preview: rustup component add llvm-tools-preview [private] install_cargo_clippy: rustup component add clippy [private] install_cargo_deny: cargo install --locked cargo-deny [private] install_cargo_fuzz: install_rust_nightly cargo install cargo-fuzz [private] install_cargo_grcov: cargo install grcov [private] install_cargo_hack: cargo install --locked cargo-hack [private] install_cargo_semver_checks: cargo install --locked cargo-semver-checks imap-next-0.3.1/rust-toolchain.toml000066400000000000000000000001401472413535300172670ustar00rootroot00000000000000[toolchain] channel = "stable" profile = "default" components = [ "rust-src", "rust-analyzer" ] imap-next-0.3.1/rustfmt.toml000066400000000000000000000001361472413535300160250ustar00rootroot00000000000000format_code_in_doc_comments=true group_imports="StdExternalCrate" imports_granularity="Crate" imap-next-0.3.1/shell.nix000066400000000000000000000006611472413535300152560ustar00rootroot00000000000000# Compatiblity file for non-flake Nix users. # # (import ( let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in fetchTarball { url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; sha256 = lock.nodes.flake-compat.locked.narHash; } ) { src = ./.; } ).shellNix imap-next-0.3.1/src/000077500000000000000000000000001472413535300142135ustar00rootroot00000000000000imap-next-0.3.1/src/client.rs000066400000000000000000000353051472413535300160450ustar00rootroot00000000000000use std::fmt::{Debug, Formatter}; use imap_codec::{ imap_types::{ auth::AuthenticateData, command::Command, response::{CommandContinuationRequest, Data, Greeting, Response, Status}, secret::Secret, }, AuthenticateDataCodec, CommandCodec, GreetingCodec, IdleDoneCodec, ResponseCodec, }; use thiserror::Error; use crate::{ client_send::{ClientSendEvent, ClientSendState, ClientSendTermination}, handle::{Handle, HandleGenerator, HandleGeneratorGenerator, RawHandle}, receive::{ReceiveError, ReceiveEvent, ReceiveState}, types::CommandAuthenticate, Interrupt, State, }; static HANDLE_GENERATOR_GENERATOR: HandleGeneratorGenerator = HandleGeneratorGenerator::new(); #[derive(Clone, Debug, PartialEq)] #[non_exhaustive] pub struct Options { pub crlf_relaxed: bool, /// Max response size that can be parsed by the client. /// /// Bigger responses raise an error. pub max_response_size: u32, /// Forces the client not to handle greetings. /// /// If `true`, the next expected message is directly set to /// `NextExpectedMessage::Response`. This is particularly useful /// when greetings are processed outside of the [`Client`] scope /// (e.g. custom STARTTLS). pub discard_greeting: bool, } #[allow(clippy::derivable_impls)] impl Default for Options { fn default() -> Self { Self { // Lean towards conformity crlf_relaxed: false, // We use a value larger than the default `server::Options::max_command_size` // because we assume that a client has usually less resource constraints than the // server. max_response_size: 100 * 1024 * 1024, // Process greetings by default discard_greeting: false, } } } pub struct Client { handle_generator: HandleGenerator, send_state: ClientSendState, receive_state: ReceiveState, next_expected_message: NextExpectedMessage, } impl Client { pub fn new(options: Options) -> Self { let send_state = ClientSendState::new( CommandCodec::default(), AuthenticateDataCodec::default(), IdleDoneCodec::default(), ); let receive_state = ReceiveState::new(options.crlf_relaxed, Some(options.max_response_size)); let next_expected_message = if options.discard_greeting { NextExpectedMessage::Response(ResponseCodec::default()) } else { NextExpectedMessage::Greeting(GreetingCodec::default()) }; Self { handle_generator: HANDLE_GENERATOR_GENERATOR.generate(), send_state, receive_state, next_expected_message, } } /// Enqueues the [`Command`] for being sent to the client. /// /// The [`Command`] is not sent immediately but during one of the next calls of /// [`Client::next`]. All [`Command`]s are sent in the same order they have been /// enqueued. pub fn enqueue_command(&mut self, command: Command<'static>) -> CommandHandle { let handle = self.handle_generator.generate(); self.send_state.enqueue_command(handle, command); handle } fn progress_send(&mut self) -> Result, Interrupt> { // Abort if we didn't received the greeting yet if let NextExpectedMessage::Greeting(_) = &self.next_expected_message { return Ok(None); } match self.send_state.next() { Ok(Some(ClientSendEvent::Command { handle, command })) => { Ok(Some(Event::CommandSent { handle, command })) } Ok(Some(ClientSendEvent::Authenticate { handle })) => { Ok(Some(Event::AuthenticateStarted { handle })) } Ok(Some(ClientSendEvent::Idle { handle })) => { Ok(Some(Event::IdleCommandSent { handle })) } Ok(Some(ClientSendEvent::IdleDone { handle })) => { Ok(Some(Event::IdleDoneSent { handle })) } Ok(None) => Ok(None), Err(Interrupt::Io(io)) => Err(Interrupt::Io(io)), Err(Interrupt::Error(_)) => unreachable!(), } } fn progress_receive(&mut self) -> Result, Interrupt> { let event = loop { match &self.next_expected_message { NextExpectedMessage::Greeting(codec) => { match self.receive_state.next::(codec) { Ok(ReceiveEvent::DecodingSuccess(greeting)) => { self.next_expected_message = NextExpectedMessage::Response(ResponseCodec::default()); break Some(Event::GreetingReceived { greeting }); } Ok(ReceiveEvent::LiteralAnnouncement { .. }) => { // Unexpected literal, let's continue and see what happens continue; } Err(interrupt) => return Err(handle_receive_interrupt(interrupt)), } } NextExpectedMessage::Response(codec) => { let response = match self.receive_state.next::(codec) { Ok(ReceiveEvent::DecodingSuccess(response)) => response, Ok(ReceiveEvent::LiteralAnnouncement { .. }) => { // The client must accept the literal in any case. continue; } Err(interrupt) => return Err(handle_receive_interrupt(interrupt)), }; match response { Response::Status(status) => { let event = if let Some(finish_result) = self.send_state.maybe_terminate(&status) { match finish_result { ClientSendTermination::LiteralRejected { handle, command } => { Event::CommandRejected { handle, command, status, } } ClientSendTermination::AuthenticateAccepted { handle, command_authenticate, } | ClientSendTermination::AuthenticateRejected { handle, command_authenticate, } => Event::AuthenticateStatusReceived { handle, command_authenticate, status, }, ClientSendTermination::IdleRejected { handle } => { Event::IdleRejected { handle, status } } } } else { Event::StatusReceived { status } }; break Some(event); } Response::Data(data) => break Some(Event::DataReceived { data }), Response::CommandContinuationRequest(continuation_request) => { if self.send_state.literal_continue() { // We received a continuation request that was necessary for // sending a command. So we abort receiving responses for now // and continue with sending commands. break None; } else if let Some(handle) = self.send_state.authenticate_continue() { break Some(Event::AuthenticateContinuationRequestReceived { handle, continuation_request, }); } else if let Some(handle) = self.send_state.idle_continue() { break Some(Event::IdleAccepted { handle, continuation_request, }); } else { break Some(Event::ContinuationRequestReceived { continuation_request, }); } } } } } }; Ok(event) } pub fn set_authenticate_data( &mut self, authenticate_data: AuthenticateData<'static>, ) -> Result> { self.send_state.set_authenticate_data(authenticate_data) } pub fn set_idle_done(&mut self) -> Option { self.send_state.set_idle_done() } } impl Debug for Client { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { f.debug_struct("Client") .field("handle_generator", &self.handle_generator) .finish_non_exhaustive() } } impl State for Client { type Event = Event; type Error = Error; fn enqueue_input(&mut self, bytes: &[u8]) { self.receive_state.enqueue_input(bytes); } fn next(&mut self) -> Result> { loop { if let Some(event) = self.progress_send()? { return Ok(event); } if let Some(event) = self.progress_receive()? { return Ok(event); } } } } fn handle_receive_interrupt(interrupt: Interrupt) -> Interrupt { match interrupt { Interrupt::Io(io) => Interrupt::Io(io), Interrupt::Error(ReceiveError::DecodingFailure { discarded_bytes }) => { Interrupt::Error(Error::MalformedMessage { discarded_bytes }) } Interrupt::Error(ReceiveError::ExpectedCrlfGotLf { discarded_bytes }) => { Interrupt::Error(Error::ExpectedCrlfGotLf { discarded_bytes }) } Interrupt::Error(ReceiveError::MessageIsPoisoned { .. }) => { // Unreachable because we don't poison messages unreachable!() } Interrupt::Error(ReceiveError::MessageTooLong { discarded_bytes }) => { Interrupt::Error(Error::ResponseTooLong { discarded_bytes }) } } } /// Handle for enqueued [`Command`]. /// /// This handle can be used to track the sending progress. After a [`Command`] was enqueued via /// [`Client::enqueue_command`] it is in the process of being sent until [`Client::next`] returns /// a [`Event::CommandSent`] or [`Event::CommandRejected`] with the corresponding handle. #[derive(Clone, Copy, Eq, PartialEq, Hash)] pub struct CommandHandle(RawHandle); /// Debug representation hiding the raw handle. impl Debug for CommandHandle { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_tuple("CommandHandle") .field(&self.0.generator_id()) .field(&self.0.handle_id()) .finish() } } impl Handle for CommandHandle { fn from_raw(handle: RawHandle) -> Self { Self(handle) } } #[derive(Debug)] pub enum Event { /// [`Greeting`] received. GreetingReceived { greeting: Greeting<'static> }, /// [`Command`] sent completely. CommandSent { /// Handle to the enqueued [`Command`]. handle: CommandHandle, /// Formerly enqueued [`Command`]. command: Command<'static>, }, /// [`Command`] rejected due to literal. CommandRejected { /// Handle to enqueued [`Command`]. handle: CommandHandle, /// Formerly enqueued [`Command`]. command: Command<'static>, /// [`Status`] sent by the server to reject the [`Command`]. /// /// Note: [`Client`] already handled this [`Status`] but it might still have /// useful information that could be logged or displayed to the user /// (e.g. [`Code::Alert`](crate::imap_types::response::Code::Alert)). status: Status<'static>, }, /// AUTHENTICATE sent. AuthenticateStarted { handle: CommandHandle }, /// Server requests (more) authentication data. /// /// The client MUST call [`Client::set_authenticate_data`] next. /// /// Note: The client can also progress the authentication by sending [`AuthenticateData::Cancel`]. /// However, it's up to the server to abort the authentication flow by sending a tagged status response. AuthenticateContinuationRequestReceived { /// Handle to the enqueued [`Command`]. handle: CommandHandle, continuation_request: CommandContinuationRequest<'static>, }, /// [`Status`] received to authenticate command. AuthenticateStatusReceived { handle: CommandHandle, command_authenticate: CommandAuthenticate, status: Status<'static>, }, /// IDLE sent. IdleCommandSent { handle: CommandHandle }, /// IDLE accepted by server. Entering IDLE state. IdleAccepted { handle: CommandHandle, continuation_request: CommandContinuationRequest<'static>, }, /// IDLE rejected by server. IdleRejected { handle: CommandHandle, status: Status<'static>, }, /// DONE sent. Exiting IDLE state. IdleDoneSent { handle: CommandHandle }, /// Server [`Data`] received. DataReceived { data: Data<'static> }, /// Server [`Status`] received. StatusReceived { status: Status<'static> }, /// Server [`CommandContinuationRequest`] response received. /// /// Note: The received continuation request was not part of [`Client`] handling. ContinuationRequestReceived { continuation_request: CommandContinuationRequest<'static>, }, } #[derive(Debug, Error)] pub enum Error { #[error("Expected `\\r\\n`, got `\\n`")] ExpectedCrlfGotLf { discarded_bytes: Secret> }, #[error("Received malformed message")] MalformedMessage { discarded_bytes: Secret> }, #[error("Response is too long")] ResponseTooLong { discarded_bytes: Secret> }, } #[derive(Clone, Debug)] enum NextExpectedMessage { Greeting(GreetingCodec), Response(ResponseCodec), } imap-next-0.3.1/src/client_send.rs000066400000000000000000000624721472413535300170630ustar00rootroot00000000000000use std::{collections::VecDeque, convert::Infallible}; use imap_codec::{ encode::{Encoder, Fragment}, imap_types::{ auth::AuthenticateData, command::{Command, CommandBody}, core::{LiteralMode, Tag}, extensions::idle::IdleDone, response::{Status, StatusBody, StatusKind, Tagged}, }, AuthenticateDataCodec, CommandCodec, IdleDoneCodec, }; use tracing::warn; use crate::{client::CommandHandle, types::CommandAuthenticate, Interrupt, Io}; pub struct ClientSendState { command_codec: CommandCodec, authenticate_data_codec: AuthenticateDataCodec, idle_done_codec: IdleDoneCodec, /// FIFO queue for messages that should be sent next. queued_messages: VecDeque, /// Message that is currently being sent. current_message: Option, } impl ClientSendState { pub fn new( command_codec: CommandCodec, authenticate_data_codec: AuthenticateDataCodec, idle_done_codec: IdleDoneCodec, ) -> Self { Self { command_codec, authenticate_data_codec, idle_done_codec, queued_messages: VecDeque::new(), current_message: None, } } pub fn enqueue_command(&mut self, handle: CommandHandle, command: Command<'static>) { self.queued_messages .push_back(QueuedMessage { handle, command }); } /// Terminates the current message depending on the received status. pub fn maybe_terminate(&mut self, status: &Status) -> Option { // TODO: Do we want more checks on the state? Was idle already accepted? Does the command even has a literal? etc. // If we reach one of the return statements, the current message will be removed let current_message = self.current_message.take()?; self.current_message = Some(match current_message { CurrentMessage::Command(state) => { // Check if status matches the current command if let Status::Tagged(Tagged { tag, body: StatusBody { kind, .. }, .. }) = status { if *kind == StatusKind::Bad && tag == &state.command.tag { // Terminate command because literal was rejected return Some(ClientSendTermination::LiteralRejected { handle: state.handle, command: state.command, }); } } CurrentMessage::Command(state) } CurrentMessage::Authenticate(state) => { // Check if status matches the current authenticate command if let Status::Tagged(Tagged { tag, body: StatusBody { kind, .. }, .. }) = status { if tag == &state.command_authenticate.tag { match kind { StatusKind::Ok => { // Terminate authenticate command because it was accepted return Some(ClientSendTermination::AuthenticateAccepted { handle: state.handle, command_authenticate: state.command_authenticate, }); } StatusKind::No | StatusKind::Bad => { // Terminate authenticate command because it was rejected return Some(ClientSendTermination::AuthenticateRejected { handle: state.handle, command_authenticate: state.command_authenticate, }); } }; } } CurrentMessage::Authenticate(state) } CurrentMessage::Idle(state) => { // Check if status matches the current idle command if let Status::Tagged(Tagged { tag, body: StatusBody { kind, .. }, .. }) = status { if tag == &state.tag { if matches!(kind, StatusKind::Ok | StatusKind::Bad) { warn!(got=?status, "Expected command continuation request response or NO command completion result"); warn!("Interpreting as IDLE rejected"); } // Terminate idle command because it was rejected return Some(ClientSendTermination::IdleRejected { handle: state.handle, }); } } CurrentMessage::Idle(state) } }); None } /// Handles the received continuation request for a literal. pub fn literal_continue(&mut self) -> bool { // Check whether in correct state let Some(current_message) = self.current_message.take() else { return false; }; let CurrentMessage::Command(state) = current_message else { self.current_message = Some(current_message); return false; }; let CommandActivity::WaitingForLiteralAccepted { limbo_literal } = state.activity else { self.current_message = Some(CurrentMessage::Command(state)); return false; }; // Change state self.current_message = Some(CurrentMessage::Command(CommandState { activity: CommandActivity::PushingFragments { accepted_literal: Some(limbo_literal), }, ..state })); true } /// Handles the received continuation request for an authenticate data. pub fn authenticate_continue(&mut self) -> Option { // Check whether in correct state let current_message = self.current_message.take()?; let CurrentMessage::Authenticate(state) = current_message else { self.current_message = Some(current_message); return None; }; let AuthenticateActivity::WaitingForAuthenticateResponse = state.activity else { self.current_message = Some(CurrentMessage::Authenticate(state)); return None; }; // Change state self.current_message = Some(CurrentMessage::Authenticate(AuthenticateState { activity: AuthenticateActivity::WaitingForAuthenticateDataSet, ..state })); Some(state.handle) } /// Takes the requested authenticate data and sends it to the server. pub fn set_authenticate_data( &mut self, authenticate_data: AuthenticateData<'static>, ) -> Result> { // Check whether in correct state let Some(current_message) = self.current_message.take() else { return Err(authenticate_data); }; let CurrentMessage::Authenticate(state) = current_message else { self.current_message = Some(current_message); return Err(authenticate_data); }; let AuthenticateActivity::WaitingForAuthenticateDataSet = state.activity else { self.current_message = Some(CurrentMessage::Authenticate(state)); return Err(authenticate_data); }; // Encode authenticate data let mut fragments = self.authenticate_data_codec.encode(&authenticate_data); // Authenticate data is a single line by definition let Some(Fragment::Line { data: authenticate_data, }) = fragments.next() else { unreachable!() }; assert!(fragments.next().is_none()); // Change state self.current_message = Some(CurrentMessage::Authenticate(AuthenticateState { activity: AuthenticateActivity::PushingAuthenticateData { authenticate_data }, ..state })); Ok(state.handle) } /// Handles the received continuation request for the idle done. pub fn idle_continue(&mut self) -> Option { // Check whether in correct state let current_message = self.current_message.take()?; let CurrentMessage::Idle(state) = current_message else { self.current_message = Some(current_message); return None; }; let IdleActivity::WaitingForIdleResponse = state.activity else { self.current_message = Some(CurrentMessage::Idle(state)); return None; }; // Change state self.current_message = Some(CurrentMessage::Idle(IdleState { activity: IdleActivity::WaitingForIdleDoneSet, ..state })); Some(state.handle) } /// Sends the requested idle done to the server. pub fn set_idle_done(&mut self) -> Option { // Check whether in correct state let current_message = self.current_message.take()?; let CurrentMessage::Idle(state) = current_message else { self.current_message = Some(current_message); return None; }; let IdleActivity::WaitingForIdleDoneSet = state.activity else { self.current_message = Some(CurrentMessage::Idle(state)); return None; }; // Encode idle done let mut fragments = self.idle_done_codec.encode(&IdleDone); // Idle done is a single line by defintion let Some(Fragment::Line { data: idle_done, .. }) = fragments.next() else { unreachable!() }; assert!(fragments.next().is_none()); // Change state let handle = state.handle; self.current_message = Some(CurrentMessage::Idle(IdleState { activity: IdleActivity::PushingIdleDone { idle_done }, ..state })); Some(handle) } pub fn next(&mut self) -> Result, Interrupt> { let current_message = match self.current_message.take() { Some(current_message) => { // We are currently sending a message but the sending process was aborted for one // of these reasons: // - The state was interrupted // - The server must send a continuation request or a status // - The client user must provide more data // Continue the sending process. current_message } None => { let Some(queued_message) = self.queued_messages.pop_front() else { // There is currently no message that needs to be sent return Ok(None); }; queued_message.start(&self.command_codec) } }; // Creates a buffer for writing the current message let mut write_buffer = Vec::new(); // Push as many bytes of the message as possible to the buffer let current_message = current_message.push_to_buffer(&mut write_buffer); if write_buffer.is_empty() { // Inform the state of the current message that all bytes are sent match current_message.finish_sending() { FinishSendingResult::Uncompleted { state: current_message, event, } => { // Message is not finished yet self.current_message = Some(current_message); Ok(event) } FinishSendingResult::Completed { event } => { // Message was sent completely Ok(Some(event)) } } } else { // Store the current message, we'll continue later self.current_message = Some(current_message); // Interrupt the state for sending all bytes of current message Err(Interrupt::Io(Io::Output(write_buffer))) } } } /// Queued (and not sent yet) message. struct QueuedMessage { handle: CommandHandle, command: Command<'static>, } impl QueuedMessage { /// Start the sending process for this message. fn start(self, codec: &CommandCodec) -> CurrentMessage { let handle = self.handle; let command = self.command; let mut fragments = codec.encode(&command); let tag = command.tag; match command.body { CommandBody::Authenticate { mechanism, initial_response, } => { // The authenticate command is a single line by definition let Some(Fragment::Line { data: authenticate }) = fragments.next() else { unreachable!() }; assert!(fragments.next().is_none()); CurrentMessage::Authenticate(AuthenticateState { handle, command_authenticate: CommandAuthenticate { tag, mechanism, initial_response, }, activity: AuthenticateActivity::PushingAuthenticate { authenticate }, }) } CommandBody::Idle => { // The idle command is a single line by definition let Some(Fragment::Line { data: idle }) = fragments.next() else { unreachable!() }; assert!(fragments.next().is_none()); CurrentMessage::Idle(IdleState { handle, tag, activity: IdleActivity::PushingIdle { idle }, }) } body => CurrentMessage::Command(CommandState { handle, command: Command { tag, body }, fragments: fragments.collect(), activity: CommandActivity::PushingFragments { accepted_literal: None, }, }), } } } /// Currently being sent message. enum CurrentMessage { /// Sending state of regular command. Command(CommandState), /// Sending state of authenticate command. Authenticate(AuthenticateState), /// Sending state of idle command. Idle(IdleState), } impl CurrentMessage { /// Pushes as many bytes as possible from the message to the buffer. fn push_to_buffer(self, write_buffer: &mut Vec) -> Self { match self { Self::Command(state) => Self::Command(state.push_to_buffer(write_buffer)), Self::Authenticate(state) => Self::Authenticate(state.push_to_buffer(write_buffer)), Self::Idle(state) => Self::Idle(state.push_to_buffer(write_buffer)), } } /// Updates the state after all bytes were sent. fn finish_sending(self) -> FinishSendingResult { match self { Self::Command(state) => state.finish_sending().map_state(Self::Command), Self::Authenticate(state) => state.finish_sending().map_state(Self::Authenticate), Self::Idle(state) => state.finish_sending().map_state(Self::Idle), } } } /// Updated message state after sending all bytes, see `finish_sending`. enum FinishSendingResult { /// Message not finished yet. Uncompleted { /// Updated message state. state: S, /// Event that needs to be returned by `progress`. event: Option, }, /// Message sent completely. Completed { /// Event that needs to be returned by `progress`. event: ClientSendEvent, }, } impl FinishSendingResult { fn map_state(self, f: impl Fn(S) -> T) -> FinishSendingResult { match self { FinishSendingResult::Uncompleted { state, event } => FinishSendingResult::Uncompleted { state: f(state), event, }, FinishSendingResult::Completed { event } => FinishSendingResult::Completed { event }, } } } struct CommandState { handle: CommandHandle, command: Command<'static>, /// Outstanding command fragments that needs to be sent. fragments: VecDeque, activity: CommandActivity, } impl CommandState { fn push_to_buffer(self, write_buffer: &mut Vec) -> Self { let mut fragments = self.fragments; let activity = match self.activity { CommandActivity::PushingFragments { accepted_literal } => { // First push the accepted literal if available if let Some(data) = accepted_literal { write_buffer.extend(data); } // Push as many fragments as possible let limbo_literal = loop { match fragments.pop_front() { Some( Fragment::Line { data } | Fragment::Literal { data, mode: LiteralMode::NonSync, }, ) => { write_buffer.extend(data); } Some(Fragment::Literal { data, mode: LiteralMode::Sync, }) => { // Stop pushing fragments because a literal needs to be accepted // by the server break Some(data); } None => break None, }; }; // Done with pushing CommandActivity::WaitingForFragmentsSent { limbo_literal } } activity => activity, }; Self { fragments, activity, ..self } } fn finish_sending(self) -> FinishSendingResult { match self.activity { CommandActivity::WaitingForFragmentsSent { limbo_literal } => match limbo_literal { Some(limbo_literal) => FinishSendingResult::Uncompleted { state: Self { activity: CommandActivity::WaitingForLiteralAccepted { limbo_literal }, ..self }, event: None, }, None => FinishSendingResult::Completed { event: ClientSendEvent::Command { handle: self.handle, command: self.command, }, }, }, activity => FinishSendingResult::Uncompleted { state: Self { activity, ..self }, event: None, }, } } } enum CommandActivity { /// Pushing fragments to the write buffer. PushingFragments { /// Literal that was accepted by the server and needs to be sent before the fragments. accepted_literal: Option>, }, /// Waiting until the pushed fragments are sent. WaitingForFragmentsSent { /// Literal that needs to be accepted by the server after the pushed fragments are sent. limbo_literal: Option>, }, /// Waiting until the server accepts the literal via continuation request or rejects it /// via status. WaitingForLiteralAccepted { /// Literal that needs to be accepted by the server. limbo_literal: Vec, }, } struct AuthenticateState { handle: CommandHandle, command_authenticate: CommandAuthenticate, activity: AuthenticateActivity, } impl AuthenticateState { fn push_to_buffer(self, write_buffer: &mut Vec) -> Self { let activity = match self.activity { AuthenticateActivity::PushingAuthenticate { authenticate } => { write_buffer.extend(authenticate); AuthenticateActivity::WaitingForAuthenticateSent } AuthenticateActivity::PushingAuthenticateData { authenticate_data } => { write_buffer.extend(authenticate_data); AuthenticateActivity::WaitingForAuthenticateDataSent } activity => activity, }; Self { activity, ..self } } fn finish_sending(self) -> FinishSendingResult { match self.activity { AuthenticateActivity::WaitingForAuthenticateSent => FinishSendingResult::Uncompleted { state: Self { activity: AuthenticateActivity::WaitingForAuthenticateResponse, ..self }, event: Some(ClientSendEvent::Authenticate { handle: self.handle, }), }, AuthenticateActivity::WaitingForAuthenticateDataSent => { FinishSendingResult::Uncompleted { state: Self { activity: AuthenticateActivity::WaitingForAuthenticateResponse, ..self }, event: None, } } activity => FinishSendingResult::Uncompleted { state: Self { activity, ..self }, event: None, }, } } } enum AuthenticateActivity { /// Pushing the authenticate command to the write buffer. PushingAuthenticate { authenticate: Vec }, /// Waiting until the pushed authenticate command is sent. WaitingForAuthenticateSent, /// Waiting until the server requests more authenticate data via continuation request or /// accepts/rejects the authenticate command via status. WaitingForAuthenticateResponse, /// Waiting until the client user provides the authenticate data. /// /// Specifically, [`Client::set_authenticate_data`](crate::client::Client::set_authenticate_data). WaitingForAuthenticateDataSet, /// Pushing the authenticate data to the write buffer. PushingAuthenticateData { authenticate_data: Vec }, /// Waiting until the pushed authenticate data is sent. WaitingForAuthenticateDataSent, } struct IdleState { handle: CommandHandle, tag: Tag<'static>, activity: IdleActivity, } impl IdleState { fn push_to_buffer(self, write_buffer: &mut Vec) -> Self { let activity = match self.activity { IdleActivity::PushingIdle { idle } => { write_buffer.extend(idle); IdleActivity::WaitingForIdleSent } IdleActivity::PushingIdleDone { idle_done } => { write_buffer.extend(idle_done); IdleActivity::WaitingForIdleDoneSent } activity => activity, }; Self { activity, ..self } } fn finish_sending(self) -> FinishSendingResult { match self.activity { IdleActivity::WaitingForIdleSent => FinishSendingResult::Uncompleted { state: Self { activity: IdleActivity::WaitingForIdleResponse, ..self }, event: Some(ClientSendEvent::Idle { handle: self.handle, }), }, IdleActivity::WaitingForIdleDoneSent => FinishSendingResult::Completed { event: ClientSendEvent::IdleDone { handle: self.handle, }, }, activity => FinishSendingResult::Uncompleted { state: Self { activity, ..self }, event: None, }, } } } enum IdleActivity { /// Pushing the idle command to the write buffer. PushingIdle { idle: Vec }, /// Waiting until the pushed idle command is sent. WaitingForIdleSent, /// Waiting until the server accepts the idle command via continuation request or rejects it /// via status. WaitingForIdleResponse, /// Waiting until the client user triggers idle done. /// /// Specifically, [`Client::set_idle_done`](crate::client::Client::set_idle_done). WaitingForIdleDoneSet, /// Pushing the idle done to the write buffer. PushingIdleDone { idle_done: Vec }, /// Waiting until the pushed idle done is sent. WaitingForIdleDoneSent, } /// Message sent. pub enum ClientSendEvent { Command { handle: CommandHandle, command: Command<'static>, }, Authenticate { handle: CommandHandle, }, Idle { handle: CommandHandle, }, IdleDone { handle: CommandHandle, }, } /// Message was terminated via [`ClientSendState::maybe_terminate`]. pub enum ClientSendTermination { /// Command was terminated because its literal was rejected by the server. LiteralRejected { handle: CommandHandle, command: Command<'static>, }, /// Authenticate command was accepted. AuthenticateAccepted { handle: CommandHandle, command_authenticate: CommandAuthenticate, }, /// Authenticate command was rejected. AuthenticateRejected { handle: CommandHandle, command_authenticate: CommandAuthenticate, }, /// Idle command was rejected. IdleRejected { handle: CommandHandle }, } imap-next-0.3.1/src/handle.rs000066400000000000000000000056021472413535300160170ustar00rootroot00000000000000use std::{ fmt::{Debug, Formatter}, marker::PhantomData, sync::atomic::{AtomicUsize, Ordering}, }; pub trait Handle { fn from_raw(raw_handle: RawHandle) -> Self; } #[derive(Clone, Copy, Eq, PartialEq, Hash)] pub struct RawHandle { generator_id: usize, handle_id: usize, } impl RawHandle { pub fn generator_id(&self) -> usize { self.generator_id } pub fn handle_id(&self) -> usize { self.handle_id } } pub struct HandleGenerator { /// This ID is used to bind the handles to the generator instance, i.e. it's possible to /// distinguish handles generated by different generators. We hope that this might /// prevent bugs when the library user is dealing with handles from different sources. generator_id: usize, next_handle_id: usize, _h: PhantomData, } impl Debug for HandleGenerator { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { f.debug_struct("HandleGenerator") .field("generator_id", &self.generator_id) .field("next_handle_id", &self.next_handle_id) .finish_non_exhaustive() } } impl HandleGenerator { pub fn generate(&mut self) -> H { let handle_id = self.next_handle_id; self.next_handle_id = self.next_handle_id.wrapping_add(1); H::from_raw(RawHandle { generator_id: self.generator_id, handle_id, }) } } pub struct HandleGeneratorGenerator { next_handle_generator_id: AtomicUsize, _h: PhantomData, } impl HandleGeneratorGenerator { pub const fn new() -> Self { Self { next_handle_generator_id: AtomicUsize::new(0), _h: PhantomData, } } pub fn generate(&self) -> HandleGenerator { // There is no synchronization required and we only care about each thread seeing a // unique value. let generator_id = self .next_handle_generator_id .fetch_add(1, Ordering::Relaxed); HandleGenerator { generator_id, next_handle_id: 0, _h: PhantomData, } } } #[cfg(test)] mod tests { use super::{Handle, HandleGeneratorGenerator, RawHandle}; struct TestHandle(RawHandle); impl Handle for TestHandle { fn from_raw(raw_handle: RawHandle) -> Self { Self(raw_handle) } } #[test] fn generated_handles_have_expected_ids() { let gen_gen = HandleGeneratorGenerator::::new(); for expected_generator_id in 0..100 { let mut gen = gen_gen.generate(); for expected_handle_id in 0..100 { let handle = gen.generate(); assert_eq!(expected_generator_id, handle.0.generator_id); assert_eq!(expected_handle_id, handle.0.handle_id); } } } } imap-next-0.3.1/src/lib.rs000066400000000000000000000042411472413535300153300ustar00rootroot00000000000000#![forbid(unsafe_code)] pub mod client; mod client_send; mod handle; mod receive; pub mod server; mod server_send; #[cfg(feature = "stream")] pub mod stream; #[cfg(test)] mod tests; pub mod types; // Re-export(s) pub use imap_codec::imap_types; // Test examples from imap-next's README. #[doc = include_str!("../README.md")] #[cfg(doctest)] pub struct ReadmeDoctests; /// State machine with sans I/O pattern. /// /// This trait is the interface between types that implement IMAP protocol flows and I/O drivers. /// Most notably [`Client`](client::Client) and [`Server`](server::Server) both implement /// this trait whereas [`Stream`](stream::Stream) uses the trait for implementing the I/O drivers. pub trait State { /// Event emitted while progressing the state. type Event; /// Error emitted while progressing the state. type Error; /// Enqueue input bytes. /// /// These bytes may be used during the next [`Self::next`] call. fn enqueue_input(&mut self, bytes: &[u8]); /// Progress the state until the next event (or interrupt). fn next(&mut self) -> Result>; } impl State for &mut F { type Event = F::Event; type Error = F::Error; fn enqueue_input(&mut self, bytes: &[u8]) { (*self).enqueue_input(bytes); } fn next(&mut self) -> Result> { (*self).next() } } /// State progression was interrupted by an event that needs to be handled externally. #[must_use = "If state progression is interrupted the interrupt must be handled. Ignoring this might result in a deadlock on IMAP level"] #[derive(Clone, Debug, Eq, PartialEq)] pub enum Interrupt { /// An IO operation is necessary. Ignoring this might result in a deadlock on IMAP level. Io(Io), /// An error occurred. Ignoring this might result in an undefined IMAP state. Error(E), } /// User of `imap-next` must perform an IO operation to progress the state. #[derive(Clone, Debug, Eq, PartialEq)] pub enum Io { /// More bytes must be read and passed to [`State::enqueue_input`]. NeedMoreInput, /// Given bytes must be written. Output(Vec), } imap-next-0.3.1/src/receive.rs000066400000000000000000000155001472413535300162040ustar00rootroot00000000000000use imap_codec::{ decode::Decoder, fragmentizer::{ DecodeMessageError, FragmentInfo, Fragmentizer, LineEnding, LiteralAnnouncement, }, imap_types::{ core::{LiteralMode, Tag}, secret::Secret, IntoStatic, }, }; use crate::{Interrupt, Io}; pub struct ReceiveState { crlf_relaxed: bool, fragmentizer: Fragmentizer, message_has_invalid_line_ending: bool, } impl ReceiveState { pub fn new(crlf_relaxed: bool, max_message_size: Option) -> Self { let fragmentizer = match max_message_size { Some(max_message_size) => Fragmentizer::new(max_message_size), None => Fragmentizer::without_max_message_size(), }; Self { crlf_relaxed, fragmentizer, message_has_invalid_line_ending: false, } } pub fn enqueue_input(&mut self, bytes: &[u8]) { self.fragmentizer.enqueue_bytes(bytes); } /// Discard the current message immediately without receiving it completely. /// /// This operation is dangerous because the next message might start in untrusted bytes. /// You should only use it if you can reasonably assume that you won't receive the remaining /// bytes of the message, e.g. the server rejected the literal of message. pub fn discard_message(&mut self) -> Secret> { let discarded_bytes = Secret::new(self.fragmentizer.message_bytes().into()); self.fragmentizer.skip_message(); discarded_bytes } /// Discard the current message once it will be received completely. /// /// This operation is safe because it ensures that next message will start at a sane point. /// To achieve this the fragments of the current message will be parsed until the end of the /// message. Then the message will be discarded without being decoded. pub fn poison_message(&mut self) { self.fragmentizer.poison_message(); } /// Tries to decode the tag of the current message before it was received completely. pub fn message_tag(&self) -> Option> { let tag = self.fragmentizer.decode_tag()?; Some(tag.into_static()) } pub fn next(&mut self, codec: &C) -> Result, Interrupt> where C: Decoder, for<'a> C::Message<'a>: IntoStatic>, { loop { // Parse the next fragment let fragment_info = self.fragmentizer.progress(); // We only need to handle line fragments match fragment_info { Some(FragmentInfo::Line { announcement, ending, .. }) => { // Check for line ending compatibility if !self.crlf_relaxed && ending == LineEnding::Lf { self.fragmentizer.poison_message(); self.message_has_invalid_line_ending = true; } match announcement { Some(LiteralAnnouncement { mode, length }) => { // The line announces a literal, allow the caller to handle it return Ok(ReceiveEvent::LiteralAnnouncement { mode, length }); } None => { // The message is now complete and can be decoded let result = match self.fragmentizer.decode_message(codec) { Ok(message) => { Ok(ReceiveEvent::DecodingSuccess(message.into_static())) } Err(DecodeMessageError::DecodingFailure(_)) => { let discarded_bytes = Secret::new(self.fragmentizer.message_bytes().into()); Err(Interrupt::Error(ReceiveError::DecodingFailure { discarded_bytes, })) } Err(DecodeMessageError::DecodingRemainder { .. }) => { let discarded_bytes = Secret::new(self.fragmentizer.message_bytes().into()); Err(Interrupt::Error(ReceiveError::DecodingFailure { discarded_bytes, })) } Err(DecodeMessageError::MessageTooLong { .. }) => { let discarded_bytes = Secret::new(self.fragmentizer.message_bytes().into()); Err(Interrupt::Error(ReceiveError::MessageTooLong { discarded_bytes, })) } Err(DecodeMessageError::MessagePoisoned { .. }) => { let discarded_bytes = Secret::new(self.fragmentizer.message_bytes().into()); if self.message_has_invalid_line_ending { Err(Interrupt::Error(ReceiveError::ExpectedCrlfGotLf { discarded_bytes, })) } else { Err(Interrupt::Error(ReceiveError::MessageIsPoisoned { discarded_bytes, })) } } }; self.message_has_invalid_line_ending = false; return result; } } } Some(FragmentInfo::Literal { .. }) => { // We don't need to handle literal fragments continue; } None => { // Not enough bytes for decoding the message, request more bytes return Err(Interrupt::Io(Io::NeedMoreInput)); } } } } } pub enum ReceiveEvent { DecodingSuccess(C::Message<'static>), LiteralAnnouncement { mode: LiteralMode, length: u32 }, } pub enum ReceiveError { DecodingFailure { discarded_bytes: Secret> }, ExpectedCrlfGotLf { discarded_bytes: Secret> }, MessageIsPoisoned { discarded_bytes: Secret> }, MessageTooLong { discarded_bytes: Secret> }, } imap-next-0.3.1/src/server.rs000066400000000000000000000530351472413535300160750ustar00rootroot00000000000000use std::fmt::{Debug, Formatter}; use imap_codec::{ imap_types::{ auth::AuthenticateData, command::{Command, CommandBody}, core::{LiteralMode, Tag, Text}, extensions::idle::IdleDone, response::{ CommandContinuationRequest, CommandContinuationRequestBasic, Data, Greeting, Response, Status, }, secret::Secret, ToStatic, }, AuthenticateDataCodec, CommandCodec, GreetingCodec, IdleDoneCodec, ResponseCodec, }; use thiserror::Error; use crate::{ handle::{Handle, HandleGenerator, HandleGeneratorGenerator, RawHandle}, receive::{ReceiveError, ReceiveEvent, ReceiveState}, server_send::{ServerSendEvent, ServerSendState}, types::CommandAuthenticate, Interrupt, State, }; static HANDLE_GENERATOR_GENERATOR: HandleGeneratorGenerator = HandleGeneratorGenerator::new(); #[derive(Clone, Debug, PartialEq)] #[non_exhaustive] pub struct Options { pub crlf_relaxed: bool, /// Max literal size accepted by server. /// /// Bigger literals are rejected by the server. /// /// Currently, we don't distinguish between general literals and the literal used in the /// APPEND command. However, this might change in the future. Note that /// `max_literal_size < max_command_size` must hold. pub max_literal_size: u32, /// Max command size that can be parsed by the server. /// /// Bigger commands raise an error. pub max_command_size: u32, literal_accept_ccr: CommandContinuationRequest<'static>, literal_reject_ccr: CommandContinuationRequest<'static>, } impl Default for Options { fn default() -> Self { Self { // Lean towards conformity crlf_relaxed: false, // 25 MiB is a common maximum email size (Oct. 2023). max_literal_size: 25 * 1024 * 1024, // Must be bigger than `max_literal_size`. // 64 KiB is used by Dovecot. max_command_size: (25 * 1024 * 1024) + (64 * 1024), // Short unmeaning text literal_accept_ccr: CommandContinuationRequest::basic(None, Text::unvalidated("...")) .unwrap(), // Short unmeaning text literal_reject_ccr: CommandContinuationRequest::basic(None, Text::unvalidated("...")) .unwrap(), } } } impl Options { pub fn literal_accept_text(&self) -> &Text { match self.literal_accept_ccr { CommandContinuationRequest::Basic(ref basic) => basic.text(), CommandContinuationRequest::Base64(_) => unreachable!(), } } pub fn set_literal_accept_text(&mut self, text: String) -> Result<(), String> { // imap-codec doesn't return `text` on error. Thus, we first check with &str as a // workaround ... if CommandContinuationRequestBasic::new(None, text.as_str()).is_ok() { // ... and can use `unwrap` later. self.literal_accept_ccr = CommandContinuationRequest::basic(None, text).unwrap(); Ok(()) } else { Err(text) } } pub fn literal_reject_text(&self) -> &Text { match self.literal_reject_ccr { CommandContinuationRequest::Basic(ref basic) => basic.text(), CommandContinuationRequest::Base64(_) => unreachable!(), } } pub fn set_literal_reject_text(&mut self, text: String) -> Result<(), String> { // imap-codec doesn't return `text` on error. Thus, we first check with &str as a // workaround ... if CommandContinuationRequestBasic::new(None, text.as_str()).is_ok() { // ... and can use `unwrap` later. self.literal_reject_ccr = CommandContinuationRequest::basic(None, text).unwrap(); Ok(()) } else { Err(text) } } } pub struct Server { options: Options, handle_generator: HandleGenerator, send_state: ServerSendState, receive_state: ReceiveState, next_expected_message: NextExpectedMessage, } impl Server { pub fn new(options: Options, greeting: Greeting<'static>) -> Self { let mut send_state = ServerSendState::new(GreetingCodec::default(), ResponseCodec::default()); send_state.enqueue_greeting(greeting); let receive_state = ReceiveState::new(options.crlf_relaxed, Some(options.max_command_size)); let next_expected_message = NextExpectedMessage::Command(CommandCodec::default()); Self { options, handle_generator: HANDLE_GENERATOR_GENERATOR.generate(), send_state, receive_state, next_expected_message, } } /// Enqueues the [`Data`] response for being sent to the client. /// /// The response is not sent immediately but during one of the next calls of /// [`Server::next`]. All responses are sent in the same order they have been /// enqueued. pub fn enqueue_data(&mut self, data: Data<'static>) -> ResponseHandle { let handle = self.handle_generator.generate(); self.send_state .enqueue_response(Some(handle), Response::Data(data)); handle } /// Enqueues the [`Status`] response for being sent to the client. /// /// The response is not sent immediately but during one of the next calls of /// [`Server::next`]. All responses are sent in the same order they have been /// enqueued. pub fn enqueue_status(&mut self, status: Status<'static>) -> ResponseHandle { let handle = self.handle_generator.generate(); self.send_state .enqueue_response(Some(handle), Response::Status(status)); handle } /// Enqueues the [`CommandContinuationRequest`] response for being sent to the client. /// /// The response is not sent immediately but during one of the next calls of /// [`Server::next`]. All responses are sent in the same order they have been /// enqueued. pub fn enqueue_continuation_request( &mut self, continuation_request: CommandContinuationRequest<'static>, ) -> ResponseHandle { let handle = self.handle_generator.generate(); self.send_state.enqueue_response( Some(handle), Response::CommandContinuationRequest(continuation_request), ); handle } fn progress_send(&mut self) -> Result, Interrupt> { match self.send_state.next() { Ok(Some(ServerSendEvent::Greeting { greeting })) => { // The initial greeting was sucessfully sent, inform the caller Ok(Some(Event::GreetingSent { greeting })) } Ok(Some(ServerSendEvent::Response { handle: Some(handle), response, })) => { // A response was sucessfully sent, inform the caller Ok(Some(Event::ResponseSent { handle, response })) } Ok(Some(ServerSendEvent::Response { handle: None, .. })) => { // An internally created response was sent, don't inform the caller Ok(None) } Ok(_) => { // No progress yet Ok(None) } Err(Interrupt::Io(io)) => Err(Interrupt::Io(io)), Err(Interrupt::Error(_)) => unreachable!(), } } fn progress_receive(&mut self) -> Result, Interrupt> { match &self.next_expected_message { NextExpectedMessage::Command(codec) => match self .receive_state .next::(codec) { Ok(ReceiveEvent::DecodingSuccess(command)) => match command.body { CommandBody::Authenticate { mechanism, initial_response, } => { self.next_expected_message = NextExpectedMessage::AuthenticateData(AuthenticateDataCodec::default()); Ok(Some(Event::CommandAuthenticateReceived { command_authenticate: CommandAuthenticate { tag: command.tag, mechanism, initial_response, }, })) } CommandBody::Idle => { self.next_expected_message = NextExpectedMessage::IdleAccept; Ok(Some(Event::IdleCommandReceived { tag: command.tag })) } body => Ok(Some(Event::CommandReceived { command: Command { tag: command.tag, body, }, })), }, Ok(ReceiveEvent::LiteralAnnouncement { mode, length }) => { if length > self.options.max_literal_size { match mode { LiteralMode::Sync => { // Inform the client that the literal was rejected. if let Some(tag) = self.receive_state.message_tag() { // Unwrap: This should never fail because the text is // not Base64. let status = Status::bad( Some(tag), None, self.options.literal_reject_text().to_static(), ) .unwrap(); self.send_state .enqueue_response(None, Response::Status(status)); let discarded_bytes = self.receive_state.discard_message(); Err(Interrupt::Error(Error::LiteralTooLong { discarded_bytes })) } else { // We need a tag for rejecting the literal, but the // message seems to be malformed because it contains no // tag. Discarding the message immediately would be // dangerous because the literal might contain bytes that // look like IMAP commands. Doing nothing might lead // to a deadlock because the client is waiting for a // response. We prefer the latter because it is more safe. // If we receive the complete message for whatever reason, // we need to make sure that it will be discarded. // Note that `max_command_size` will still prevent // allocation of unlimited memory. self.receive_state.poison_message(); Ok(None) } } LiteralMode::NonSync => { // We can't (reliably) make the client stop sending data. // Discarding the message immediately would be dangerous // because the literal might contain bytes that look like // IMAP commands. So instead we continue receiving the // message but discard it afterwards. Note that // `max_command_size` will still prevent allocation of // unlimited memory. self.receive_state.poison_message(); Ok(None) } } } else { match mode { LiteralMode::Sync => { // Inform the client that the literal was accepted. // Unwrap: This should never fail because the text is not Base64. let cont = CommandContinuationRequest::basic( None, self.options.literal_accept_text().to_static(), ) .unwrap(); self.send_state.enqueue_response( None, Response::CommandContinuationRequest(cont), ); } LiteralMode::NonSync => { // We don't need to inform the client because non-sync literals // are automatically accepted. } } Ok(None) } } Err(interrupt) => Err(handle_receive_interrupt(interrupt)), }, NextExpectedMessage::AuthenticateData(codec) => { match self.receive_state.next::(codec) { Ok(ReceiveEvent::DecodingSuccess(authenticate_data)) => { Ok(Some(Event::AuthenticateDataReceived { authenticate_data })) } Ok(ReceiveEvent::LiteralAnnouncement { .. }) => { // Unexpected literal, let's continue and see what happens Ok(None) } Err(interrupt) => Err(handle_receive_interrupt(interrupt)), } } NextExpectedMessage::IdleAccept => { // We don't expect any message until the server user calls // `idle_accept` or `idle_reject`. // TODO: It's strange to return NeedMoreInput here, but it works for now. Err(Interrupt::Io(crate::Io::NeedMoreInput)) } NextExpectedMessage::IdleDone(codec) => { match self.receive_state.next::(codec) { Ok(ReceiveEvent::DecodingSuccess(IdleDone)) => { self.next_expected_message = NextExpectedMessage::Command(CommandCodec::default()); Ok(Some(Event::IdleDoneReceived)) } Ok(ReceiveEvent::LiteralAnnouncement { .. }) => { // Unexpected literal, let's continue and see what happens Ok(None) } Err(interrupt) => Err(handle_receive_interrupt(interrupt)), } } } } pub fn authenticate_continue( &mut self, continuation_request: CommandContinuationRequest<'static>, ) -> Result> { if let NextExpectedMessage::AuthenticateData { .. } = self.next_expected_message { let handle = self.enqueue_continuation_request(continuation_request); Ok(handle) } else { Err(continuation_request) } } pub fn authenticate_finish( &mut self, status: Status<'static>, ) -> Result> { if let NextExpectedMessage::AuthenticateData(_) = &mut self.next_expected_message { let handle = self.enqueue_status(status); self.next_expected_message = NextExpectedMessage::Command(CommandCodec::default()); Ok(handle) } else { Err(status) } } pub fn idle_accept( &mut self, continuation_request: CommandContinuationRequest<'static>, ) -> Result> { if let NextExpectedMessage::IdleAccept = &mut self.next_expected_message { let handle = self.enqueue_continuation_request(continuation_request); self.next_expected_message = NextExpectedMessage::IdleDone(IdleDoneCodec::default()); Ok(handle) } else { Err(continuation_request) } } pub fn idle_reject( &mut self, status: Status<'static>, ) -> Result> { if let NextExpectedMessage::IdleAccept = &mut self.next_expected_message { let handle = self.enqueue_status(status); self.next_expected_message = NextExpectedMessage::Command(CommandCodec::default()); Ok(handle) } else { Err(status) } } } impl Debug for Server { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { f.debug_struct("Server") .field("options", &self.options) .field("handle_generator", &self.handle_generator) .finish_non_exhaustive() } } impl State for Server { type Event = Event; type Error = Error; fn enqueue_input(&mut self, bytes: &[u8]) { self.receive_state.enqueue_input(bytes); } fn next(&mut self) -> Result> { loop { if let Some(event) = self.progress_send()? { return Ok(event); } if let Some(event) = self.progress_receive()? { return Ok(event); } } } } fn handle_receive_interrupt(receive_interrupt: Interrupt) -> Interrupt { match receive_interrupt { Interrupt::Io(io) => Interrupt::Io(io), Interrupt::Error(ReceiveError::DecodingFailure { discarded_bytes }) => { Interrupt::Error(Error::MalformedMessage { discarded_bytes }) } Interrupt::Error(ReceiveError::ExpectedCrlfGotLf { discarded_bytes }) => { Interrupt::Error(Error::ExpectedCrlfGotLf { discarded_bytes }) } Interrupt::Error(ReceiveError::MessageIsPoisoned { discarded_bytes }) => { Interrupt::Error(Error::MalformedMessage { discarded_bytes }) } Interrupt::Error(ReceiveError::MessageTooLong { discarded_bytes }) => { Interrupt::Error(Error::CommandTooLong { discarded_bytes }) } } } /// Handle for enqueued [`Response`]. /// /// This handle can be used to track the sending progress. After a [`Response`] was enqueued via /// [`Server::enqueue_data`] or [`Server::enqueue_status`] it is in the process of being /// sent until [`Server::next`] returns a [`Event::ResponseSent`] with the /// corresponding handle. #[derive(Clone, Copy, Eq, PartialEq, Hash)] pub struct ResponseHandle(RawHandle); // Implement a short debug representation that hides the underlying raw handle impl Debug for ResponseHandle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("ResponseHandle") .field(&self.0.generator_id()) .field(&self.0.handle_id()) .finish() } } impl Handle for ResponseHandle { fn from_raw(raw_handle: RawHandle) -> Self { Self(raw_handle) } } #[derive(Debug)] pub enum Event { /// Initial [`Greeting] was sent successfully. GreetingSent { greeting: Greeting<'static>, }, /// Enqueued [`Response`] was sent successfully. ResponseSent { /// Handle of the formerly enqueued [`Response`]. handle: ResponseHandle, /// Formerly enqueued [`Response`] that was now sent. response: Response<'static>, }, /// Command received. CommandReceived { command: Command<'static>, }, /// Command AUTHENTICATE received. /// /// Note: The server MUST call [`Server::authenticate_continue`] (if it needs more data for /// authentication) or [`Server::authenticate_finish`] (if there already is enough data for /// authentication) next. "Enough data" is determined by the used SASL mechanism, if there was /// an initial response (SASL-IR), etc. CommandAuthenticateReceived { command_authenticate: CommandAuthenticate, }, /// Continuation to AUTHENTICATE received. /// /// Note: The server MUST call [`Server::authenticate_continue`] (if it needs more data for /// authentication) or [`Server::authenticate_finish`] (if there already is enough data for /// authentication) next. "Enough data" is determined by the used SASL mechanism, if there was /// an initial response (SASL-IR), etc. /// /// Note, too: The client may abort the authentication by using [`AuthenticateData::Cancel`]. /// Make sure to honor the client's request to not end up in an infinite loop. It's up to the /// server to end the authentication flow. AuthenticateDataReceived { authenticate_data: AuthenticateData<'static>, }, IdleCommandReceived { tag: Tag<'static>, }, IdleDoneReceived, } #[derive(Debug, Error)] pub enum Error { #[error("Expected `\\r\\n`, got `\\n`")] ExpectedCrlfGotLf { discarded_bytes: Secret> }, #[error("Received malformed message")] MalformedMessage { discarded_bytes: Secret> }, #[error("Literal was rejected because it was too long")] LiteralTooLong { discarded_bytes: Secret> }, #[error("Command is too long")] CommandTooLong { discarded_bytes: Secret> }, } #[derive(Clone, Debug)] enum NextExpectedMessage { Command(CommandCodec), AuthenticateData(AuthenticateDataCodec), IdleAccept, IdleDone(IdleDoneCodec), } imap-next-0.3.1/src/server_send.rs000066400000000000000000000111531472413535300171010ustar00rootroot00000000000000use std::{collections::VecDeque, convert::Infallible}; use imap_codec::{ encode::{Encoded, Encoder, Fragment}, imap_types::response::{Greeting, Response}, GreetingCodec, ResponseCodec, }; use crate::{server::ResponseHandle, Interrupt, Io}; pub struct ServerSendState { greeting_codec: GreetingCodec, response_codec: ResponseCodec, // FIFO queue for messages that should be sent next. queued_messages: VecDeque, // The message that is currently being sent. current_message: Option, } impl ServerSendState { pub fn new(greeting_codec: GreetingCodec, response_codec: ResponseCodec) -> Self { Self { greeting_codec, response_codec, queued_messages: VecDeque::new(), current_message: None, } } pub fn enqueue_greeting(&mut self, greeting: Greeting<'static>) { self.queued_messages .push_back(QueuedMessage::Greeting { greeting }); } pub fn enqueue_response( &mut self, handle: Option, response: Response<'static>, ) { self.queued_messages .push_back(QueuedMessage::Response { handle, response }); } pub fn next(&mut self) -> Result, Interrupt> { match self.current_message.take() { Some(current_message) => { // Continue the message that was interrupted. let event = match current_message { CurrentMessage::Greeting { greeting } => ServerSendEvent::Greeting { greeting }, CurrentMessage::Response { handle, response } => { ServerSendEvent::Response { handle, response } } }; Ok(Some(event)) } None => { let Some(queued_message) = self.queued_messages.pop_front() else { // There is currently no message that needs to be sent return Ok(None); }; // Creates a buffer for writing the current message let mut write_buffer = Vec::new(); // Push the bytes of the message to the buffer let current_message = queued_message.push_to_buffer( &mut write_buffer, &self.greeting_codec, &self.response_codec, ); self.current_message = Some(current_message); // Interrupt the state for sending all bytes of current message Err(Interrupt::Io(Io::Output(write_buffer))) } } } } /// Message that is queued but not sent yet. enum QueuedMessage { Greeting { greeting: Greeting<'static>, }, Response { handle: Option, response: Response<'static>, }, } impl QueuedMessage { fn push_to_buffer( self, write_buffer: &mut Vec, greeting_codec: &GreetingCodec, response_codec: &ResponseCodec, ) -> CurrentMessage { match self { QueuedMessage::Greeting { greeting } => { let encoded = greeting_codec.encode(&greeting); push_encoded_to_buffer(write_buffer, encoded); CurrentMessage::Greeting { greeting } } QueuedMessage::Response { handle, response } => { let encoded = response_codec.encode(&response); push_encoded_to_buffer(write_buffer, encoded); CurrentMessage::Response { handle, response } } } } } fn push_encoded_to_buffer(write_buffer: &mut Vec, encoded: Encoded) { for fragment in encoded { let data = match fragment { Fragment::Line { data } => data, // Note: The server doesn't need to wait before sending a literal. // Thus, non-sync literals doesn't make sense here. // This is currently an issue in imap-codec, // see https://github.com/duesee/imap-codec/issues/332 Fragment::Literal { data, .. } => data, }; write_buffer.extend(data); } } /// Message that is currently being sent. enum CurrentMessage { Greeting { greeting: Greeting<'static>, }, Response { handle: Option, response: Response<'static>, }, } /// Message was sent. pub enum ServerSendEvent { Greeting { greeting: Greeting<'static>, }, Response { handle: Option, response: Response<'static>, }, } imap-next-0.3.1/src/stream.rs000066400000000000000000000253361472413535300160650ustar00rootroot00000000000000use std::{ convert::Infallible, io::{ErrorKind, Read, Write}, }; use bytes::{Buf, BufMut, BytesMut}; #[cfg(debug_assertions)] use imap_codec::imap_types::utils::escape_byte_string; use thiserror::Error; use tokio::{ io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, net::TcpStream, select, }; use tokio_rustls::{rustls, TlsStream}; #[cfg(debug_assertions)] use tracing::trace; use crate::{Interrupt, Io, State}; pub struct Stream { stream: TcpStream, tls: Option, read_buffer: BytesMut, write_buffer: BytesMut, } impl Stream { pub fn insecure(stream: TcpStream) -> Self { Self { stream, tls: None, read_buffer: BytesMut::default(), write_buffer: BytesMut::default(), } } pub fn tls(stream: TlsStream) -> Self { // We want to use `TcpStream::split` for handling reading and writing separately, // but `TlsStream` does not expose this functionality. Therefore, we destruct `TlsStream` // into `TcpStream` and `rustls::Connection` and handling them ourselves. // // Some notes: // // - There is also `tokio::io::split` which works for all kind of streams. But this // involves too much scary magic because its use-case is reading and writing from // different threads. We prefer to use the more low-level `TcpStream::split`. // // - We could get rid of `TlsStream` and construct `rustls::Connection` directly. // But `TlsStream` is still useful because it gives us the guarantee that the handshake // was already handled properly. // // - In the long run it would be nice if `TlsStream::split` would exist and we would use // it because `TlsStream` is better at handling the edge cases of `rustls`. let (stream, tls) = match stream { TlsStream::Client(stream) => { let (stream, tls) = stream.into_inner(); (stream, rustls::Connection::Client(tls)) } TlsStream::Server(stream) => { let (stream, tls) = stream.into_inner(); (stream, rustls::Connection::Server(tls)) } }; Self { stream, tls: Some(tls), read_buffer: BytesMut::default(), write_buffer: BytesMut::default(), } } pub async fn flush(&mut self) -> Result<(), Error> { // Flush TLS if let Some(tls) = &mut self.tls { tls.writer().flush()?; encrypt(tls, &mut self.write_buffer, Vec::new())?; } // Flush TCP write(&mut self.stream, &mut self.write_buffer).await?; self.stream.flush().await?; Ok(()) } pub async fn next(&mut self, mut state: F) -> Result> { let event = loop { match &mut self.tls { None => { // Provide input bytes to the client/server if !self.read_buffer.is_empty() { state.enqueue_input(&self.read_buffer); self.read_buffer.clear(); } } Some(tls) => { // Decrypt input bytes let plain_bytes = decrypt(tls, &mut self.read_buffer)?; // Provide input bytes to the client/server if !plain_bytes.is_empty() { state.enqueue_input(&plain_bytes); } } } // Progress the client/server let result = state.next(); // Return events immediately without doing IO let interrupt = match result { Err(interrupt) => interrupt, Ok(event) => break event, }; // Return errors immediately without doing IO let io = match interrupt { Interrupt::Io(io) => io, Interrupt::Error(err) => return Err(Error::State(err)), }; match &mut self.tls { None => { // Handle the output bytes from the client/server if let Io::Output(bytes) = io { self.write_buffer.extend(bytes); } } Some(tls) => { // Handle the output bytes from the client/server let plain_bytes = if let Io::Output(bytes) = io { bytes } else { Vec::new() }; // Encrypt output bytes encrypt(tls, &mut self.write_buffer, plain_bytes)?; } } // Progress the stream if self.write_buffer.is_empty() { read(&mut self.stream, &mut self.read_buffer).await?; } else { // We read and write the stream simultaneously because otherwise // a deadlock between client and server might occur if both sides // would only read or only write. let (read_stream, write_stream) = self.stream.split(); select! { result = read(read_stream, &mut self.read_buffer) => result, result = write(write_stream, &mut self.write_buffer) => result, }?; }; }; Ok(event) } #[cfg(feature = "expose_stream")] /// Return the underlying stream for debug purposes (or experiments). /// /// Note: Writing to or reading from the stream may introduce /// conflicts with `imap-next`. pub fn stream_mut(&mut self) -> &mut TcpStream { &mut self.stream } } /// Take the [`TcpStream`] out of a [`Stream`]. /// /// Useful when a TCP stream needs to be upgraded to a TLS one. #[cfg(feature = "expose_stream")] impl From for TcpStream { fn from(stream: Stream) -> Self { stream.stream } } /// Error during reading into or writing from a stream. #[derive(Debug, Error)] pub enum Error { /// Operation failed because stream is closed. /// /// We detect this by checking if the read or written byte count is 0. Whether the stream is /// closed indefinitely or temporarily depends on the actual stream implementation. #[error("Stream was closed")] Closed, /// An I/O error occurred in the underlying stream. #[error(transparent)] Io(#[from] tokio::io::Error), /// An error occurred in the underlying TLS connection. #[error(transparent)] Tls(#[from] rustls::Error), /// An error occurred while progressing the state. #[error(transparent)] State(E), } async fn read( mut stream: S, read_buffer: &mut BytesMut, ) -> Result<(), ReadWriteError> { #[cfg(debug_assertions)] let old_len = read_buffer.len(); let byte_count = stream.read_buf(read_buffer).await?; #[cfg(debug_assertions)] trace!( data = escape_byte_string(&read_buffer[old_len..]), "io/read/raw" ); if byte_count == 0 { // The result is 0 if the stream reached "end of file" or the read buffer was // already full before calling `read_buf`. Because we use an unlimited buffer we // know that the first case occurred. return Err(ReadWriteError::Closed); } Ok(()) } async fn write( mut stream: S, write_buffer: &mut BytesMut, ) -> Result<(), ReadWriteError> { while !write_buffer.is_empty() { let byte_count = stream.write(write_buffer).await?; #[cfg(debug_assertions)] trace!( data = escape_byte_string(&write_buffer[..byte_count]), "io/write/raw" ); write_buffer.advance(byte_count); if byte_count == 0 { // The result is 0 if the stream doesn't accept bytes anymore or the write buffer // was already empty before calling `write_buf`. Because we checked the buffer // we know that the first case occurred. return Err(ReadWriteError::Closed); } } Ok(()) } #[derive(Debug, Error)] enum ReadWriteError { #[error("Stream was closed")] Closed, #[error(transparent)] Io(#[from] tokio::io::Error), } impl From for Error { fn from(value: ReadWriteError) -> Self { match value { ReadWriteError::Closed => Error::Closed, ReadWriteError::Io(err) => Error::Io(err), } } } fn decrypt( tls: &mut rustls::Connection, read_buffer: &mut BytesMut, ) -> Result, DecryptEncryptError> { let mut plain_bytes = Vec::new(); while tls.wants_read() && !read_buffer.is_empty() { let mut encrypted_bytes = read_buffer.reader(); tls.read_tls(&mut encrypted_bytes)?; tls.process_new_packets()?; } loop { let mut plain_bytes_chunk = [0; 128]; // We need to handle different cases according to: // https://docs.rs/rustls/latest/rustls/struct.Reader.html#method.read match tls.reader().read(&mut plain_bytes_chunk) { // There are no more bytes to read Err(err) if err.kind() == ErrorKind::WouldBlock => break, // The TLS session was closed uncleanly Err(err) if err.kind() == ErrorKind::UnexpectedEof => { return Err(DecryptEncryptError::Closed) } // We got an unexpected error Err(err) => return Err(DecryptEncryptError::Io(err)), // The TLS session was closed cleanly Ok(0) => return Err(DecryptEncryptError::Closed), // We read some plaintext bytes Ok(n) => plain_bytes.extend(&plain_bytes_chunk[0..n]), }; } Ok(plain_bytes) } fn encrypt( tls: &mut rustls::Connection, write_buffer: &mut BytesMut, plain_bytes: Vec, ) -> Result<(), DecryptEncryptError> { if !plain_bytes.is_empty() { tls.writer().write_all(&plain_bytes)?; } while tls.wants_write() { let mut encrypted_bytes = write_buffer.writer(); tls.write_tls(&mut encrypted_bytes)?; } Ok(()) } #[derive(Debug, Error)] enum DecryptEncryptError { #[error("Session was closed")] Closed, #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] Tls(#[from] rustls::Error), } impl From for Error { fn from(value: DecryptEncryptError) -> Self { match value { DecryptEncryptError::Closed => Error::Closed, DecryptEncryptError::Io(err) => Error::Io(err), DecryptEncryptError::Tls(err) => Error::Tls(err), } } } imap-next-0.3.1/src/tests.rs000066400000000000000000000052401472413535300157240ustar00rootroot00000000000000use imap_codec::imap_types::{ auth::AuthMechanism, command::{Command, CommandBody}, core::Tag, response::{Greeting, Status}, }; use tokio::net::{TcpListener, TcpStream}; use crate::{ client::{self, Client}, server::{self, Server}, stream::Stream, }; #[tokio::test] async fn self_test() { let greeting = Greeting::ok(None, "Hello, World!").unwrap(); // Port 0 means "pick any available port" let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let port = listener.local_addr().unwrap().port(); let server = { let greeting = greeting.clone(); async move { let (stream, _) = listener.accept().await.unwrap(); let mut stream = Stream::insecure(stream); let mut server = Server::new(server::Options::default(), greeting.clone()); loop { match stream.next(&mut server).await.unwrap() { server::Event::CommandReceived { command } => { let no = Status::no(Some(command.tag), None, "...").unwrap(); server.enqueue_status(no); } server::Event::CommandAuthenticateReceived { command_authenticate, } => { let no = Status::no(Some(command_authenticate.tag), None, "...").unwrap(); server.enqueue_status(no); } _ => {} } } } }; #[allow(clippy::let_underscore_future)] let _ = tokio::task::spawn(server); let stream = TcpStream::connect(("127.0.0.1", port)).await.unwrap(); let mut stream = Stream::insecure(stream); let mut client = Client::new(client::Options::default()); client.enqueue_command(Command::new(Tag::unvalidated("A1"), CommandBody::Capability).unwrap()); loop { match stream.next(&mut client).await.unwrap() { client::Event::GreetingReceived { greeting: received_greeting, } => { assert_eq!(greeting, received_greeting) } client::Event::StatusReceived { .. } => { client.enqueue_command( Command::new( Tag::unvalidated("A2"), CommandBody::Authenticate { mechanism: AuthMechanism::Plain, initial_response: None, }, ) .unwrap(), ); } client::Event::AuthenticateStatusReceived { .. } => break, _ => {} } } } imap-next-0.3.1/src/types.rs000066400000000000000000000014351472413535300157300ustar00rootroot00000000000000//! Types that extend `imap-types`. // TODO: Do we really need this? use std::borrow::Cow; use imap_codec::imap_types::{ auth::AuthMechanism, command::{Command, CommandBody}, core::Tag, secret::Secret, }; #[derive(Debug)] pub struct CommandAuthenticate { pub tag: Tag<'static>, pub mechanism: AuthMechanism<'static>, pub initial_response: Option>>, } impl From for Command<'static> { fn from(command_authenticate: CommandAuthenticate) -> Self { Self { tag: command_authenticate.tag, body: CommandBody::Authenticate { mechanism: command_authenticate.mechanism, initial_response: command_authenticate.initial_response, }, } } }