├── .github ├── DOCS.md ├── codecov.yml ├── dependabot.yml └── workflows │ ├── check.yml │ ├── scheduled.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── README.md ├── basic.rs ├── gmail_oauth2.rs ├── idle.rs ├── plaintext.rs ├── rustls.rs ├── starttls.rs └── timeout.rs ├── src ├── authenticator.rs ├── client.rs ├── client_builder.rs ├── conn.rs ├── error.rs ├── extensions │ ├── idle.rs │ ├── list_status.rs │ ├── metadata.rs │ ├── mod.rs │ └── sort.rs ├── lib.rs ├── mock_stream.rs ├── parse.rs ├── testing.rs ├── types │ ├── acls.rs │ ├── appended.rs │ ├── capabilities.rs │ ├── deleted.rs │ ├── fetch.rs │ ├── flag.rs │ ├── mailbox.rs │ ├── mod.rs │ ├── name.rs │ ├── quota.rs │ └── unsolicited_response.rs └── utils.rs └── tests ├── builder_integration.rs └── imap_integration.rs /.github/DOCS.md: -------------------------------------------------------------------------------- 1 | # Github config and workflows 2 | 3 | In this folder there is configuration for codecoverage, dependabot, and ci 4 | workflows that check the library more deeply than the default configurations. 5 | 6 | This folder can be or was merged using a --allow-unrelated-histories merge 7 | strategy from which provides a 8 | reasonably sensible base for writing your own ci on. By using this strategy 9 | the history of the CI repo is included in your repo, and future updates to 10 | the CI can be merged later. 11 | 12 | To perform this merge run: 13 | 14 | ```shell 15 | git remote add ci https://github.com/jonhoo/rust-ci-conf.git 16 | git fetch ci 17 | git merge --allow-unrelated-histories ci/main 18 | ``` 19 | 20 | An overview of the files in this project is available at: 21 | , which contains some 22 | rationale for decisions and runs through an example of solving minimal version 23 | and OpenSSL issues. 24 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | # ref: https://docs.codecov.com/docs/codecovyml-reference 2 | coverage: 3 | # Hold ourselves to a high bar 4 | range: 85..100 5 | round: down 6 | precision: 1 7 | status: 8 | # ref: https://docs.codecov.com/docs/commit-status 9 | project: 10 | default: 11 | # Avoid false negatives 12 | threshold: 1% 13 | 14 | # Test files aren't important for coverage 15 | ignore: 16 | - "tests" 17 | 18 | # Make comments less noisy 19 | comment: 20 | layout: "files" 21 | require_changes: true 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: cargo 8 | directory: / 9 | schedule: 10 | interval: daily 11 | ignore: 12 | - dependency-name: "*" 13 | # patch and minor updates don't matter for libraries as consumers of this library build 14 | # with their own lockfile, rather than the version specified in this library's lockfile 15 | # remove this ignore rule if your package has binaries to ensure that the binaries are 16 | # built with the exact set of dependencies and those are up to date. 17 | update-types: 18 | - "version-update:semver-patch" 19 | - "version-update:semver-minor" 20 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs whenever a PR is opened or updated, or a commit is pushed to main. It runs 2 | # several checks: 3 | # - fmt: checks that the code is formatted according to rustfmt 4 | # - clippy: checks that the code does not contain any clippy warnings 5 | # - doc: checks that the code can be documented without errors 6 | # - hack: check combinations of feature flags 7 | # - msrv: check that the msrv specified in the crate is correct 8 | permissions: 9 | contents: read 10 | # This configuration allows maintainers of this repo to create a branch and pull request based on 11 | # the new branch. Restricting the push trigger to the main branch ensures that the PR only gets 12 | # built once. 13 | on: 14 | push: 15 | branches: [main] 16 | pull_request: 17 | # If new code is pushed to a PR branch, then cancel in progress workflows for that PR. Ensures that 18 | # we don't waste CI time, and returns results quicker https://github.com/jonhoo/rust-ci-conf/pull/5 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 21 | cancel-in-progress: true 22 | name: check 23 | jobs: 24 | fmt: 25 | runs-on: ubuntu-latest 26 | name: stable / fmt 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | submodules: true 31 | - name: Install stable 32 | uses: dtolnay/rust-toolchain@stable 33 | with: 34 | components: rustfmt 35 | - name: cargo fmt --check 36 | run: cargo fmt --check 37 | clippy: 38 | runs-on: ubuntu-latest 39 | name: ${{ matrix.toolchain }} / clippy 40 | permissions: 41 | contents: read 42 | checks: write 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | # Get early warning of new lints which are regularly introduced in beta channels. 47 | toolchain: [stable, beta] 48 | steps: 49 | - uses: actions/checkout@v4 50 | with: 51 | submodules: true 52 | - name: Install ${{ matrix.toolchain }} 53 | uses: dtolnay/rust-toolchain@master 54 | with: 55 | toolchain: ${{ matrix.toolchain }} 56 | components: clippy 57 | - name: cargo clippy 58 | uses: giraffate/clippy-action@v1 59 | with: 60 | reporter: 'github-pr-check' 61 | github_token: ${{ secrets.GITHUB_TOKEN }} 62 | semver: 63 | runs-on: ubuntu-latest 64 | name: semver 65 | steps: 66 | - uses: actions/checkout@v4 67 | with: 68 | submodules: true 69 | - name: Install stable 70 | uses: dtolnay/rust-toolchain@stable 71 | with: 72 | components: rustfmt 73 | - name: cargo-semver-checks 74 | uses: obi1kenobi/cargo-semver-checks-action@v2 75 | doc: 76 | # run docs generation on nightly rather than stable. This enables features like 77 | # https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html which allows an 78 | # API be documented as only available in some specific platforms. 79 | runs-on: ubuntu-latest 80 | name: nightly / doc 81 | steps: 82 | - uses: actions/checkout@v4 83 | with: 84 | submodules: true 85 | - name: Install nightly 86 | uses: dtolnay/rust-toolchain@nightly 87 | - name: Install cargo-docs-rs 88 | uses: dtolnay/install@cargo-docs-rs 89 | - name: cargo docs-rs 90 | run: cargo docs-rs 91 | hack: 92 | # cargo-hack checks combinations of feature flags to ensure that features are all additive 93 | # which is required for feature unification 94 | runs-on: ubuntu-latest 95 | name: ubuntu / stable / features 96 | steps: 97 | - uses: actions/checkout@v4 98 | with: 99 | submodules: true 100 | - name: Install stable 101 | uses: dtolnay/rust-toolchain@stable 102 | - name: cargo install cargo-hack 103 | uses: taiki-e/install-action@cargo-hack 104 | # intentionally no target specifier; see https://github.com/jonhoo/rust-ci-conf/pull/4 105 | # --feature-powerset runs for every combination of features 106 | - name: cargo hack 107 | run: cargo hack --feature-powerset check 108 | msrv: 109 | # check that we can build using the minimal rust version that is specified by this crate 110 | runs-on: ubuntu-latest 111 | # we use a matrix here just because env can't be used in job names 112 | # https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability 113 | strategy: 114 | matrix: 115 | msrv: ["1.65.0"] 116 | name: ubuntu / ${{ matrix.msrv }} 117 | steps: 118 | - uses: actions/checkout@v4 119 | with: 120 | submodules: true 121 | - name: Install ${{ matrix.msrv }} 122 | uses: dtolnay/rust-toolchain@master 123 | with: 124 | toolchain: ${{ matrix.msrv }} 125 | - name: cargo +${{ matrix.msrv }} check 126 | run: cargo check 127 | -------------------------------------------------------------------------------- /.github/workflows/scheduled.yml: -------------------------------------------------------------------------------- 1 | # Run scheduled (rolling) jobs on a nightly basis, as your crate may break independently of any 2 | # given PR. E.g., updates to rust nightly and updates to this crates dependencies. See check.yml for 3 | # information about how the concurrency cancellation and workflow triggering works 4 | permissions: 5 | contents: read 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | schedule: 11 | - cron: '7 7 * * *' 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 14 | cancel-in-progress: true 15 | name: rolling 16 | jobs: 17 | # https://twitter.com/mycoliza/status/1571295690063753218 18 | nightly: 19 | runs-on: ubuntu-latest 20 | name: ubuntu / nightly 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | submodules: true 25 | - name: Install nightly 26 | uses: dtolnay/rust-toolchain@nightly 27 | - name: cargo generate-lockfile 28 | if: hashFiles('Cargo.lock') == '' 29 | run: cargo generate-lockfile 30 | - name: cargo test --locked 31 | run: cargo test --locked --features test-full-imap --all-targets 32 | services: 33 | cyrus_imapd: 34 | image: outoforder/cyrus-imapd-tester:latest 35 | ports: 36 | - 3025:25 37 | - 3143:143 38 | - 3465:465 39 | - 3993:993 40 | # https://twitter.com/alcuadrado/status/1571291687837732873 41 | update: 42 | # This action checks that updating the dependencies of this crate to the latest available that 43 | # satisfy the versions in Cargo.toml does not break this crate. This is important as consumers 44 | # of this crate will generally use the latest available crates. This is subject to the standard 45 | # Cargo semver rules (i.e cargo does not update to a new major version unless explicitly told 46 | # to). 47 | runs-on: ubuntu-latest 48 | name: ubuntu / beta / updated 49 | # There's no point running this if no Cargo.lock was checked in in the first place, since we'd 50 | # just redo what happened in the regular test job. Unfortunately, hashFiles only works in if on 51 | # steps, so we repeat it. 52 | steps: 53 | - uses: actions/checkout@v4 54 | with: 55 | submodules: true 56 | - name: Install beta 57 | if: hashFiles('Cargo.lock') != '' 58 | uses: dtolnay/rust-toolchain@beta 59 | - name: cargo update 60 | if: hashFiles('Cargo.lock') != '' 61 | run: cargo update 62 | - name: cargo test 63 | if: hashFiles('Cargo.lock') != '' 64 | run: cargo test --locked --features test-full-imap --all-targets 65 | env: 66 | RUSTFLAGS: -D deprecated 67 | services: 68 | cyrus_imapd: 69 | image: outoforder/cyrus-imapd-tester:latest 70 | ports: 71 | - 3025:25 72 | - 3143:143 73 | - 3465:465 74 | - 3993:993 75 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This is the main CI workflow that runs the test suite on all pushes to main and all pull requests. 2 | # It runs the following jobs: 3 | # - required: runs the test suite on ubuntu with stable and beta rust toolchains 4 | # - minimal: runs the test suite with the minimal versions of the dependencies that satisfy the 5 | # requirements of this crate, and its dependencies 6 | # - os-check: runs the test suite on mac and windows 7 | # - coverage: runs the test suite and collects coverage information 8 | # See check.yml for information about how the concurrency cancellation and workflow triggering works 9 | permissions: 10 | contents: read 11 | on: 12 | push: 13 | branches: [main] 14 | pull_request: 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 17 | cancel-in-progress: true 18 | name: test 19 | jobs: 20 | greenmail: 21 | runs-on: ubuntu-latest 22 | name: ubuntu / ${{ matrix.toolchain }} / greenmail 23 | strategy: 24 | matrix: 25 | # run on stable and beta to ensure that tests won't break on the next version of the rust 26 | # toolchain 27 | toolchain: [stable, beta] 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | submodules: true 32 | - name: Install ${{ matrix.toolchain }} 33 | uses: dtolnay/rust-toolchain@master 34 | with: 35 | toolchain: ${{ matrix.toolchain }} 36 | - name: cargo generate-lockfile 37 | # enable this ci template to run regardless of whether the lockfile is checked in or not 38 | if: hashFiles('Cargo.lock') == '' 39 | run: cargo generate-lockfile 40 | # https://twitter.com/jonhoo/status/1571290371124260865 41 | - name: cargo test --locked 42 | run: cargo test --locked --all-targets 43 | services: 44 | greenmail: 45 | image: greenmail/standalone:1.6.15 46 | ports: 47 | - 3025:3025 48 | - 3110:3110 49 | - 3143:3143 50 | - 3465:3465 51 | - 3993:3993 52 | - 3995:3995 53 | env: 54 | GREENMAIL_OPTS: "-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.auth.disabled -Dgreenmail.verbose" 55 | cyrus: 56 | runs-on: ubuntu-latest 57 | name: ubuntu / ${{ matrix.toolchain }} / cyrus 58 | strategy: 59 | matrix: 60 | toolchain: [stable, beta] 61 | steps: 62 | - uses: actions/checkout@v4 63 | with: 64 | submodules: true 65 | - name: Install ${{ matrix.toolchain }} 66 | uses: dtolnay/rust-toolchain@master 67 | with: 68 | toolchain: ${{ matrix.toolchain }} 69 | - name: cargo generate-lockfile 70 | if: hashFiles('Cargo.lock') == '' 71 | run: cargo generate-lockfile 72 | # https://twitter.com/jonhoo/status/1571290371124260865 73 | - name: cargo test --locked 74 | run: cargo test --locked --features test-full-imap --all-targets 75 | # https://github.com/rust-lang/cargo/issues/6669 76 | - name: cargo test --doc 77 | run: cargo test --locked --all-features --doc 78 | services: 79 | cyrus_imapd: 80 | image: outoforder/cyrus-imapd-tester:latest 81 | ports: 82 | - 3025:25 83 | - 3143:143 84 | - 3465:465 85 | - 3993:993 86 | minimal: 87 | # This action chooses the oldest version of the dependencies permitted by Cargo.toml to ensure 88 | # that this crate is compatible with the minimal version that this crate and its dependencies 89 | # require. This will pickup issues where this create relies on functionality that was introduced 90 | # later than the actual version specified (e.g., when we choose just a major version, but a 91 | # method was added after this version). 92 | # 93 | # This particular check can be difficult to get to succeed as often transitive dependencies may 94 | # be incorrectly specified (e.g., a dependency specifies 1.0 but really requires 1.1.5). There 95 | # is an alternative flag available -Zdirect-minimal-versions that uses the minimal versions for 96 | # direct dependencies of this crate, while selecting the maximal versions for the transitive 97 | # dependencies. Alternatively, you can add a line in your Cargo.toml to artificially increase 98 | # the minimal dependency, which you do with e.g.: 99 | # ```toml 100 | # # for minimal-versions 101 | # [target.'cfg(any())'.dependencies] 102 | # openssl = { version = "0.10.55", optional = true } # needed to allow foo to build with -Zminimal-versions 103 | # ``` 104 | # The optional = true is necessary in case that dependency isn't otherwise transitively required 105 | # by your library, and the target bit is so that this dependency edge never actually affects 106 | # Cargo build order. See also 107 | # https://github.com/jonhoo/fantoccini/blob/fde336472b712bc7ebf5b4e772023a7ba71b2262/Cargo.toml#L47-L49. 108 | # This action is run on ubuntu with the stable toolchain, as it is not expected to fail 109 | runs-on: ubuntu-latest 110 | name: ubuntu / stable / minimal-versions 111 | steps: 112 | - uses: actions/checkout@v4 113 | with: 114 | submodules: true 115 | - name: Install stable 116 | uses: dtolnay/rust-toolchain@stable 117 | - name: Install nightly for -Zminimal-versions 118 | uses: dtolnay/rust-toolchain@nightly 119 | - name: rustup default stable 120 | run: rustup default stable 121 | - name: cargo update -Zminimal-versions 122 | run: cargo +nightly update -Zminimal-versions 123 | - name: cargo test 124 | run: cargo test --locked --features test-full-imap --all-targets 125 | services: 126 | cyrus_imapd: 127 | image: outoforder/cyrus-imapd-tester:latest 128 | ports: 129 | - 3025:25 130 | - 3143:143 131 | - 3465:465 132 | - 3993:993 133 | os-check: 134 | # run cargo test on mac and windows 135 | runs-on: ${{ matrix.os }} 136 | name: ${{ matrix.os }} / stable 137 | strategy: 138 | fail-fast: false 139 | matrix: 140 | os: [macos-latest, windows-latest] 141 | steps: 142 | - run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append 143 | if: runner.os == 'Windows' 144 | - run: vcpkg install openssl:x64-windows-static-md 145 | if: runner.os == 'Windows' 146 | - uses: actions/checkout@v4 147 | with: 148 | submodules: true 149 | - name: Install stable 150 | uses: dtolnay/rust-toolchain@stable 151 | - name: cargo generate-lockfile 152 | if: hashFiles('Cargo.lock') == '' 153 | run: cargo generate-lockfile 154 | - name: cargo install cargo-hack 155 | uses: taiki-e/install-action@cargo-hack 156 | - name: cargo check 157 | run: cargo hack --feature-powerset check --locked --all-targets 158 | coverage: 159 | # use llvm-cov to build and collect coverage and outputs in a format that 160 | # is compatible with codecov.io 161 | # 162 | # note that codecov as of v4 requires that CODECOV_TOKEN from 163 | # 164 | # https://app.codecov.io/gh///settings 165 | # 166 | # is set in two places on your repo: 167 | # 168 | # - https://github.com/jonhoo/guardian/settings/secrets/actions 169 | # - https://github.com/jonhoo/guardian/settings/secrets/dependabot 170 | # 171 | # (the former is needed for codecov uploads to work with Dependabot PRs) 172 | # 173 | # PRs coming from forks of your repo will not have access to the token, but 174 | # for those, codecov allows uploading coverage reports without a token. 175 | # it's all a little weird and inconvenient. see 176 | # 177 | # https://github.com/codecov/feedback/issues/112 178 | # 179 | # for lots of more discussion 180 | runs-on: ubuntu-latest 181 | name: ubuntu / stable / coverage 182 | steps: 183 | - uses: actions/checkout@v4 184 | with: 185 | submodules: true 186 | - name: Install stable 187 | uses: dtolnay/rust-toolchain@stable 188 | with: 189 | components: llvm-tools-preview 190 | - name: cargo install cargo-llvm-cov 191 | uses: taiki-e/install-action@cargo-llvm-cov 192 | - name: cargo generate-lockfile 193 | if: hashFiles('Cargo.lock') == '' 194 | run: cargo generate-lockfile 195 | - name: cargo llvm-cov 196 | run: cargo llvm-cov --locked --features test-full-imap --lcov --output-path lcov.info 197 | - name: Record Rust version 198 | run: echo "RUST=$(rustc --version)" >> "$GITHUB_ENV" 199 | - name: Upload to codecov.io 200 | uses: codecov/codecov-action@v5 201 | with: 202 | fail_ci_if_error: true 203 | token: ${{ secrets.CODECOV_TOKEN }} 204 | env_vars: OS,RUST 205 | services: 206 | cyrus_imapd: 207 | image: outoforder/cyrus-imapd-tester:latest 208 | ports: 209 | - 3025:25 210 | - 3143:143 211 | - 3465:465 212 | - 3993:993 213 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ### Added 10 | - VANISHED support in EXPUNGE responses and unsolicited responses (#172). 11 | - SORT command extension (#178). 12 | - Support for grabbing Gmail labels (#225). 13 | - Support for the ACL extension (#227). 14 | - Support for the quote extension (#235). 15 | - Support for the list-status extension (#249). 16 | - Expose APPENDUID data (#232). 17 | 18 | ### Changed 19 | - MSRV increased to 1.57.0 for 2021 edition and base64 20 | - `expunge` and `uid_expunge` return `Result` instead of `Result>`. 21 | - IDLE capability now provides a builder interface. All `wait_*` functions are merged into `wait_while` which takes a callback with an `UnsolicitedResponse` in parameter. 22 | - All `Session.append_with_*` methods are obsoleted by `append` which returns now an `AppendCmd` builder. 23 | - Envelope `&'a [u8]` attributes are replaced by `Cow<'a, [u8]>`. 24 | - `Flag`, `Mailbox`, `UnsolicitedResponse` and `Error` are now declared as non exhaustive. 25 | - `ClientBuilder` now replaces the `imap::connect` function [#197](https://github.com/jonhoo/rust-imap/pull/197). 26 | - The `tls` feature is now `native-tls` to disambiguate it from the new `rustls-tls` feature. `native-tls` remains in the default feature set. 27 | - TLS is now enforced by default, and doesn't require generics [#245] 28 | 29 | ## [2.4.1] - 2021-01-12 30 | ### Changed 31 | 32 | - Handle empty-set inputs to `fetch` and `uid_fetch` (#177) 33 | 34 | ## [2.4.0] - 2020-12-15 35 | ### Added 36 | 37 | - `append_with_flags_and_date` (#174) 38 | 39 | ## [2.3.0] - 2020-08-23 40 | ### Added 41 | 42 | - `append_with_flags` (#171) 43 | 44 | ## [2.2.0] - 2020-07-27 45 | ### Added 46 | 47 | - Changelog 48 | - STARTTLS example (#165) 49 | - Timeout example (#168) 50 | - Export `Result` and `Error` types (#170) 51 | 52 | ### Changed 53 | 54 | - MSRV increased 55 | - Better documentation of server greeting handling (#168) 56 | 57 | [Unreleased]: https://github.com/jonhoo/rust-imap/compare/v2.4.1...HEAD 58 | [2.4.1]: https://github.com/jonhoo/rust-imap/compare/v2.4.0...v2.4.1 59 | [2.4.0]: https://github.com/jonhoo/rust-imap/compare/v2.3.0...v2.4.0 60 | [2.3.0]: https://github.com/jonhoo/rust-imap/compare/v2.2.0...v2.3.0 61 | [2.2.0]: https://github.com/jonhoo/rust-imap/compare/v2.1.2...v2.2.0 62 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "imap" 3 | version = "3.0.0-alpha.15" 4 | authors = ["Jon Gjengset ", 5 | "Matt McCoy "] 6 | documentation = "https://docs.rs/imap/" 7 | repository = "https://github.com/jonhoo/rust-imap" 8 | homepage = "https://github.com/jonhoo/rust-imap" 9 | description = "IMAP client for Rust" 10 | license = "Apache-2.0 OR MIT" 11 | edition = "2021" 12 | 13 | keywords = ["email", "imap"] 14 | categories = ["email", "network-programming"] 15 | 16 | [features] 17 | rustls-tls = ["rustls-connector"] 18 | default = ["native-tls"] 19 | # Used to expose helpers in the imap::testing module to build response objects 20 | test_helpers = [] 21 | # Used to activate full integration tests when running against a more complete IMAP server 22 | test-full-imap = [] 23 | 24 | [dependencies] 25 | native-tls = { version = "0.2.2", optional = true } 26 | rustls-connector = { version = "0.19.0", optional = true } 27 | regex = "1.0" 28 | bufstream = "0.1.3" 29 | imap-proto = "0.16.1" 30 | nom = { version = "7.1.0", default-features = false } 31 | base64 = "0.22" 32 | chrono = { version = "0.4.37", default-features = false, features = ["std"]} 33 | lazy_static = "1.4" 34 | ouroboros = "0.18.0" 35 | 36 | [dev-dependencies] 37 | lettre = "0.11" 38 | rustls-connector = "0.19.0" 39 | clap = { version = "4.5.4", features = ["derive"] } 40 | 41 | # to make -Zminimal-versions work 42 | [target.'cfg(any())'.dependencies] 43 | openssl = { version = "0.10.60", optional = true } 44 | openssl-macros = { version = "0.1.1", optional = true } 45 | 46 | [[example]] 47 | name = "basic" 48 | required-features = ["default"] 49 | 50 | [[example]] 51 | name = "gmail_oauth2" 52 | required-features = ["default"] 53 | 54 | [[example]] 55 | name = "idle" 56 | required-features = ["default"] 57 | 58 | [[example]] 59 | name = "rustls" 60 | required-features = ["rustls-tls"] 61 | 62 | [[example]] 63 | name = "starttls" 64 | required-features = ["default"] 65 | 66 | [[example]] 67 | name = "timeout" 68 | required-features = ["default"] 69 | 70 | [[test]] 71 | name = "imap_integration" 72 | required-features = ["default"] 73 | 74 | [[test]] 75 | name = "builder_integration" 76 | required-features = [] 77 | 78 | [package.metadata.docs.rs] 79 | all-features = true 80 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Matt McCoy 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imap 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/imap.svg)](https://crates.io/crates/imap) 4 | [![Documentation](https://docs.rs/imap/badge.svg)](https://docs.rs/imap/) 5 | [![Crate License](https://img.shields.io/crates/l/imap.svg)](https://crates.io/crates/imap) 6 | [![codecov](https://codecov.io/gh/jonhoo/rust-imap/graph/badge.svg?token=2cO5sjtmyR)](https://codecov.io/gh/jonhoo/rust-imap) 7 | [![Dependency status](https://deps.rs/repo/github/jonhoo/rust-imap/status.svg)](https://deps.rs/repo/github/jonhoo/rust-imap) 8 | 9 | This crate lets you connect to and interact with servers that implement the IMAP protocol ([RFC 10 | 3501](https://tools.ietf.org/html/rfc3501) and various extensions). After authenticating with 11 | the server, IMAP lets you list, fetch, and search for e-mails, as well as monitor mailboxes for 12 | changes. It supports at least the latest three stable Rust releases (possibly even older ones; 13 | check the [CI 14 | results](https://dev.azure.com/jonhoo/jonhoo/_build/latest?definitionId=11&branchName=master)). 15 | 16 | **This crate is looking for maintainers — reach out to [@jonhoo] if you're interested.** 17 | 18 | [@jonhoo]: https://thesquareplanet.com/ 19 | 20 | To connect, use the [`ClientBuilder`]. This gives you an unauthenticated [`Client`]. You can 21 | then use [`Client::login`] or [`Client::authenticate`] to perform username/password or 22 | challenge/response authentication respectively. This in turn gives you an authenticated 23 | [`Session`], which lets you access the mailboxes at the server. 24 | 25 | The documentation within this crate borrows heavily from the various RFCs, but should not be 26 | considered a complete reference. If anything is unclear, follow the links to the RFCs embedded 27 | in the documentation for the various types and methods and read the raw text there! 28 | 29 | Below is a basic client example. See the `examples/` directory for more. 30 | 31 | ```rust 32 | fn fetch_inbox_top() -> imap::error::Result> { 33 | 34 | let client = imap::ClientBuilder::new("imap.example.com", 993).connect()?; 35 | 36 | // the client we have here is unauthenticated. 37 | // to do anything useful with the e-mails, we need to log in 38 | let mut imap_session = client 39 | .login("me@example.com", "password") 40 | .map_err(|e| e.0)?; 41 | 42 | // we want to fetch the first email in the INBOX mailbox 43 | imap_session.select("INBOX")?; 44 | 45 | // fetch message number 1 in this mailbox, along with its RFC822 field. 46 | // RFC 822 dictates the format of the body of e-mails 47 | let messages = imap_session.fetch("1", "RFC822")?; 48 | let message = if let Some(m) = messages.iter().next() { 49 | m 50 | } else { 51 | return Ok(None); 52 | }; 53 | 54 | // extract the message's body 55 | let body = message.body().expect("message did not have a body!"); 56 | let body = std::str::from_utf8(body) 57 | .expect("message was not valid utf-8") 58 | .to_string(); 59 | 60 | // be nice to the server and log out 61 | imap_session.logout()?; 62 | 63 | Ok(Some(body)) 64 | } 65 | ``` 66 | 67 | ### Opting out of `native_tls` 68 | 69 | For situations where using openssl becomes problematic, you can disable the 70 | default feature which provides integration with the `native_tls` crate. One major 71 | reason you might want to do this is cross-compiling. To opt out of native_tls, add 72 | this to your Cargo.toml file: 73 | 74 | ```toml 75 | [dependencies.imap] 76 | version = "" 77 | default-features = false 78 | ``` 79 | 80 | Even without `native_tls`, you can still use TLS by leveraging the pure Rust `rustls` 81 | crate, which is enabled with the `rustls-tls` feature. See the example/rustls.rs file 82 | for a working example. 83 | 84 | ## Running the test suite 85 | 86 | To run the integration tests, you need to have [GreenMail 87 | running](http://www.icegreen.com/greenmail/#deploy_docker_standalone). The 88 | easiest way to do that is with Docker: 89 | 90 | ```console 91 | $ docker pull greenmail/standalone:1.6.15 92 | $ docker run -it --rm -e GREENMAIL_OPTS='-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.auth.disabled -Dgreenmail.verbose' -p 3025:3025 -p 3110:3110 -p 3143:3143 -p 3465:3465 -p 3993:3993 -p 3995:3995 greenmail/standalone:1.6.15 93 | ``` 94 | 95 | Another alternative is to test against cyrus imapd which is a more complete IMAP implementation that greenmail (supporting quotas and ACLs). 96 | 97 | ``` 98 | $ docker pull outoforder/cyrus-imapd-tester 99 | $ docker run -it --rm -p 3025:25 -p 3110:110 -p 3143:143 -p 3465:465 -p 3993:993 outoforder/cyrus-imapd-tester:latest 100 | ``` 101 | 102 | ## License 103 | 104 | Licensed under either of 105 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 106 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 107 | at your option. 108 | 109 | ## Contribution 110 | 111 | Unless you explicitly state otherwise, any contribution intentionally submitted 112 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall 113 | be dual licensed as above, without any additional terms or conditions. 114 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | This directory contains examples of working with the IMAP client. 5 | 6 | Examples: 7 | * basic - This is a very basic example of using the client. 8 | * gmail_oauth2 - This is an example using oauth2 for logging into gmail as a secure application. 9 | * idle - This is an example showing how to use IDLE to monitor a mailbox. 10 | * rustls - This demonstrates how to use Rustls instead of Openssl for secure connections (helpful for cross compilation). 11 | * starttls - This is an example showing how to use STARTTLS after connecting over plaintext. 12 | * timeout - This demonstrates how to use timeouts while connecting to an IMAP server by using a custom TCP/TLS stream initialization and creating a `Client` directly instead of using the `ClientBuilder`. 13 | * plaintext - This demonstrates how to make an unencrypted IMAP connection (usually over 143) with a `Client` using a naked TCP connection. 14 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | extern crate imap; 2 | 3 | fn main() { 4 | // To connect to the gmail IMAP server with this you will need to allow unsecure apps access. 5 | // See: https://support.google.com/accounts/answer/6010255?hl=en 6 | // Look at the gmail_oauth2.rs example on how to connect to a gmail server securely. 7 | fetch_inbox_top().unwrap(); 8 | } 9 | 10 | fn fetch_inbox_top() -> imap::error::Result> { 11 | let client = imap::ClientBuilder::new("imap.example.com", 993).connect()?; 12 | 13 | // the client we have here is unauthenticated. 14 | // to do anything useful with the e-mails, we need to log in 15 | let mut imap_session = client 16 | .login("me@example.com", "password") 17 | .map_err(|e| e.0)?; 18 | 19 | // we want to fetch the first email in the INBOX mailbox 20 | imap_session.select("INBOX")?; 21 | 22 | // fetch message number 1 in this mailbox, along with its RFC822 field. 23 | // RFC 822 dictates the format of the body of e-mails 24 | let messages = imap_session.fetch("1", "RFC822")?; 25 | let message = if let Some(m) = messages.iter().next() { 26 | m 27 | } else { 28 | return Ok(None); 29 | }; 30 | 31 | // extract the message's body 32 | let body = message.body().expect("message did not have a body!"); 33 | let body = std::str::from_utf8(body) 34 | .expect("message was not valid utf-8") 35 | .to_string(); 36 | 37 | // be nice to the server and log out 38 | imap_session.logout()?; 39 | 40 | Ok(Some(body)) 41 | } 42 | -------------------------------------------------------------------------------- /examples/gmail_oauth2.rs: -------------------------------------------------------------------------------- 1 | extern crate base64; 2 | extern crate imap; 3 | 4 | struct GmailOAuth2 { 5 | user: String, 6 | access_token: String, 7 | } 8 | 9 | impl imap::Authenticator for GmailOAuth2 { 10 | type Response = String; 11 | #[allow(unused_variables)] 12 | fn process(&self, data: &[u8]) -> Self::Response { 13 | format!( 14 | "user={}\x01auth=Bearer {}\x01\x01", 15 | self.user, self.access_token 16 | ) 17 | } 18 | } 19 | 20 | fn main() { 21 | let gmail_auth = GmailOAuth2 { 22 | user: String::from("sombody@gmail.com"), 23 | access_token: String::from(""), 24 | }; 25 | 26 | let client = imap::ClientBuilder::new("imap.gmail.com", 993) 27 | .connect() 28 | .expect("Could not connect to imap.gmail.com"); 29 | 30 | let mut imap_session = match client.authenticate("XOAUTH2", &gmail_auth) { 31 | Ok(c) => c, 32 | Err((e, _unauth_client)) => { 33 | println!("error authenticating: {}", e); 34 | return; 35 | } 36 | }; 37 | 38 | match imap_session.select("INBOX") { 39 | Ok(mailbox) => println!("{}", mailbox), 40 | Err(e) => println!("Error selecting INBOX: {}", e), 41 | }; 42 | 43 | match imap_session.fetch("2", "body[text]") { 44 | Ok(msgs) => { 45 | for msg in msgs.iter() { 46 | print!("{:?}", msg); 47 | } 48 | } 49 | Err(e) => println!("Error Fetching email 2: {}", e), 50 | }; 51 | 52 | imap_session.logout().unwrap(); 53 | } 54 | -------------------------------------------------------------------------------- /examples/idle.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser, Debug)] 4 | #[clap(name = "idle")] 5 | struct Opt { 6 | // The server name to connect to 7 | #[clap(short, long)] 8 | server: String, 9 | 10 | // The port to use 11 | #[clap(short, long, default_value = "993")] 12 | port: u16, 13 | 14 | // The account username 15 | #[clap(short, long)] 16 | username: String, 17 | 18 | // The account password. In a production system passwords 19 | // would normally be in a config or fetched at runtime from 20 | // a password manager or user prompt and not passed on the 21 | // command line. 22 | #[clap(short = 'w', long)] 23 | password: String, 24 | 25 | // The mailbox to IDLE on 26 | #[clap(short, long, default_value = "INBOX")] 27 | mailbox: String, 28 | 29 | #[clap( 30 | short = 'x', 31 | long, 32 | help = "The number of responses to receive before exiting", 33 | default_value = "5" 34 | )] 35 | max_responses: usize, 36 | } 37 | 38 | fn main() { 39 | let opt = Opt::parse(); 40 | 41 | let client = imap::ClientBuilder::new(opt.server.clone(), opt.port) 42 | .connect() 43 | .expect("Could not connect to imap server"); 44 | 45 | let mut imap = client 46 | .login(opt.username, opt.password) 47 | .expect("Could not authenticate"); 48 | 49 | // Turn on debug output so we can see the actual traffic coming 50 | // from the server and how it is handled in our callback. 51 | // This wouldn't be turned on in a production build, but is helpful 52 | // in examples and for debugging. 53 | imap.debug = true; 54 | 55 | imap.select(opt.mailbox).expect("Could not select mailbox"); 56 | 57 | // Implement a trivial counter that causes the IDLE callback to end the IDLE 58 | // after a fixed number of responses. 59 | // 60 | // A threaded client could use channels or shared data to interact with the 61 | // rest of the program and update mailbox state, decide to exit the IDLE, etc. 62 | let mut num_responses = 0; 63 | let max_responses = opt.max_responses; 64 | let idle_result = imap.idle().wait_while(|response| { 65 | num_responses += 1; 66 | println!("IDLE response #{}: {:?}", num_responses, response); 67 | if num_responses >= max_responses { 68 | // Stop IDLE 69 | false 70 | } else { 71 | // Continue IDLE 72 | true 73 | } 74 | }); 75 | 76 | match idle_result { 77 | Ok(reason) => println!("IDLE finished normally {:?}", reason), 78 | Err(e) => println!("IDLE finished with error {:?}", e), 79 | } 80 | 81 | imap.logout().expect("Could not log out"); 82 | } 83 | -------------------------------------------------------------------------------- /examples/plaintext.rs: -------------------------------------------------------------------------------- 1 | use std::net::TcpStream; 2 | 3 | fn main() { 4 | // REMINDER this is dangerous, the credentials are sent over the connection in CLEARTEXT! 5 | // Anyone or anything between this connection and the server can read your login creds! 6 | // Please oh please do not use this where this is even a possibility. 7 | match plaintext() { 8 | Ok(conn) => { 9 | eprintln!("Connection successful!"); 10 | println!("{:?}", conn); 11 | } 12 | Err(e) => { 13 | eprintln!("Connection error!"); 14 | eprintln!("{:?}", e); 15 | } 16 | } 17 | } 18 | 19 | fn plaintext() -> imap::error::Result> { 20 | let stream = TcpStream::connect("imap.example.com:143").unwrap(); 21 | let mut client = imap::Client::new(stream); 22 | client.read_greeting()?; 23 | eprintln!("\nUNENCRYPTED connection made!!!!\n"); 24 | eprintln!("This is highly not recommended.\n"); 25 | // to do anything useful with the e-mails, we need to log in 26 | // keep in mind that this is over plain TCP, so may leak all your secrets! 27 | let mut imap_session = client.login("user", "pass").unwrap(); 28 | 29 | // we want to fetch the first email in the INBOX mailbox 30 | imap_session.select("INBOX")?; 31 | 32 | // fetch message number 1 in this mailbox, along with its RFC822 field. 33 | // RFC 822 dictates the format of the body of e-mails 34 | let messages = imap_session.fetch("1", "RFC822")?; 35 | let message = if let Some(m) = messages.iter().next() { 36 | m 37 | } else { 38 | return Ok(None); 39 | }; 40 | 41 | // extract the message's body 42 | let body = message 43 | .body() 44 | .map(|body| String::from_utf8(body.to_vec()).expect("message was not valid utf-8")) 45 | .unwrap_or_else(String::new); 46 | 47 | // be nice to the server and log out 48 | imap_session.logout()?; 49 | Ok(Some(body)) 50 | } 51 | -------------------------------------------------------------------------------- /examples/rustls.rs: -------------------------------------------------------------------------------- 1 | extern crate imap; 2 | 3 | use std::{env, error::Error}; 4 | 5 | fn main() -> Result<(), Box> { 6 | // Read config from environment or .env file 7 | let host = env::var("HOST").expect("missing envvar host"); 8 | let user = env::var("MAILUSER").expect("missing envvar USER"); 9 | let password = env::var("PASSWORD").expect("missing envvar password"); 10 | let port = 993; 11 | 12 | if let Some(email) = fetch_inbox_top(host, user, password, port)? { 13 | println!("{}", &email); 14 | } 15 | 16 | Ok(()) 17 | } 18 | 19 | fn fetch_inbox_top( 20 | host: String, 21 | user: String, 22 | password: String, 23 | port: u16, 24 | ) -> Result, Box> { 25 | let client = imap::ClientBuilder::new(&host, port).connect()?; 26 | 27 | // the client we have here is unauthenticated. 28 | // to do anything useful with the e-mails, we need to log in 29 | let mut imap_session = client.login(&user, &password).map_err(|e| e.0)?; 30 | 31 | // we want to fetch the first email in the INBOX mailbox 32 | imap_session.select("INBOX")?; 33 | 34 | // fetch message number 1 in this mailbox, along with its RFC822 field. 35 | // RFC 822 dictates the format of the body of e-mails 36 | let messages = imap_session.fetch("1", "RFC822")?; 37 | let message = if let Some(m) = messages.iter().next() { 38 | m 39 | } else { 40 | return Ok(None); 41 | }; 42 | 43 | // extract the message's body 44 | let body = message.body().expect("message did not have a body!"); 45 | let body = std::str::from_utf8(body) 46 | .expect("message was not valid utf-8") 47 | .to_string(); 48 | 49 | // be nice to the server and log out 50 | imap_session.logout()?; 51 | 52 | Ok(Some(body)) 53 | } 54 | -------------------------------------------------------------------------------- /examples/starttls.rs: -------------------------------------------------------------------------------- 1 | /** 2 | * Here's an example showing how to connect to the IMAP server with STARTTLS. 3 | * 4 | * The only difference is calling `starttls()` on the `ClientBuilder` before 5 | * initiating the secure connection with `connect()`, so you 6 | * can connect on port 143 instead of 993. 7 | * 8 | * The following env vars are expected to be set: 9 | * - IMAP_HOST 10 | * - IMAP_USERNAME 11 | * - IMAP_PASSWORD 12 | * - IMAP_PORT (supposed to be 143) 13 | */ 14 | extern crate imap; 15 | 16 | use std::env; 17 | use std::error::Error; 18 | 19 | fn main() -> Result<(), Box> { 20 | let imap_host = env::var("IMAP_HOST").expect("Missing or invalid env var: IMAP_HOST"); 21 | let imap_username = 22 | env::var("IMAP_USERNAME").expect("Missing or invalid env var: IMAP_USERNAME"); 23 | let imap_password = 24 | env::var("IMAP_PASSWORD").expect("Missing or invalid env var: IMAP_PASSWORD"); 25 | let imap_port: u16 = env::var("IMAP_PORT") 26 | .expect("Missing or invalid env var: IMAP_PORT") 27 | .to_string() 28 | .parse() 29 | .unwrap(); 30 | 31 | if let Some(_email) = fetch_inbox_top(imap_host, imap_username, imap_password, imap_port)? { 32 | eprintln!("OK :)"); 33 | } 34 | 35 | Ok(()) 36 | } 37 | 38 | fn fetch_inbox_top( 39 | host: String, 40 | username: String, 41 | password: String, 42 | port: u16, 43 | ) -> Result, Box> { 44 | let client = imap::ClientBuilder::new(&host, port) 45 | .connect() 46 | .expect("Could not connect to server"); 47 | 48 | // the client we have here is unauthenticated. 49 | // to do anything useful with the e-mails, we need to log in 50 | let mut _imap_session = client 51 | .login(username.as_str(), password.as_str()) 52 | .map_err(|e| e.0)?; 53 | 54 | // TODO Here you can process as you want. eg. search/fetch messages according to your needs. 55 | 56 | // This returns `Ok(None)` for the need of the example 57 | Ok(None) 58 | } 59 | -------------------------------------------------------------------------------- /examples/timeout.rs: -------------------------------------------------------------------------------- 1 | extern crate imap; 2 | extern crate native_tls; 3 | 4 | use imap::Client; 5 | use native_tls::TlsConnector; 6 | use native_tls::TlsStream; 7 | use std::env; 8 | use std::error::Error; 9 | use std::fmt; 10 | use std::net::{SocketAddr, TcpStream, ToSocketAddrs}; 11 | use std::time::Duration; 12 | 13 | fn main() -> Result<(), Box> { 14 | let server = env::var("IMAP_SERVER")?; 15 | let port = env::var("IMAP_PORT").unwrap_or_else(|_| String::from("993")); 16 | let port = port.parse()?; 17 | 18 | let username = env::var("IMAP_USER")?; 19 | let password = env::var("IMAP_PASSWORD")?; 20 | 21 | let timeout = env::var("IMAP_TIMEOUT").unwrap_or_else(|_| String::from("1")); 22 | let timeout = timeout.parse()?; 23 | let timeout = Duration::from_secs(timeout); 24 | 25 | let tls = TlsConnector::builder().build()?; 26 | 27 | let client = connect_all_timeout((server.as_str(), port), server.as_str(), &tls, timeout)?; 28 | 29 | let mut session = client.login(&username, &password).map_err(|e| e.0)?; 30 | 31 | // do something productive with session 32 | 33 | session.logout()?; 34 | 35 | Ok(()) 36 | } 37 | 38 | // connect to an IMAP host with a `Duration` timeout; note that this accepts only a single 39 | // `SocketAddr` while `connect_all_timeout` does resolve the DNS entry and try to connect to all; 40 | // this is necessary due to the difference of the function signatures of `TcpStream::connect` and 41 | // `TcpStream::connect_timeout` 42 | fn connect_timeout>( 43 | addr: &SocketAddr, 44 | domain: S, 45 | ssl_connector: &TlsConnector, 46 | timeout: Duration, 47 | ) -> Result>, Box> { 48 | // the timeout is actually used with the initial TcpStream 49 | let tcp_stream = TcpStream::connect_timeout(addr, timeout)?; 50 | 51 | let tls_stream = TlsConnector::connect(ssl_connector, domain.as_ref(), tcp_stream)?; 52 | 53 | let mut client = Client::new(tls_stream); 54 | 55 | // don't forget to wait for the IMAP protocol server greeting ;) 56 | client.read_greeting()?; 57 | 58 | Ok(client) 59 | } 60 | 61 | // resolve address and try to connect to all in order 62 | fn connect_all_timeout>( 63 | addr: A, 64 | domain: S, 65 | ssl_connector: &TlsConnector, 66 | timeout: Duration, 67 | ) -> Result>, Box> { 68 | let addrs = addr.to_socket_addrs()?; 69 | 70 | for addr in addrs { 71 | match connect_timeout(&addr, &domain, ssl_connector, timeout) { 72 | Ok(client) => return Ok(client), 73 | Err(error) => eprintln!("couldn't connect to {}: {}", addr, error), 74 | } 75 | } 76 | 77 | Err(Box::new(TimeoutError)) 78 | } 79 | 80 | // very simple timeout error; instead of printing the errors immediately like in 81 | // `connect_all_timeout`, you may want to collect and return them 82 | #[derive(Debug)] 83 | struct TimeoutError; 84 | 85 | impl fmt::Display for TimeoutError { 86 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 87 | write!(f, "all addresses failed to connect") 88 | } 89 | } 90 | 91 | impl Error for TimeoutError {} 92 | -------------------------------------------------------------------------------- /src/authenticator.rs: -------------------------------------------------------------------------------- 1 | /// This trait allows for pluggable authentication schemes. It is used by `Client::authenticate` to 2 | /// [authenticate using SASL](https://tools.ietf.org/html/rfc3501#section-6.2.2). 3 | pub trait Authenticator { 4 | /// The type of the response to the challenge. This will usually be a `Vec` or `String`. 5 | type Response: AsRef<[u8]>; 6 | 7 | /// Each base64-decoded server challenge is passed to `process`. 8 | /// The returned byte-string is base64-encoded and then sent back to the server. 9 | fn process(&self, challenge: &[u8]) -> Self::Response; 10 | } 11 | -------------------------------------------------------------------------------- /src/client_builder.rs: -------------------------------------------------------------------------------- 1 | use crate::{Client, Connection, Error, Result}; 2 | 3 | use lazy_static::lazy_static; 4 | use std::io::{Read, Write}; 5 | use std::net::TcpStream; 6 | 7 | #[cfg(feature = "native-tls")] 8 | use native_tls::TlsConnector as NativeTlsConnector; 9 | 10 | use crate::extensions::idle::SetReadTimeout; 11 | #[cfg(feature = "rustls-tls")] 12 | use rustls_connector::{ 13 | rustls, 14 | rustls::pki_types::{CertificateDer, ServerName}, 15 | rustls::{ClientConfig, RootCertStore}, 16 | rustls_native_certs::load_native_certs, 17 | RustlsConnector, 18 | }; 19 | #[cfg(feature = "rustls-tls")] 20 | use std::sync::Arc; 21 | 22 | #[cfg(feature = "rustls-tls")] 23 | #[derive(Debug)] 24 | struct NoCertVerification(Arc); 25 | 26 | #[cfg(feature = "rustls-tls")] 27 | impl rustls::client::danger::ServerCertVerifier for NoCertVerification { 28 | fn verify_server_cert( 29 | &self, 30 | _: &CertificateDer<'_>, 31 | _: &[CertificateDer<'_>], 32 | _: &ServerName<'_>, 33 | _: &[u8], 34 | _: rustls::pki_types::UnixTime, 35 | ) -> std::result::Result { 36 | Ok(rustls::client::danger::ServerCertVerified::assertion()) 37 | } 38 | 39 | fn verify_tls12_signature( 40 | &self, 41 | message: &[u8], 42 | cert: &CertificateDer<'_>, 43 | dss: &rustls::DigitallySignedStruct, 44 | ) -> std::result::Result { 45 | self.0.verify_tls12_signature(message, cert, dss) 46 | } 47 | 48 | fn verify_tls13_signature( 49 | &self, 50 | message: &[u8], 51 | cert: &CertificateDer<'_>, 52 | dss: &rustls::DigitallySignedStruct, 53 | ) -> std::prelude::v1::Result 54 | { 55 | self.0.verify_tls13_signature(message, cert, dss) 56 | } 57 | 58 | fn supported_verify_schemes(&self) -> Vec { 59 | self.0.supported_verify_schemes() 60 | } 61 | } 62 | 63 | #[cfg(feature = "rustls-tls")] 64 | lazy_static! { 65 | static ref CACERTS: RootCertStore = { 66 | let mut store = RootCertStore::empty(); 67 | for cert in load_native_certs().unwrap_or_else(|_| vec![]) { 68 | if let Ok(_) = store.add(cert) {} 69 | } 70 | store 71 | }; 72 | } 73 | 74 | lazy_static! { 75 | static ref STARTLS_CHECK_REGEX: regex::bytes::Regex = 76 | regex::bytes::Regex::new(r"\bSTARTTLS\b").unwrap(); 77 | } 78 | 79 | /// The connection mode we are going to use 80 | #[derive(Clone, Debug, PartialEq, Eq)] 81 | #[non_exhaustive] 82 | pub enum ConnectionMode { 83 | /// Automatically detect what connection mode should be used. 84 | /// 85 | /// This will use TLS if the port is 993, and otherwise STARTTLS if available. 86 | /// If no TLS communication mechanism is available, the connection will fail. 87 | AutoTls, 88 | /// Automatically detect what connection mode should be used. 89 | /// 90 | /// This will use TLS if the port is 993, and otherwise STARTTLS if available. 91 | /// It will fallback to a plaintext connection if no TLS option can be used. 92 | Auto, 93 | /// A plain unencrypted TCP connection 94 | Plaintext, 95 | /// An encrypted TLS connection 96 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 97 | Tls, 98 | /// An eventually-encrypted (i.e., STARTTLS) connection 99 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 100 | StartTls, 101 | } 102 | 103 | /// A selection for TLS implementation 104 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 105 | #[derive(Clone, Debug, Eq, PartialEq)] 106 | #[non_exhaustive] 107 | pub enum TlsKind { 108 | /// Use the NativeTLS backend 109 | #[cfg(feature = "native-tls")] 110 | Native, 111 | /// Use the Rustls backend 112 | #[cfg(feature = "rustls-tls")] 113 | Rust, 114 | /// Use whatever backend is available (uses rustls if both are available) 115 | Any, 116 | } 117 | 118 | /// A convenience builder for [`Client`] structs over various encrypted transports. 119 | /// 120 | /// Creating a [`Client`] using TLS is straightforward. 121 | /// 122 | /// This will make a TLS connection directly since the port is 993. 123 | /// ```no_run 124 | /// # use imap::ClientBuilder; 125 | /// # {} #[cfg(feature = "native-tls")] 126 | /// # fn main() -> Result<(), imap::Error> { 127 | /// let client = ClientBuilder::new("imap.example.com", 993).connect()?; 128 | /// # Ok(()) 129 | /// # } 130 | /// ``` 131 | /// 132 | /// By default it will detect and use `STARTTLS` if available. 133 | /// ```no_run 134 | /// # use imap::ClientBuilder; 135 | /// # {} #[cfg(feature = "native-tls")] 136 | /// # fn main() -> Result<(), imap::Error> { 137 | /// let client = ClientBuilder::new("imap.example.com", 143).connect()?; 138 | /// # Ok(()) 139 | /// # } 140 | /// ``` 141 | /// 142 | /// To force a certain implementation you can call tls_kind(): 143 | /// ```no_run 144 | /// # use imap::ClientBuilder; 145 | /// # {} #[cfg(feature = "rustls-tls")] 146 | /// # fn main() -> Result<(), imap::Error> { 147 | /// let client = ClientBuilder::new("imap.example.com", 993) 148 | /// .tls_kind(imap::TlsKind::Rust).connect()?; 149 | /// # Ok(()) 150 | /// # } 151 | /// ``` 152 | /// 153 | /// To force the use `STARTTLS`, just call `mode()` before connect(): 154 | /// 155 | /// If the server does not provide STARTTLS this will error out. 156 | /// ```no_run 157 | /// # use imap::ClientBuilder; 158 | /// # {} #[cfg(feature = "rustls-tls")] 159 | /// # fn main() -> Result<(), imap::Error> { 160 | /// use imap::ConnectionMode; 161 | /// let client = ClientBuilder::new("imap.example.com", 993) 162 | /// .mode(ConnectionMode::StartTls) 163 | /// .connect()?; 164 | /// # Ok(()) 165 | /// # } 166 | /// ``` 167 | /// The returned [`Client`] is unauthenticated; to access session-related methods (through 168 | /// [`Session`](crate::Session)), use [`Client::login`] or [`Client::authenticate`]. 169 | #[derive(Clone)] 170 | pub struct ClientBuilder 171 | where 172 | D: AsRef, 173 | { 174 | domain: D, 175 | port: u16, 176 | mode: ConnectionMode, 177 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 178 | tls_kind: TlsKind, 179 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 180 | skip_tls_verify: bool, 181 | } 182 | 183 | impl ClientBuilder 184 | where 185 | D: AsRef, 186 | { 187 | /// Make a new `ClientBuilder` using the given domain and port. 188 | pub fn new(domain: D, port: u16) -> Self { 189 | ClientBuilder { 190 | domain, 191 | port, 192 | mode: ConnectionMode::AutoTls, 193 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 194 | tls_kind: TlsKind::Any, 195 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 196 | skip_tls_verify: false, 197 | } 198 | } 199 | 200 | /// Sets the Connection mode to use for this connection 201 | pub fn mode(mut self, mode: ConnectionMode) -> Self { 202 | self.mode = mode; 203 | self 204 | } 205 | 206 | /// Sets the TLS backend to use for this connection. 207 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 208 | pub fn tls_kind(mut self, kind: TlsKind) -> Self { 209 | self.tls_kind = kind; 210 | self 211 | } 212 | 213 | /// Controls the use of certificate validation. 214 | /// 215 | /// Defaults to `false`. 216 | /// 217 | /// # Warning 218 | /// 219 | /// You should only use this as a last resort as it allows another server to impersonate the 220 | /// server you think you're talking to, which would include being able to receive your 221 | /// credentials. 222 | /// 223 | /// See [`native_tls::TlsConnectorBuilder::danger_accept_invalid_certs`], 224 | /// [`native_tls::TlsConnectorBuilder::danger_accept_invalid_hostnames`], 225 | /// [`rustls::ClientConfig::dangerous`] 226 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 227 | pub fn danger_skip_tls_verify(mut self, skip_tls_verify: bool) -> Self { 228 | self.skip_tls_verify = skip_tls_verify; 229 | self 230 | } 231 | 232 | /// Make a [`Client`] using the configuration. 233 | /// 234 | /// ```no_run 235 | /// # use imap::ClientBuilder; 236 | /// # {} #[cfg(feature = "rustls-tls")] 237 | /// # fn main() -> Result<(), imap::Error> { 238 | /// let client = ClientBuilder::new("imap.example.com", 143).connect()?; 239 | /// # Ok(()) 240 | /// # } 241 | /// ``` 242 | pub fn connect(&self) -> Result> { 243 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 244 | return self.connect_with(|_domain, tcp| self.build_tls_connection(tcp)); 245 | #[cfg(all(not(feature = "native-tls"), not(feature = "rustls-tls")))] 246 | return self.connect_with(|_domain, _tcp| -> Result { 247 | return Err(Error::TlsNotConfigured); 248 | }); 249 | } 250 | 251 | #[allow(unused_variables)] 252 | fn connect_with(&self, handshake: F) -> Result> 253 | where 254 | F: FnOnce(&str, TcpStream) -> Result, 255 | C: Read + Write + Send + SetReadTimeout + 'static, 256 | { 257 | #[allow(unused_mut)] 258 | let mut greeting_read = false; 259 | let tcp = TcpStream::connect((self.domain.as_ref(), self.port))?; 260 | 261 | let stream: Connection = match self.mode { 262 | ConnectionMode::AutoTls => { 263 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 264 | if self.port == 993 { 265 | Box::new(handshake(self.domain.as_ref(), tcp)?) 266 | } else { 267 | let (stream, upgraded) = self.upgrade_tls(Client::new(tcp), handshake)?; 268 | greeting_read = true; 269 | 270 | if !upgraded { 271 | Err(Error::StartTlsNotAvailable)? 272 | } 273 | stream 274 | } 275 | #[cfg(all(not(feature = "native-tls"), not(feature = "rustls-tls")))] 276 | Err(Error::TlsNotConfigured)? 277 | } 278 | ConnectionMode::Auto => { 279 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 280 | if self.port == 993 { 281 | Box::new(handshake(self.domain.as_ref(), tcp)?) 282 | } else { 283 | let (stream, _upgraded) = self.upgrade_tls(Client::new(tcp), handshake)?; 284 | greeting_read = true; 285 | 286 | stream 287 | } 288 | #[cfg(all(not(feature = "native-tls"), not(feature = "rustls-tls")))] 289 | Box::new(tcp) 290 | } 291 | ConnectionMode::Plaintext => Box::new(tcp), 292 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 293 | ConnectionMode::StartTls => { 294 | let (stream, upgraded) = self.upgrade_tls(Client::new(tcp), handshake)?; 295 | greeting_read = true; 296 | 297 | if !upgraded { 298 | Err(Error::StartTlsNotAvailable)? 299 | } 300 | stream 301 | } 302 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 303 | ConnectionMode::Tls => Box::new(handshake(self.domain.as_ref(), tcp)?), 304 | }; 305 | 306 | let mut client = Client::new(stream); 307 | if !greeting_read { 308 | client.read_greeting()?; 309 | } else { 310 | client.greeting_read = true; 311 | } 312 | 313 | Ok(client) 314 | } 315 | 316 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 317 | fn upgrade_tls( 318 | &self, 319 | mut client: Client, 320 | handshake: F, 321 | ) -> Result<(Connection, bool)> 322 | where 323 | F: FnOnce(&str, TcpStream) -> Result, 324 | C: Read + Write + Send + SetReadTimeout + 'static, 325 | { 326 | client.read_greeting()?; 327 | 328 | let capabilities = client.capabilities()?; 329 | if capabilities.has(&imap_proto::Capability::Atom("STARTTLS".into())) { 330 | client.run_command_and_check_ok("STARTTLS")?; 331 | let tcp = client.into_inner()?; 332 | Ok((Box::new(handshake(self.domain.as_ref(), tcp)?), true)) 333 | } else { 334 | Ok((Box::new(client.into_inner()?), false)) 335 | } 336 | } 337 | 338 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 339 | fn build_tls_connection(&self, tcp: TcpStream) -> Result { 340 | match self.tls_kind { 341 | #[cfg(feature = "native-tls")] 342 | TlsKind::Native => self.build_tls_native(tcp), 343 | #[cfg(feature = "rustls-tls")] 344 | TlsKind::Rust => self.build_tls_rustls(tcp), 345 | TlsKind::Any => self.build_tls_any(tcp), 346 | } 347 | } 348 | 349 | #[cfg(feature = "rustls-tls")] 350 | fn build_tls_any(&self, tcp: TcpStream) -> Result { 351 | self.build_tls_rustls(tcp) 352 | } 353 | 354 | #[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))] 355 | fn build_tls_any(&self, tcp: TcpStream) -> Result { 356 | self.build_tls_native(tcp) 357 | } 358 | 359 | #[cfg(feature = "rustls-tls")] 360 | fn build_tls_rustls(&self, tcp: TcpStream) -> Result { 361 | let mut config = ClientConfig::builder() 362 | .with_root_certificates(CACERTS.clone()) 363 | .with_no_client_auth(); 364 | if self.skip_tls_verify { 365 | config 366 | .dangerous() 367 | .set_certificate_verifier(Arc::new(NoCertVerification( 368 | rustls::client::WebPkiServerVerifier::builder(Arc::new(CACERTS.clone())) 369 | .build() 370 | .expect("can construct standard verifier"), 371 | ))); 372 | } 373 | let ssl_conn: RustlsConnector = config.into(); 374 | Ok(Box::new(ssl_conn.connect(self.domain.as_ref(), tcp)?)) 375 | } 376 | 377 | #[cfg(feature = "native-tls")] 378 | fn build_tls_native(&self, tcp: TcpStream) -> Result { 379 | let mut builder = NativeTlsConnector::builder(); 380 | if self.skip_tls_verify { 381 | builder.danger_accept_invalid_certs(true); 382 | builder.danger_accept_invalid_hostnames(true); 383 | } 384 | let ssl_conn = builder.build()?; 385 | Ok(Box::new(NativeTlsConnector::connect( 386 | &ssl_conn, 387 | self.domain.as_ref(), 388 | tcp, 389 | )?)) 390 | } 391 | } 392 | 393 | #[cfg(test)] 394 | mod tests { 395 | use super::*; 396 | 397 | mod connection_mode { 398 | use super::*; 399 | 400 | #[test] 401 | fn connection_mode_eq() { 402 | assert_eq!(ConnectionMode::Auto, ConnectionMode::Auto); 403 | } 404 | 405 | #[test] 406 | fn connection_mode_ne() { 407 | assert_ne!(ConnectionMode::Auto, ConnectionMode::AutoTls); 408 | } 409 | } 410 | 411 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 412 | mod tls_kind { 413 | use super::*; 414 | 415 | #[test] 416 | fn connection_mode_eq() { 417 | assert_eq!(TlsKind::Any, TlsKind::Any); 418 | } 419 | 420 | #[cfg(feature = "native-tls")] 421 | #[test] 422 | fn connection_mode_ne_native() { 423 | assert_ne!(TlsKind::Any, TlsKind::Native); 424 | } 425 | 426 | #[cfg(feature = "rustls-tls")] 427 | #[test] 428 | fn connection_mode_ne_rust() { 429 | assert_ne!(TlsKind::Any, TlsKind::Rust); 430 | } 431 | } 432 | 433 | mod client_builder { 434 | use super::*; 435 | 436 | #[test] 437 | fn can_clone() { 438 | let builder = ClientBuilder::new("imap.example.com", 143); 439 | 440 | let clone = builder.clone(); 441 | assert_eq!(clone.domain, builder.domain); 442 | assert_eq!(clone.port, builder.port); 443 | } 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /src/conn.rs: -------------------------------------------------------------------------------- 1 | use crate::extensions::idle::SetReadTimeout; 2 | 3 | use std::fmt::{Debug, Formatter}; 4 | use std::io::{Read, Write}; 5 | 6 | /// Imap connection trait of a read/write stream 7 | pub trait ImapConnection: Read + Write + Send + SetReadTimeout + private::Sealed {} 8 | 9 | impl ImapConnection for T where T: Read + Write + Send + SetReadTimeout {} 10 | 11 | impl Debug for dyn ImapConnection { 12 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 13 | write!(f, "Imap connection") 14 | } 15 | } 16 | 17 | /// A boxed connection type 18 | pub type Connection = Box; 19 | 20 | mod private { 21 | use super::{Read, SetReadTimeout, Write}; 22 | 23 | pub trait Sealed {} 24 | 25 | impl Sealed for T where T: Read + Write + SetReadTimeout {} 26 | } 27 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! IMAP error types. 2 | 3 | use std::error::Error as StdError; 4 | use std::fmt; 5 | use std::io::Error as IoError; 6 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 7 | use std::net::TcpStream; 8 | use std::result; 9 | use std::str::Utf8Error; 10 | 11 | use base64::DecodeError; 12 | use bufstream::IntoInnerError as BufError; 13 | use imap_proto::{types::ResponseCode, Response}; 14 | #[cfg(feature = "native-tls")] 15 | use native_tls::Error as TlsError; 16 | #[cfg(feature = "native-tls")] 17 | use native_tls::HandshakeError as TlsHandshakeError; 18 | #[cfg(feature = "rustls-tls")] 19 | use rustls_connector::HandshakeError as RustlsHandshakeError; 20 | 21 | /// A convenience wrapper around `Result` for `imap::Error`. 22 | pub type Result = result::Result; 23 | 24 | /// A BAD response from the server, which indicates an error message from the server. 25 | #[derive(Debug)] 26 | #[non_exhaustive] 27 | pub struct Bad { 28 | /// Human-readable message included with the Bad response. 29 | pub information: String, 30 | /// A more specific error status code included with the Bad response. 31 | pub code: Option>, 32 | } 33 | 34 | impl fmt::Display for Bad { 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | write!(f, "{}", self.information) 37 | } 38 | } 39 | 40 | /// A NO response from the server, which indicates an operational error message from the server. 41 | #[derive(Debug)] 42 | #[non_exhaustive] 43 | pub struct No { 44 | /// Human-readable message included with the NO response. 45 | pub information: String, 46 | /// A more specific error status code included with the NO response. 47 | pub code: Option>, 48 | } 49 | 50 | impl fmt::Display for No { 51 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 52 | write!(f, "{}", self.information) 53 | } 54 | } 55 | 56 | /// A BYE response from the server, which indicates it is going to hang up on us. 57 | #[derive(Debug)] 58 | #[non_exhaustive] 59 | pub struct Bye { 60 | /// Human-readable message included with the response. 61 | pub information: String, 62 | /// A more specific error status code included with the response. 63 | pub code: Option>, 64 | } 65 | 66 | impl fmt::Display for Bye { 67 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 68 | write!(f, "{}", self.information) 69 | } 70 | } 71 | /// A set of errors that can occur in the IMAP client 72 | #[derive(Debug)] 73 | #[non_exhaustive] 74 | pub enum Error { 75 | /// An `io::Error` that occurred while trying to read or write to a network stream. 76 | Io(IoError), 77 | /// An error from the `rustls` library during the TLS handshake. 78 | #[cfg(feature = "rustls-tls")] 79 | RustlsHandshake(RustlsHandshakeError), 80 | /// An error from the `native_tls` library during the TLS handshake. 81 | #[cfg(feature = "native-tls")] 82 | TlsHandshake(TlsHandshakeError), 83 | /// An error from the `native_tls` library while managing the socket. 84 | #[cfg(feature = "native-tls")] 85 | Tls(TlsError), 86 | /// A BAD response from the IMAP server. 87 | Bad(Bad), 88 | /// A NO response from the IMAP server. 89 | No(No), 90 | /// A BYE response from the IMAP server. 91 | Bye(Bye), 92 | /// The connection was terminated unexpectedly. 93 | ConnectionLost, 94 | /// Error parsing a server response. 95 | Parse(ParseError), 96 | /// Command inputs were not valid [IMAP 97 | /// strings](https://tools.ietf.org/html/rfc3501#section-4.3). 98 | Validate(ValidateError), 99 | /// Error appending an e-mail. 100 | Append, 101 | /// An unexpected response was received. This could be a response from a command, 102 | /// or an unsolicited response that could not be converted into a local type in 103 | /// [`UnsolicitedResponse`](crate::types::UnsolicitedResponse). 104 | Unexpected(Response<'static>), 105 | /// In response to a STATUS command, the server sent OK without actually sending any STATUS 106 | /// responses first. 107 | MissingStatusResponse, 108 | /// The server responded with a different command tag than the one we just sent. 109 | /// 110 | /// A new session must generally be established to recover from this. You can also use 111 | /// [`Connection::skip_tag`](crate::client::Connection::skip_tag) (which is available through 112 | /// both [`Client`](crate::Client) and [`Session`](crate::Session)). 113 | TagMismatch(TagMismatch), 114 | /// StartTls is not available on the server 115 | StartTlsNotAvailable, 116 | /// Returns when Tls is not configured 117 | TlsNotConfigured, 118 | } 119 | 120 | impl From for Error { 121 | fn from(err: IoError) -> Error { 122 | Error::Io(err) 123 | } 124 | } 125 | 126 | impl From for Error { 127 | fn from(err: ParseError) -> Error { 128 | Error::Parse(err) 129 | } 130 | } 131 | 132 | impl From> for Error { 133 | fn from(err: BufError) -> Error { 134 | Error::Io(err.into()) 135 | } 136 | } 137 | 138 | #[cfg(feature = "rustls-tls")] 139 | impl From> for Error { 140 | fn from(err: RustlsHandshakeError) -> Error { 141 | Error::RustlsHandshake(err) 142 | } 143 | } 144 | 145 | #[cfg(feature = "native-tls")] 146 | impl From> for Error { 147 | fn from(err: TlsHandshakeError) -> Error { 148 | Error::TlsHandshake(err) 149 | } 150 | } 151 | 152 | #[cfg(feature = "native-tls")] 153 | impl From for Error { 154 | fn from(err: TlsError) -> Error { 155 | Error::Tls(err) 156 | } 157 | } 158 | 159 | impl<'a> From> for Error { 160 | fn from(err: Response<'a>) -> Error { 161 | Error::Unexpected(err.into_owned()) 162 | } 163 | } 164 | 165 | impl fmt::Display for Error { 166 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 167 | match *self { 168 | Error::Io(ref e) => fmt::Display::fmt(e, f), 169 | #[cfg(feature = "rustls-tls")] 170 | Error::RustlsHandshake(ref e) => fmt::Display::fmt(e, f), 171 | #[cfg(feature = "native-tls")] 172 | Error::Tls(ref e) => fmt::Display::fmt(e, f), 173 | #[cfg(feature = "native-tls")] 174 | Error::TlsHandshake(ref e) => fmt::Display::fmt(e, f), 175 | Error::Validate(ref e) => fmt::Display::fmt(e, f), 176 | Error::Parse(ref e) => fmt::Display::fmt(e, f), 177 | Error::No(ref data) => write!(f, "No Response: {}", data), 178 | Error::Bad(ref data) => write!(f, "Bad Response: {}", data), 179 | Error::Bye(ref data) => write!(f, "Bye Response: {}", data), 180 | Error::ConnectionLost => f.write_str("Connection Lost"), 181 | Error::Append => f.write_str("Could not append mail to mailbox"), 182 | Error::Unexpected(ref r) => write!(f, "Unexpected Response: {:?}", r), 183 | Error::MissingStatusResponse => write!(f, "Missing STATUS Response"), 184 | Error::TagMismatch(ref data) => write!(f, "Mismatched Tag: {:?}", data), 185 | Error::StartTlsNotAvailable => write!(f, "StartTls is not available on the server"), 186 | Error::TlsNotConfigured => { 187 | write!(f, "TLS was requested, but no TLS features are enabled") 188 | } 189 | } 190 | } 191 | } 192 | 193 | impl StdError for Error { 194 | #[allow(deprecated)] 195 | fn description(&self) -> &str { 196 | match *self { 197 | Error::Io(ref e) => e.description(), 198 | #[cfg(feature = "rustls-tls")] 199 | Error::RustlsHandshake(ref e) => e.description(), 200 | #[cfg(feature = "native-tls")] 201 | Error::Tls(ref e) => e.description(), 202 | #[cfg(feature = "native-tls")] 203 | Error::TlsHandshake(ref e) => e.description(), 204 | Error::Parse(ref e) => e.description(), 205 | Error::Validate(ref e) => e.description(), 206 | Error::Bad(_) => "Bad Response", 207 | Error::No(_) => "No Response", 208 | Error::Bye(_) => "Bye Response", 209 | Error::ConnectionLost => "Connection lost", 210 | Error::Append => "Could not append mail to mailbox", 211 | Error::Unexpected(_) => "Unexpected Response", 212 | Error::MissingStatusResponse => "Missing STATUS Response", 213 | Error::TagMismatch(ref e) => e.description(), 214 | Error::StartTlsNotAvailable => "StartTls is not available on the server", 215 | Error::TlsNotConfigured => "TLS was requested, but no TLS features are enabled", 216 | } 217 | } 218 | 219 | fn cause(&self) -> Option<&dyn StdError> { 220 | match *self { 221 | Error::Io(ref e) => Some(e), 222 | #[cfg(feature = "rustls-tls")] 223 | Error::RustlsHandshake(ref e) => Some(e), 224 | #[cfg(feature = "native-tls")] 225 | Error::Tls(ref e) => Some(e), 226 | #[cfg(feature = "native-tls")] 227 | Error::TlsHandshake(ref e) => Some(e), 228 | Error::Parse(ParseError::DataNotUtf8(_, ref e)) => Some(e), 229 | Error::TagMismatch(ref e) => Some(e), 230 | _ => None, 231 | } 232 | } 233 | } 234 | 235 | /// An error occurred while trying to parse a server response. 236 | #[derive(Debug)] 237 | pub enum ParseError { 238 | /// Indicates an error parsing the status response. Such as OK, NO, and BAD. 239 | Invalid(Vec), 240 | /// The client could not find or decode the server's authentication challenge. 241 | Authentication(String, Option), 242 | /// The client received data that was not UTF-8 encoded. 243 | DataNotUtf8(Vec, Utf8Error), 244 | } 245 | 246 | impl fmt::Display for ParseError { 247 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 248 | match *self { 249 | ParseError::Invalid(_) => f.write_str("Unable to parse status response"), 250 | ParseError::Authentication(_, _) => { 251 | f.write_str("Unable to parse authentication response") 252 | } 253 | ParseError::DataNotUtf8(_, _) => f.write_str("Unable to parse data as UTF-8 text"), 254 | } 255 | } 256 | } 257 | 258 | impl StdError for ParseError { 259 | fn description(&self) -> &str { 260 | match *self { 261 | ParseError::Invalid(_) => "Unable to parse status response", 262 | ParseError::Authentication(_, _) => "Unable to parse authentication response", 263 | ParseError::DataNotUtf8(_, _) => "Unable to parse data as UTF-8 text", 264 | } 265 | } 266 | 267 | fn cause(&self) -> Option<&dyn StdError> { 268 | match *self { 269 | ParseError::Authentication(_, Some(ref e)) => Some(e), 270 | _ => None, 271 | } 272 | } 273 | } 274 | 275 | /// An [invalid character](https://tools.ietf.org/html/rfc3501#section-4.3) was found in a command 276 | /// argument. 277 | #[derive(Debug)] 278 | pub struct ValidateError { 279 | /// the synopsis of the invalid command 280 | pub(crate) command_synopsis: String, 281 | /// the name of the invalid argument 282 | pub(crate) argument: String, 283 | /// the invalid character contained in the argument 284 | pub(crate) offending_char: char, 285 | } 286 | 287 | impl fmt::Display for ValidateError { 288 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 289 | // print character in debug form because invalid ones are often whitespaces 290 | write!( 291 | f, 292 | "Invalid character {:?} in argument '{}' of command '{}'", 293 | self.offending_char, self.argument, self.command_synopsis 294 | ) 295 | } 296 | } 297 | 298 | impl StdError for ValidateError { 299 | fn description(&self) -> &str { 300 | "Invalid character in command argument" 301 | } 302 | 303 | fn cause(&self) -> Option<&dyn StdError> { 304 | None 305 | } 306 | } 307 | 308 | /// The server responded with a different command tag than last one we sent. 309 | #[derive(Debug, Clone, PartialEq, Eq)] 310 | #[non_exhaustive] 311 | pub struct TagMismatch { 312 | /// Expected tag number 313 | pub(crate) expect: u32, 314 | /// Actual tag number, 0 if parse failed 315 | pub(crate) actual: std::result::Result>, 316 | } 317 | 318 | impl fmt::Display for TagMismatch { 319 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 320 | write!( 321 | f, 322 | "Expected tag number is {}, actual {:?}", 323 | self.expect, self.actual 324 | ) 325 | } 326 | } 327 | 328 | impl StdError for TagMismatch {} 329 | 330 | #[cfg(test)] 331 | mod tests { 332 | use super::*; 333 | 334 | #[test] 335 | fn validate_error_display() { 336 | assert_eq!( 337 | ValidateError { 338 | command_synopsis: "COMMAND arg1 arg2".to_owned(), 339 | argument: "arg2".to_string(), 340 | offending_char: '\n' 341 | } 342 | .to_string(), 343 | "Invalid character '\\n' in argument 'arg2' of command 'COMMAND arg1 arg2'" 344 | ); 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/extensions/idle.rs: -------------------------------------------------------------------------------- 1 | //! Adds support for the IMAP IDLE command specified in [RFC 2 | //! 2177](https://tools.ietf.org/html/rfc2177). 3 | 4 | use crate::client::Session; 5 | use crate::error::{Error, Result}; 6 | use crate::parse::parse_idle; 7 | use crate::types::UnsolicitedResponse; 8 | use crate::Connection; 9 | #[cfg(feature = "native-tls")] 10 | use native_tls::TlsStream; 11 | #[cfg(feature = "rustls-tls")] 12 | use rustls_connector::TlsStream as RustlsStream; 13 | use std::io::{self, Read, Write}; 14 | use std::net::TcpStream; 15 | use std::ops::DerefMut; 16 | use std::time::Duration; 17 | 18 | /// `Handle` allows a client to block waiting for changes to the remote mailbox. 19 | /// 20 | /// The handle blocks using the [`IDLE` command](https://tools.ietf.org/html/rfc2177#section-3) 21 | /// specified in [RFC 2177](https://tools.ietf.org/html/rfc2177) until the underlying server state 22 | /// changes in some way. 23 | /// 24 | /// The `wait_while` function takes a callback function which receives any responses 25 | /// that arrive on the channel while IDLE. The callback function implements whatever 26 | /// logic is needed to handle the IDLE response, and then returns a boolean 27 | /// to continue idling (`true`) or stop (`false`). 28 | /// 29 | /// For users that want the IDLE to exit on any change (the behavior proior to version 3.0), 30 | /// a convenience callback function [`stop_on_any`] is provided. 31 | /// 32 | /// ```no_run 33 | /// use imap::extensions::idle; 34 | /// # #[cfg(feature = "native-tls")] 35 | /// # { 36 | /// let client = imap::ClientBuilder::new("example.com", 993).connect() 37 | /// .expect("Could not connect to imap server"); 38 | /// let mut imap = client.login("user@example.com", "password") 39 | /// .expect("Could not authenticate"); 40 | /// imap.select("INBOX") 41 | /// .expect("Could not select mailbox"); 42 | /// 43 | /// // Exit on any mailbox change. By default, connections will be periodically 44 | /// // refreshed in the background. 45 | /// let result = imap.idle().wait_while(idle::stop_on_any); 46 | /// # } 47 | /// ``` 48 | /// 49 | /// Note that the server MAY consider a client inactive if it has an IDLE command running, and if 50 | /// such a server has an inactivity timeout it MAY log the client off implicitly at the end of its 51 | /// timeout period. Because of that, clients using IDLE are advised to terminate the IDLE and 52 | /// re-issue it at least every 29 minutes to avoid being logged off. This is done by default, but 53 | /// can be disabled by calling [`Handle::keepalive`] 54 | /// 55 | /// As long as a [`Handle`] is active, the mailbox cannot be otherwise accessed. 56 | #[derive(Debug)] 57 | pub struct Handle<'a, T: Read + Write> { 58 | session: &'a mut Session, 59 | timeout: Duration, 60 | keepalive: bool, 61 | done: bool, 62 | } 63 | 64 | /// The result of a wait on a [`Handle`] 65 | #[derive(Debug, PartialEq, Eq)] 66 | pub enum WaitOutcome { 67 | /// The wait timed out 68 | TimedOut, 69 | /// The mailbox was modified 70 | MailboxChanged, 71 | } 72 | 73 | /// A convenience function to always cause the IDLE handler to exit on any change. 74 | pub fn stop_on_any(_response: UnsolicitedResponse) -> bool { 75 | false 76 | } 77 | 78 | /// Must be implemented for a transport in order for a `Session` to use IDLE. 79 | pub trait SetReadTimeout { 80 | /// Set the timeout for subsequent reads to the given one. 81 | /// 82 | /// If `timeout` is `None`, the read timeout should be removed. 83 | /// 84 | /// See also `std::net::TcpStream::set_read_timeout`. 85 | fn set_read_timeout(&mut self, timeout: Option) -> Result<()>; 86 | } 87 | 88 | impl<'a, T: Read + Write + 'a> Handle<'a, T> { 89 | pub(crate) fn make(session: &'a mut Session) -> Self { 90 | Handle { 91 | session, 92 | timeout: Duration::from_secs(29 * 60), 93 | keepalive: true, 94 | done: false, 95 | } 96 | } 97 | 98 | fn init(&mut self) -> Result<()> { 99 | // https://tools.ietf.org/html/rfc2177 100 | // 101 | // The IDLE command takes no arguments. 102 | self.session.run_command("IDLE")?; 103 | 104 | // A tagged response will be sent either 105 | // 106 | // a) if there's an error, or 107 | // b) *after* we send DONE 108 | let mut v = Vec::new(); 109 | self.session.readline(&mut v)?; 110 | if v.starts_with(b"+") { 111 | self.done = false; 112 | return Ok(()); 113 | } 114 | 115 | self.session.read_response_onto(&mut v)?; 116 | // We should *only* get a continuation on an error (i.e., it gives BAD or NO). 117 | unreachable!(); 118 | } 119 | 120 | fn terminate(&mut self) -> Result<()> { 121 | if !self.done { 122 | self.done = true; 123 | self.session.write_line(b"DONE")?; 124 | self.session.read_response().map(|_| ()) 125 | } else { 126 | Ok(()) 127 | } 128 | } 129 | 130 | /// Internal helper that doesn't consume self. 131 | /// 132 | /// This is necessary so that we can keep using the inner `Session` in `wait_while`. 133 | fn wait_inner(&mut self, reconnect: bool, mut callback: F) -> Result 134 | where 135 | F: FnMut(UnsolicitedResponse) -> bool, 136 | { 137 | let mut v = Vec::new(); 138 | let result = loop { 139 | match self.session.readline(&mut v) { 140 | Err(Error::Io(ref e)) 141 | if e.kind() == io::ErrorKind::TimedOut 142 | || e.kind() == io::ErrorKind::WouldBlock => 143 | { 144 | break Ok(WaitOutcome::TimedOut); 145 | } 146 | Ok(_len) => { 147 | // Handle Dovecot's imap_idle_notify_interval message 148 | if v.eq_ignore_ascii_case(b"* OK Still here\r\n") { 149 | v.clear(); 150 | continue; 151 | } 152 | match parse_idle(&v) { 153 | // Something went wrong parsing. 154 | (_rest, Some(Err(r))) => break Err(r), 155 | // Complete response. We expect rest to be empty. 156 | (rest, Some(Ok(response))) => { 157 | if !callback(response) { 158 | break Ok(WaitOutcome::MailboxChanged); 159 | } 160 | 161 | // Assert on partial parse in debug builds - we expect 162 | // to always parse all or none of the input buffer. 163 | // On release builds, we still do the right thing. 164 | debug_assert!( 165 | rest.is_empty(), 166 | "Unexpected partial parse: input: {:?}, output: {:?}", 167 | v, 168 | rest, 169 | ); 170 | 171 | if rest.is_empty() { 172 | v.clear(); 173 | } else { 174 | let used = v.len() - rest.len(); 175 | v.drain(0..used); 176 | } 177 | } 178 | // Incomplete parse - do nothing and read more. 179 | (_rest, None) => {} 180 | } 181 | } 182 | Err(r) => break Err(r), 183 | }; 184 | }; 185 | 186 | // Reconnect on timeout if needed 187 | match (reconnect, result) { 188 | (true, Ok(WaitOutcome::TimedOut)) => { 189 | self.terminate()?; 190 | self.init()?; 191 | self.wait_inner(reconnect, callback) 192 | } 193 | (_, result) => result, 194 | } 195 | } 196 | } 197 | 198 | impl<'a, T: SetReadTimeout + Read + Write + 'a> Handle<'a, T> { 199 | /// Set the timeout duration on the connection. This will also set the frequency 200 | /// at which the connection is refreshed. 201 | /// 202 | /// The interval defaults to 29 minutes as given in RFC 2177. 203 | pub fn timeout(&mut self, interval: Duration) -> &mut Self { 204 | self.timeout = interval; 205 | self 206 | } 207 | 208 | /// Do not continuously refresh the IDLE connection in the background. 209 | /// 210 | /// By default, connections will periodically be refreshed in the background using the 211 | /// timeout duration set by [`Handle::timeout`]. If you do not want this behaviour, call 212 | /// this function and the connection will simply IDLE until `wait_while` returns or 213 | /// the timeout expires. 214 | pub fn keepalive(&mut self, keepalive: bool) -> &mut Self { 215 | self.keepalive = keepalive; 216 | self 217 | } 218 | 219 | /// Block until the given callback returns `false`, or until a response 220 | /// arrives that is not explicitly handled by [`UnsolicitedResponse`]. 221 | pub fn wait_while(&mut self, callback: F) -> Result 222 | where 223 | F: FnMut(UnsolicitedResponse) -> bool, 224 | { 225 | self.init()?; 226 | // The server MAY consider a client inactive if it has an IDLE command 227 | // running, and if such a server has an inactivity timeout it MAY log 228 | // the client off implicitly at the end of its timeout period. Because 229 | // of that, clients using IDLE are advised to terminate the IDLE and 230 | // re-issue it at least every 29 minutes to avoid being logged off. 231 | // This still allows a client to receive immediate mailbox updates even 232 | // though it need only "poll" at half hour intervals. 233 | self.session 234 | .stream 235 | .get_mut() 236 | .set_read_timeout(Some(self.timeout))?; 237 | let res = self.wait_inner(self.keepalive, callback); 238 | let _ = self.session.stream.get_mut().set_read_timeout(None).is_ok(); 239 | res 240 | } 241 | } 242 | 243 | impl<'a, T: Read + Write + 'a> Drop for Handle<'a, T> { 244 | fn drop(&mut self) { 245 | // we don't want to panic here if we can't terminate the Idle 246 | let _ = self.terminate().is_ok(); 247 | } 248 | } 249 | 250 | impl SetReadTimeout for Connection { 251 | fn set_read_timeout(&mut self, timeout: Option) -> Result<()> { 252 | self.deref_mut().set_read_timeout(timeout) 253 | } 254 | } 255 | 256 | impl SetReadTimeout for TcpStream { 257 | fn set_read_timeout(&mut self, timeout: Option) -> Result<()> { 258 | TcpStream::set_read_timeout(self, timeout).map_err(Error::Io) 259 | } 260 | } 261 | 262 | #[cfg(feature = "native-tls")] 263 | impl SetReadTimeout for TlsStream { 264 | fn set_read_timeout(&mut self, timeout: Option) -> Result<()> { 265 | self.get_ref().set_read_timeout(timeout).map_err(Error::Io) 266 | } 267 | } 268 | 269 | #[cfg(feature = "rustls-tls")] 270 | impl SetReadTimeout for RustlsStream { 271 | fn set_read_timeout(&mut self, timeout: Option) -> Result<()> { 272 | self.get_ref().set_read_timeout(timeout).map_err(Error::Io) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/extensions/list_status.rs: -------------------------------------------------------------------------------- 1 | //! Adds support for the IMAP LIST-STATUS extension specified in [RFC 2 | //! 5819](https://tools.ietf.org/html/rfc5819). 3 | 4 | use crate::client::{validate_str, Session}; 5 | use crate::error::{Error, ParseError, Result}; 6 | use crate::parse::try_handle_unilateral; 7 | use crate::types::{Mailbox, Name, UnsolicitedResponse}; 8 | use imap_proto::types::{MailboxDatum, Response, StatusAttribute}; 9 | use ouroboros::self_referencing; 10 | use std::collections::VecDeque; 11 | use std::io::{Read, Write}; 12 | use std::slice::Iter; 13 | 14 | /// A wrapper for one or more [`Name`] responses paired with optional [`Mailbox`] responses. 15 | /// 16 | /// This structure represents responses to a LIST-STATUS command, as implemented in 17 | /// [`Session::list_status`]. See [RFC 5819, section 2](https://tools.ietf.org/html/rfc5819.html#section-2). 18 | #[self_referencing] 19 | pub struct ExtendedNames { 20 | data: Vec, 21 | #[borrows(data)] 22 | #[covariant] 23 | pub(crate) extended_names: Vec<(Name<'this>, Option)>, 24 | } 25 | 26 | impl ExtendedNames { 27 | /// Parse one or more LIST-STATUS responses from a response buffer 28 | pub(crate) fn parse( 29 | owned: Vec, 30 | unsolicited: &mut VecDeque, 31 | ) -> core::result::Result { 32 | ExtendedNamesTryBuilder { 33 | data: owned, 34 | extended_names_builder: |input| { 35 | let mut lines: &[u8] = input; 36 | let mut names = Vec::new(); 37 | let mut current_name: Option> = None; 38 | let mut current_mailbox: Option = None; 39 | 40 | loop { 41 | if lines.is_empty() { 42 | if let Some(cur_name) = current_name { 43 | names.push((cur_name, current_mailbox)); 44 | } 45 | break; 46 | } 47 | 48 | match imap_proto::parser::parse_response(lines) { 49 | Ok(( 50 | rest, 51 | Response::MailboxData(MailboxDatum::List { 52 | name_attributes, 53 | delimiter, 54 | name, 55 | }), 56 | )) => { 57 | lines = rest; 58 | if let Some(cur_name) = current_name { 59 | names.push((cur_name, current_mailbox)); 60 | current_mailbox = None; 61 | } 62 | current_name = Some(Name { 63 | attributes: name_attributes, 64 | delimiter, 65 | name, 66 | }); 67 | } 68 | Ok(( 69 | rest, 70 | Response::MailboxData(MailboxDatum::Status { mailbox: _, status }), 71 | )) => { 72 | lines = rest; 73 | let mut mb = Mailbox::default(); 74 | for attr in status { 75 | match attr { 76 | StatusAttribute::HighestModSeq(v) => { 77 | mb.highest_mod_seq = Some(v) 78 | } 79 | StatusAttribute::Messages(v) => mb.exists = v, 80 | StatusAttribute::Recent(v) => mb.recent = v, 81 | StatusAttribute::UidNext(v) => mb.uid_next = Some(v), 82 | StatusAttribute::UidValidity(v) => mb.uid_validity = Some(v), 83 | StatusAttribute::Unseen(v) => mb.unseen = Some(v), 84 | _ => {} // needed because StatusAttribute is #[non_exhaustive] 85 | } 86 | } 87 | current_mailbox = Some(mb); 88 | } 89 | Ok((rest, resp)) => { 90 | lines = rest; 91 | if let Some(unhandled) = try_handle_unilateral(resp, unsolicited) { 92 | return Err(unhandled.into()); 93 | } 94 | } 95 | Err(_) => { 96 | return Err(Error::Parse(ParseError::Invalid(lines.to_vec()))); 97 | } 98 | } 99 | } 100 | 101 | Ok(names) 102 | }, 103 | } 104 | .try_build() 105 | } 106 | 107 | /// Iterate over the contained elements 108 | pub fn iter(&self) -> Iter<'_, (Name<'_>, Option)> { 109 | self.borrow_extended_names().iter() 110 | } 111 | 112 | /// Get the number of elements in this container. 113 | pub fn len(&self) -> usize { 114 | self.borrow_extended_names().len() 115 | } 116 | 117 | /// Return true of there are no elements in the container. 118 | pub fn is_empty(&self) -> bool { 119 | self.borrow_extended_names().is_empty() 120 | } 121 | 122 | /// Get the element at the given index 123 | pub fn get(&self, index: usize) -> Option<&(Name<'_>, Option)> { 124 | self.borrow_extended_names().get(index) 125 | } 126 | } 127 | 128 | impl Session { 129 | /// The [extended `LIST` command](https://tools.ietf.org/html/rfc5819.html#section-2) returns 130 | /// a subset of names from the complete set of all names available to the client. Each name 131 | /// _should_ be paired with a STATUS response, though the server _may_ drop it if it encounters 132 | /// problems looking up the required information. 133 | /// 134 | /// This version of the command is also often referred to as `LIST-STATUS` command, as that is 135 | /// the name of the extension and it is a combination of the two. 136 | /// 137 | /// The `reference_name` and `mailbox_pattern` arguments have the same semantics as they do in 138 | /// [`Session::list`]. 139 | /// 140 | /// The `data_items` argument has the same semantics as it does in [`Session::status`]. 141 | pub fn list_status( 142 | &mut self, 143 | reference_name: Option<&str>, 144 | mailbox_pattern: Option<&str>, 145 | data_items: &str, 146 | ) -> Result { 147 | let reference = validate_str("LIST-STATUS", "reference", reference_name.unwrap_or(""))?; 148 | let lines = self.run_command_and_read_response(format!( 149 | "LIST {} {} RETURN (STATUS {})", 150 | &reference, 151 | mailbox_pattern.unwrap_or("\"\""), 152 | data_items 153 | ))?; 154 | ExtendedNames::parse(lines, &mut self.unsolicited_responses) 155 | } 156 | } 157 | 158 | #[cfg(test)] 159 | mod tests { 160 | use imap_proto::NameAttribute; 161 | 162 | use super::*; 163 | 164 | #[test] 165 | fn parse_list_status_test() { 166 | let lines = b"\ 167 | * LIST () \".\" foo\r\n\ 168 | * STATUS foo (HIGHESTMODSEQ 122)\r\n\ 169 | * LIST () \".\" foo.bar\r\n\ 170 | * STATUS foo.bar (HIGHESTMODSEQ 132)\r\n\ 171 | * LIST (\\UnMarked) \".\" feeds\r\n\ 172 | * LIST () \".\" feeds.test\r\n\ 173 | * STATUS feeds.test (HIGHESTMODSEQ 757)\r\n"; 174 | let mut queue = VecDeque::new(); 175 | let fetches = ExtendedNames::parse(lines.to_vec(), &mut queue).unwrap(); 176 | assert_eq!(queue.pop_front(), None); 177 | assert!(!fetches.is_empty()); 178 | assert_eq!(fetches.len(), 4); 179 | let (name, status) = fetches.get(0).unwrap(); 180 | assert_eq!(&name.name, "foo"); 181 | assert!(status.is_some()); 182 | assert_eq!(status.as_ref().unwrap().highest_mod_seq, Some(122)); 183 | let (name, status) = fetches.get(1).unwrap(); 184 | assert_eq!(&name.name, "foo.bar"); 185 | assert!(status.is_some()); 186 | assert_eq!(status.as_ref().unwrap().highest_mod_seq, Some(132)); 187 | let (name, status) = fetches.get(2).unwrap(); 188 | assert_eq!(&name.name, "feeds"); 189 | assert_eq!(name.attributes.len(), 1); 190 | assert_eq!(name.attributes.get(0).unwrap(), &NameAttribute::Unmarked); 191 | assert!(status.is_none()); 192 | let (name, status) = fetches.get(3).unwrap(); 193 | assert_eq!(&name.name, "feeds.test"); 194 | assert!(status.is_some()); 195 | assert_eq!(status.as_ref().unwrap().highest_mod_seq, Some(757)); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/extensions/mod.rs: -------------------------------------------------------------------------------- 1 | //! Implementations of various IMAP extensions. 2 | pub mod idle; 3 | pub mod list_status; 4 | pub mod metadata; 5 | pub mod sort; 6 | -------------------------------------------------------------------------------- /src/extensions/sort.rs: -------------------------------------------------------------------------------- 1 | //! Adds support for the IMAP SORT extension specified in [RFC 2 | //! 5464](https://tools.ietf.org/html/rfc5256#section-3). 3 | //! 4 | //! The SORT command is a variant of SEARCH with sorting semantics for 5 | //! the results. There are two arguments before the searching 6 | //! criteria argument: a parenthesized list of sort criteria, and the 7 | //! searching charset. 8 | 9 | use std::{borrow::Cow, fmt}; 10 | 11 | pub(crate) struct SortCriteria<'c>(pub(crate) &'c [SortCriterion<'c>]); 12 | 13 | impl<'c> fmt::Display for SortCriteria<'c> { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | if self.0.is_empty() { 16 | write!(f, "") 17 | } else { 18 | let criteria: Vec = self.0.iter().map(|c| c.to_string()).collect(); 19 | write!(f, "({})", criteria.join(" ")) 20 | } 21 | } 22 | } 23 | 24 | /// Message sorting preferences used for [`Session::sort`](crate::Session::sort) 25 | /// and [`Session::uid_sort`](crate::Session::uid_sort). 26 | /// 27 | /// Any sorting criterion that refers to an address (`From`, `To`, etc.) sorts according to the 28 | /// "addr-mailbox" of the indicated address. You can find the formal syntax for addr-mailbox [in 29 | /// the IMAP spec](https://tools.ietf.org/html/rfc3501#section-9), and a more detailed discussion 30 | /// of the relevant semantics [in RFC 2822](https://tools.ietf.org/html/rfc2822#section-3.4.1). 31 | /// Essentially, the address refers _either_ to the name of the contact _or_ to its local-part (the 32 | /// left part of the email address, before the `@`). 33 | #[non_exhaustive] 34 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)] 35 | pub enum SortCriterion<'c> { 36 | /// Internal date and time of the message. This differs from the 37 | /// ON criteria in SEARCH, which uses just the internal date. 38 | Arrival, 39 | 40 | /// IMAP addr-mailbox of the first "Cc" address. 41 | Cc, 42 | 43 | /// Sent date and time, as described in 44 | /// [section 2.2](https://tools.ietf.org/html/rfc5256#section-2.2). 45 | Date, 46 | 47 | /// IMAP addr-mailbox of the first "From" address. 48 | From, 49 | 50 | /// Followed by another sort criterion, has the effect of that 51 | /// criterion but in reverse (descending) order. 52 | Reverse(&'c SortCriterion<'c>), 53 | 54 | /// Size of the message in octets. 55 | Size, 56 | 57 | /// Base subject text. 58 | Subject, 59 | 60 | /// IMAP addr-mailbox of the first "To" address. 61 | To, 62 | } 63 | 64 | impl<'c> fmt::Display for SortCriterion<'c> { 65 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 66 | use SortCriterion::*; 67 | 68 | match self { 69 | Arrival => write!(f, "ARRIVAL"), 70 | Cc => write!(f, "CC"), 71 | Date => write!(f, "DATE"), 72 | From => write!(f, "FROM"), 73 | Reverse(c) => write!(f, "REVERSE {}", c), 74 | Size => write!(f, "SIZE"), 75 | Subject => write!(f, "SUBJECT"), 76 | To => write!(f, "TO"), 77 | } 78 | } 79 | } 80 | 81 | /// The character encoding to use for strings that are subject to a [`SortCriterion`]. 82 | /// 83 | /// Servers are only required to implement [`SortCharset::UsAscii`] and [`SortCharset::Utf8`]. 84 | #[non_exhaustive] 85 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 86 | pub enum SortCharset<'c> { 87 | /// Strings are UTF-8 encoded. 88 | Utf8, 89 | 90 | /// Strings are encoded with ASCII. 91 | UsAscii, 92 | 93 | /// Strings are encoded using some other character set. 94 | /// 95 | /// Note that this option is subject to server support for the specified character set. 96 | Custom(Cow<'c, str>), 97 | } 98 | 99 | impl<'c> fmt::Display for SortCharset<'c> { 100 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 101 | use SortCharset::*; 102 | 103 | match self { 104 | Utf8 => write!(f, "UTF-8"), 105 | UsAscii => write!(f, "US-ASCII"), 106 | Custom(c) => write!(f, "{}", c), 107 | } 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use super::*; 114 | 115 | #[test] 116 | fn test_criterion_to_string() { 117 | use SortCriterion::*; 118 | 119 | assert_eq!("ARRIVAL", Arrival.to_string()); 120 | assert_eq!("CC", Cc.to_string()); 121 | assert_eq!("DATE", Date.to_string()); 122 | assert_eq!("FROM", From.to_string()); 123 | assert_eq!("SIZE", Size.to_string()); 124 | assert_eq!("SUBJECT", Subject.to_string()); 125 | assert_eq!("TO", To.to_string()); 126 | assert_eq!("REVERSE TO", Reverse(&To).to_string()); 127 | assert_eq!("REVERSE REVERSE TO", Reverse(&Reverse(&To)).to_string()); 128 | } 129 | 130 | #[test] 131 | fn test_criteria_to_string() { 132 | use SortCriterion::*; 133 | 134 | assert_eq!("", SortCriteria(&[]).to_string()); 135 | assert_eq!("(ARRIVAL)", SortCriteria(&[Arrival]).to_string()); 136 | assert_eq!( 137 | "(ARRIVAL REVERSE FROM)", 138 | SortCriteria(&[Arrival, Reverse(&From)]).to_string() 139 | ); 140 | assert_eq!( 141 | "(ARRIVAL REVERSE REVERSE REVERSE FROM)", 142 | SortCriteria(&[Arrival, Reverse(&Reverse(&Reverse(&From)))]).to_string() 143 | ); 144 | } 145 | 146 | #[test] 147 | fn test_charset_to_string() { 148 | use SortCharset::*; 149 | 150 | assert_eq!("UTF-8", Utf8.to_string()); 151 | assert_eq!("US-ASCII", UsAscii.to_string()); 152 | assert_eq!("CHARSET", Custom("CHARSET".into()).to_string()); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate lets you connect to and interact with servers that implement the IMAP protocol ([RFC 2 | //! 3501](https://tools.ietf.org/html/rfc3501) and various extensions). After authenticating with 3 | //! the server, IMAP lets you list, fetch, and search for e-mails, as well as monitor mailboxes for 4 | //! changes. It supports at least the latest three stable Rust releases (possibly even older ones; 5 | //! check the [CI 6 | //! results](https://dev.azure.com/jonhoo/jonhoo/_build/latest?definitionId=11&branchName=master)). 7 | //! 8 | //! **This crate is looking for maintainers — reach out to [@jonhoo] if you're interested.** 9 | //! 10 | //! [@jonhoo]: https://thesquareplanet.com/ 11 | //! 12 | //! To connect, use the [`ClientBuilder`]. This gives you an unauthenticated [`Client`]. You can 13 | //! then use [`Client::login`] or [`Client::authenticate`] to perform username/password or 14 | //! challenge/response authentication respectively. This in turn gives you an authenticated 15 | //! [`Session`], which lets you access the mailboxes at the server. 16 | //! 17 | //! The documentation within this crate borrows heavily from the various RFCs, but should not be 18 | //! considered a complete reference. If anything is unclear, follow the links to the RFCs embedded 19 | //! in the documentation for the various types and methods and read the raw text there! 20 | //! 21 | //! Below is a basic client example. See the `examples/` directory for more. 22 | //! 23 | //! ```no_run 24 | //! # #[cfg(feature = "native-tls")] 25 | //! fn fetch_inbox_top() -> imap::error::Result> { 26 | //! 27 | //! let client = imap::ClientBuilder::new("imap.example.com", 993).connect()?; 28 | //! 29 | //! // the client we have here is unauthenticated. 30 | //! // to do anything useful with the e-mails, we need to log in 31 | //! let mut imap_session = client 32 | //! .login("me@example.com", "password") 33 | //! .map_err(|e| e.0)?; 34 | //! 35 | //! // we want to fetch the first email in the INBOX mailbox 36 | //! imap_session.select("INBOX")?; 37 | //! 38 | //! // fetch message number 1 in this mailbox, along with its RFC822 field. 39 | //! // RFC 822 dictates the format of the body of e-mails 40 | //! let messages = imap_session.fetch("1", "RFC822")?; 41 | //! let message = if let Some(m) = messages.iter().next() { 42 | //! m 43 | //! } else { 44 | //! return Ok(None); 45 | //! }; 46 | //! 47 | //! // extract the message's body 48 | //! let body = message.body().expect("message did not have a body!"); 49 | //! let body = std::str::from_utf8(body) 50 | //! .expect("message was not valid utf-8") 51 | //! .to_string(); 52 | //! 53 | //! // be nice to the server and log out 54 | //! imap_session.logout()?; 55 | //! 56 | //! Ok(Some(body)) 57 | //! } 58 | //! ``` 59 | //! 60 | //! ## Opting out of `native_tls` 61 | //! 62 | //! For situations where using openssl becomes problematic, you can disable the 63 | //! default feature which provides integration with the `native_tls` crate. One major 64 | //! reason you might want to do this is cross-compiling. To opt out of native_tls, add 65 | //! this to your Cargo.toml file: 66 | //! 67 | //! ```toml 68 | //! [dependencies.imap] 69 | //! version = "" 70 | //! default-features = false 71 | //! ``` 72 | //! 73 | //! Even without `native_tls`, you can still use TLS by leveraging the pure Rust `rustls` 74 | //! crate, which is enabled with the `rustls-tls` feature. See the example/rustls.rs file 75 | //! for a working example. 76 | #![deny(missing_docs)] 77 | #![warn(rust_2018_idioms)] 78 | #![cfg_attr(docsrs, feature(doc_cfg))] 79 | 80 | mod parse; 81 | mod utils; 82 | 83 | pub mod types; 84 | 85 | mod authenticator; 86 | pub use crate::authenticator::Authenticator; 87 | 88 | mod conn; 89 | pub use conn::{Connection, ImapConnection}; 90 | 91 | mod client; 92 | pub use crate::client::*; 93 | mod client_builder; 94 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 95 | pub use crate::client_builder::TlsKind; 96 | pub use crate::client_builder::{ClientBuilder, ConnectionMode}; 97 | 98 | pub mod error; 99 | pub use error::{Error, Result}; 100 | 101 | pub mod extensions; 102 | 103 | #[cfg(feature = "test_helpers")] 104 | pub mod testing; 105 | 106 | #[cfg(test)] 107 | mod mock_stream; 108 | -------------------------------------------------------------------------------- /src/mock_stream.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | use std::io::{Error, ErrorKind, Read, Result, Write}; 3 | 4 | #[derive(Clone, Debug, Eq, PartialEq, Hash)] 5 | pub struct MockStream { 6 | read_buf: Vec, 7 | read_pos: usize, 8 | pub written_buf: Vec, 9 | err_on_read: bool, 10 | eof_on_read: bool, 11 | read_delay: usize, 12 | } 13 | 14 | impl Default for MockStream { 15 | fn default() -> Self { 16 | MockStream { 17 | read_buf: Vec::new(), 18 | read_pos: 0, 19 | written_buf: Vec::new(), 20 | err_on_read: false, 21 | eof_on_read: false, 22 | read_delay: 0, 23 | } 24 | } 25 | } 26 | 27 | impl MockStream { 28 | pub fn new(read_buf: Vec) -> MockStream { 29 | MockStream::default().with_buf(read_buf) 30 | } 31 | 32 | pub fn with_buf(mut self, read_buf: Vec) -> MockStream { 33 | self.read_buf = read_buf; 34 | self 35 | } 36 | 37 | pub fn with_eof(mut self) -> MockStream { 38 | self.eof_on_read = true; 39 | self 40 | } 41 | 42 | pub fn with_err(mut self) -> MockStream { 43 | self.err_on_read = true; 44 | self 45 | } 46 | 47 | pub fn with_delay(mut self) -> MockStream { 48 | self.read_delay = 1; 49 | self 50 | } 51 | } 52 | 53 | impl Read for MockStream { 54 | fn read(&mut self, buf: &mut [u8]) -> Result { 55 | if self.eof_on_read { 56 | return Ok(0); 57 | } 58 | if self.err_on_read { 59 | return Err(Error::new(ErrorKind::Other, "MockStream Error")); 60 | } 61 | if self.read_pos >= self.read_buf.len() { 62 | return Err(Error::new(ErrorKind::UnexpectedEof, "EOF")); 63 | } 64 | let mut write_len = min(buf.len(), self.read_buf.len() - self.read_pos); 65 | if self.read_delay > 0 { 66 | self.read_delay -= 1; 67 | write_len = min(write_len, 1); 68 | } 69 | let max_pos = self.read_pos + write_len; 70 | for x in self.read_pos..max_pos { 71 | buf[x - self.read_pos] = self.read_buf[x]; 72 | } 73 | self.read_pos += write_len; 74 | Ok(write_len) 75 | } 76 | } 77 | 78 | impl Write for MockStream { 79 | fn write(&mut self, buf: &[u8]) -> Result { 80 | self.written_buf.extend_from_slice(buf); 81 | Ok(buf.len()) 82 | } 83 | 84 | fn flush(&mut self) -> Result<()> { 85 | Ok(()) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/testing.rs: -------------------------------------------------------------------------------- 1 | //! Enable the test_helpers feature to expose helper methods to build 2 | //! mock response structures for testing your code that uses the imap crate 3 | //! 4 | //! To use add a dev-dependency on the imap extension adding the feature "test_helpers" 5 | //! e.g. 6 | //! 7 | //! ```toml 8 | //! [dependencies] 9 | //! imap = { version = "3.0" } 10 | //! 11 | //! [dev-dependencies] 12 | //! # mirror the same configuration your dependencies and add test_helpers 13 | //! imap = { version = "3.0", features = ["test_helpers"] } 14 | //! ``` 15 | //! 16 | #[cfg(doc)] 17 | use crate::{extensions::list_status::ExtendedNames, types::*}; 18 | 19 | /// Methods to build a [`Capabilities`] response object 20 | pub mod capabilities { 21 | use crate::types::Capabilities; 22 | use std::collections::VecDeque; 23 | 24 | /// Builds an [`Capabilities`] based on the provided input 25 | /// 26 | /// Example input. 27 | /// 28 | /// ``` 29 | /// let input = "* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"; 30 | /// let response = imap::testing::capabilities::parse(input); 31 | /// ``` 32 | pub fn parse(input: impl Into>) -> Capabilities { 33 | let mut unsolicited_responses = VecDeque::new(); 34 | 35 | Capabilities::parse(input.into(), &mut unsolicited_responses).unwrap() 36 | } 37 | } 38 | 39 | /// Methods to build a [`Fetches`] response object 40 | pub mod fetches { 41 | use crate::types::Fetches; 42 | use std::collections::VecDeque; 43 | 44 | /// Builds an [`Fetches`] based on the provided input 45 | /// 46 | /// Example input. 47 | /// 48 | /// ``` 49 | /// let input = "\ 50 | /// * 24 FETCH (FLAGS (\\Seen) UID 4827943)\r\n\ 51 | /// * 25 FETCH (FLAGS (\\Seen))\r\n\ 52 | /// "; 53 | /// let response = imap::testing::fetches::parse(input); 54 | /// ``` 55 | pub fn parse(input: impl Into>) -> Fetches { 56 | let mut unsolicited_responses = VecDeque::new(); 57 | 58 | Fetches::parse(input.into(), &mut unsolicited_responses).unwrap() 59 | } 60 | } 61 | 62 | /// Methods to build a [`Names`] response object 63 | pub mod names { 64 | use crate::types::Names; 65 | use std::collections::VecDeque; 66 | 67 | /// Builds an [`Names`] based on the provided input 68 | /// Example input. 69 | /// 70 | /// ``` 71 | /// let input = "\ 72 | /// * LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n\ 73 | /// "; 74 | /// let response = imap::testing::names::parse(input); 75 | ///``` 76 | pub fn parse(input: impl Into>) -> Names { 77 | let mut unsolicited_responses = VecDeque::new(); 78 | 79 | Names::parse(input.into(), &mut unsolicited_responses).unwrap() 80 | } 81 | } 82 | 83 | /// Methods to build a [`ExtendedNames`] response object 84 | pub mod extended_names { 85 | use crate::extensions::list_status::ExtendedNames; 86 | use std::collections::VecDeque; 87 | 88 | /// Builds an [`ExtendedNames`] based on the provided input 89 | /// 90 | /// Example input. 91 | /// 92 | /// ``` 93 | /// let input = "\ 94 | /// * LIST () \".\" foo\r\n\ 95 | /// * STATUS foo (HIGHESTMODSEQ 122)\r\n\ 96 | /// * LIST () \".\" foo.bar\r\n\ 97 | /// * STATUS foo.bar (HIGHESTMODSEQ 132)\r\n\ 98 | /// * LIST (\\UnMarked) \".\" feeds\r\n\ 99 | /// * LIST () \".\" feeds.test\r\n\ 100 | /// * STATUS feeds.test (HIGHESTMODSEQ 757)\r\n\ 101 | /// "; 102 | /// let response = imap::testing::extended_names::parse(input); 103 | /// ``` 104 | pub fn parse(input: impl Into>) -> ExtendedNames { 105 | let mut unsolicited_responses = VecDeque::new(); 106 | 107 | ExtendedNames::parse(input.into(), &mut unsolicited_responses).unwrap() 108 | } 109 | } 110 | 111 | /// Methods to build a [`AclResponse`] response object 112 | pub mod acl_response { 113 | use crate::types::AclResponse; 114 | use std::collections::VecDeque; 115 | 116 | /// Builds an [`AclResponse`] based on the provided input 117 | /// 118 | /// Example input. 119 | /// 120 | /// ``` 121 | /// let input = "* ACL INBOX user1 lr user2 lrx\r\n"; 122 | /// let response = imap::testing::acl_response::parse(input); 123 | /// ``` 124 | pub fn parse(input: impl Into>) -> AclResponse { 125 | let mut unsolicited_responses = VecDeque::new(); 126 | 127 | AclResponse::parse(input.into(), &mut unsolicited_responses).unwrap() 128 | } 129 | } 130 | 131 | /// Methods to build a [`ListRightsResponse`] response object 132 | pub mod list_rights_response { 133 | use crate::types::ListRightsResponse; 134 | use std::collections::VecDeque; 135 | 136 | /// Builds an [`ListRightsResponse`] based on the provided input 137 | /// 138 | /// Example input. 139 | /// 140 | /// ``` 141 | /// let input = "* LISTRIGHTS INBOX myuser lr x k\r\n"; 142 | /// let response = imap::testing::list_rights_response::parse(input); 143 | ///``` 144 | pub fn parse(input: impl Into>) -> ListRightsResponse { 145 | let mut unsolicited_responses = VecDeque::new(); 146 | 147 | ListRightsResponse::parse(input.into(), &mut unsolicited_responses).unwrap() 148 | } 149 | } 150 | 151 | /// Methods to build a [`MyRightsResponse`] response object 152 | pub mod my_rights_response { 153 | use crate::types::MyRightsResponse; 154 | use std::collections::VecDeque; 155 | 156 | /// Builds an [`MyRightsResponse`] based on the provided input 157 | /// 158 | /// Example input. 159 | /// 160 | /// ``` 161 | /// let input = "* MYRIGHTS INBOX lrxk\r\n"; 162 | /// let response = imap::testing::my_rights_response::parse(input); 163 | /// ``` 164 | pub fn parse(input: impl Into>) -> MyRightsResponse { 165 | let mut unsolicited_responses = VecDeque::new(); 166 | 167 | MyRightsResponse::parse(input.into(), &mut unsolicited_responses).unwrap() 168 | } 169 | } 170 | 171 | /// Methods to build a [`QuotaResponse`] response object 172 | pub mod quota_response { 173 | use crate::types::QuotaResponse; 174 | use std::collections::VecDeque; 175 | 176 | /// Builds an [`QuotaResponse`] based on the provided input 177 | /// 178 | /// Example input. 179 | /// 180 | /// ``` 181 | /// let input = "* QUOTA my_root (STORAGE 10 500)\r\n"; 182 | /// let response = imap::testing::quota_response::parse(input); 183 | /// ``` 184 | pub fn parse(input: impl Into>) -> QuotaResponse { 185 | let mut unsolicited_responses = VecDeque::new(); 186 | 187 | QuotaResponse::parse(input.into(), &mut unsolicited_responses).unwrap() 188 | } 189 | } 190 | 191 | /// Methods to build a [`QuotaRootResponse`] response object 192 | pub mod quota_root_response { 193 | use crate::types::QuotaRootResponse; 194 | use std::collections::VecDeque; 195 | 196 | /// Builds an [`QuotaRootResponse`] based on the provided input 197 | /// 198 | /// Example input. 199 | /// 200 | /// ``` 201 | /// let input = "\ 202 | /// * QUOTAROOT INBOX my_root\r\n\ 203 | /// * QUOTA my_root (STORAGE 10 500)\r\n\ 204 | /// "; 205 | /// let response = imap::testing::quota_root_response::parse(input); 206 | /// ``` 207 | pub fn parse(input: impl Into>) -> QuotaRootResponse { 208 | let mut unsolicited_responses = VecDeque::new(); 209 | 210 | QuotaRootResponse::parse(input.into(), &mut unsolicited_responses).unwrap() 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/types/acls.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::parse::{parse_until_done, MapOrNot}; 3 | use crate::types::UnsolicitedResponse; 4 | #[cfg(doc)] 5 | use crate::Session; 6 | use imap_proto::types::AclRight; 7 | use imap_proto::Response; 8 | use ouroboros::self_referencing; 9 | use std::borrow::Cow; 10 | use std::collections::HashSet; 11 | use std::collections::VecDeque; 12 | use std::fmt::{Display, Formatter}; 13 | 14 | /// Specifies how [`Session::set_acl`] should modify an existing permission set. 15 | #[derive(Debug, Clone, Copy)] 16 | pub enum AclModifyMode { 17 | /// Replace all ACLs on the identifier for the mailbox 18 | Replace, 19 | /// Add the given ACLs to the identifier for the mailbox 20 | Add, 21 | /// Remove the given ACLs from the identifier for the mailbox 22 | Remove, 23 | } 24 | 25 | /// A set of [`imap_proto::AclRight`]s. 26 | /// 27 | /// Used as input for [`Session::set_acl`] as output in [`ListRights`], [`MyRights`], and [`AclEntry`] 28 | #[derive(Debug, Eq, PartialEq, Clone)] 29 | pub struct AclRights { 30 | pub(crate) data: HashSet, 31 | } 32 | 33 | impl AclRights { 34 | /// Returns true if the AclRights has the provided ACL (either as a char or an AclRight enum) 35 | pub fn contains>(&self, right: T) -> bool { 36 | self.data.contains(&right.into()) 37 | } 38 | } 39 | 40 | impl Display for AclRights { 41 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 42 | let mut v: Vec = self.data.iter().map(|c| char::from(*c)).collect(); 43 | 44 | v.sort_unstable(); 45 | 46 | write!(f, "{}", v.into_iter().collect::()) 47 | } 48 | } 49 | 50 | impl From> for AclRights { 51 | fn from(hash: HashSet) -> Self { 52 | Self { data: hash } 53 | } 54 | } 55 | 56 | impl From> for AclRights { 57 | fn from(vec: Vec) -> Self { 58 | AclRights { 59 | data: vec.into_iter().collect(), 60 | } 61 | } 62 | } 63 | 64 | impl TryFrom<&str> for AclRights { 65 | type Error = AclRightError; 66 | 67 | fn try_from(input: &str) -> Result { 68 | if !input 69 | .chars() 70 | .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) 71 | { 72 | return Err(AclRightError::InvalidRight); 73 | } 74 | 75 | Ok(input 76 | .chars() 77 | .map(|c| c.into()) 78 | .collect::>() 79 | .into()) 80 | } 81 | } 82 | 83 | /// Error from parsing AclRight strings 84 | #[derive(Debug, Eq, PartialEq)] 85 | #[non_exhaustive] 86 | pub enum AclRightError { 87 | /// Returned when a non-lower-case alpha numeric is provided in the rights list string. 88 | InvalidRight, 89 | } 90 | 91 | impl Display for AclRightError { 92 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 93 | match *self { 94 | AclRightError::InvalidRight => { 95 | write!(f, "Rights may only be lowercase alpha numeric characters") 96 | } 97 | } 98 | } 99 | } 100 | 101 | impl std::error::Error for AclRightError {} 102 | 103 | /// From [section 3.6 of RFC 4313](https://datatracker.ietf.org/doc/html/rfc4314#section-3.6). 104 | /// 105 | /// This is a wrapper around a single [`Acl`]. 106 | /// 107 | /// The ACL response from the [`Session::get_acl`] IMAP command 108 | #[self_referencing] 109 | pub struct AclResponse { 110 | data: Vec, 111 | #[borrows(data)] 112 | #[covariant] 113 | pub(crate) acl: Acl<'this>, 114 | } 115 | 116 | impl AclResponse { 117 | /// Parse the given input into a [`Acl`] response. 118 | pub(crate) fn parse( 119 | owned: Vec, 120 | unsolicited: &mut VecDeque, 121 | ) -> Result { 122 | AclResponseTryBuilder { 123 | data: owned, 124 | acl_builder: |input| { 125 | // There should only be ONE single ACL response 126 | parse_until_done(input, unsolicited, |response| match response { 127 | Response::Acl(a) => Ok(MapOrNot::Map(Acl { 128 | mailbox: a.mailbox, 129 | acls: a 130 | .acls 131 | .into_iter() 132 | .map(|e| AclEntry { 133 | identifier: e.identifier, 134 | rights: e.rights.into(), 135 | }) 136 | .collect(), 137 | })), 138 | resp => Ok(MapOrNot::Not(resp)), 139 | }) 140 | }, 141 | } 142 | .try_build() 143 | } 144 | 145 | /// Access to the wrapped [`ListRights`] struct 146 | pub fn parsed(&self) -> &Acl<'_> { 147 | self.borrow_acl() 148 | } 149 | } 150 | 151 | /// From [section 3.6 of RFC 4313](https://datatracker.ietf.org/doc/html/rfc4314#section-3.6). 152 | /// 153 | /// Used by [`AclResponse`]. 154 | #[derive(Debug, Eq, PartialEq)] 155 | pub struct Acl<'a> { 156 | /// The mailbox the ACL Entries belong to 157 | pub(crate) mailbox: Cow<'a, str>, 158 | /// The list of identifier/rights pairs for the mailbox 159 | pub(crate) acls: Vec>, 160 | } 161 | 162 | impl<'a> Acl<'a> { 163 | /// Return the mailbox the ACL entries belong to 164 | pub fn mailbox(&self) -> &str { 165 | &self.mailbox 166 | } 167 | 168 | /// Returns a list of identifier/rights pairs for the mailbox 169 | pub fn acls(&self) -> &[AclEntry<'_>] { 170 | &self.acls 171 | } 172 | } 173 | 174 | /// From [section 3.6 of RFC 4313](https://datatracker.ietf.org/doc/html/rfc4314#section-3.6). 175 | /// 176 | /// The list of identifiers and rights for the [`Acl`] response 177 | #[derive(Debug, Eq, PartialEq, Clone)] 178 | #[non_exhaustive] 179 | pub struct AclEntry<'a> { 180 | /// The user identifier the rights are for 181 | pub identifier: Cow<'a, str>, 182 | /// the rights for the provided identifier 183 | pub rights: AclRights, 184 | } 185 | 186 | /// From [section 3.7 of RFC 4313](https://datatracker.ietf.org/doc/html/rfc4314#section-3.7). 187 | /// 188 | /// This is a wrapper around a single [`ListRights`]. 189 | /// 190 | /// The LISTRIGHTS response from the [`Session::list_rights`] IMAP command 191 | #[self_referencing] 192 | pub struct ListRightsResponse { 193 | data: Vec, 194 | #[borrows(data)] 195 | #[covariant] 196 | pub(crate) rights: ListRights<'this>, 197 | } 198 | 199 | impl ListRightsResponse { 200 | /// Parse the given input into a [`ListRights`] response. 201 | pub(crate) fn parse( 202 | owned: Vec, 203 | unsolicited: &mut VecDeque, 204 | ) -> Result { 205 | ListRightsResponseTryBuilder { 206 | data: owned, 207 | rights_builder: |input| { 208 | // There should only be ONE single LISTRIGHTS response 209 | parse_until_done(input, unsolicited, |response| match response { 210 | Response::ListRights(a) => Ok(MapOrNot::Map(ListRights { 211 | mailbox: a.mailbox, 212 | identifier: a.identifier, 213 | required: a.required.into(), 214 | optional: a.optional.into(), 215 | })), 216 | resp => Ok(MapOrNot::Not(resp)), 217 | }) 218 | }, 219 | } 220 | .try_build() 221 | } 222 | 223 | /// Access to the wrapped [`ListRights`] struct 224 | pub fn parsed(&self) -> &ListRights<'_> { 225 | self.borrow_rights() 226 | } 227 | } 228 | 229 | /// From [section 3.7 of RFC 4313](https://datatracker.ietf.org/doc/html/rfc4314#section-3.7). 230 | /// 231 | /// Used by [`ListRightsResponse`]. 232 | #[derive(Debug, Eq, PartialEq)] 233 | pub struct ListRights<'a> { 234 | /// The mailbox for the rights 235 | pub(crate) mailbox: Cow<'a, str>, 236 | /// The user identifier for the rights 237 | pub(crate) identifier: Cow<'a, str>, 238 | /// The set of rights that are always provided for this identifier 239 | pub(crate) required: AclRights, 240 | /// The set of rights that can be granted to the identifier 241 | pub(crate) optional: AclRights, 242 | } 243 | 244 | impl ListRights<'_> { 245 | /// Returns the mailbox for the rights 246 | pub fn mailbox(&self) -> &str { 247 | &self.mailbox 248 | } 249 | 250 | /// Returns the user identifier for the rights 251 | pub fn identifier(&self) -> &str { 252 | &self.identifier 253 | } 254 | 255 | /// Returns the set of rights that are always provided for this identifier 256 | pub fn required(&self) -> &AclRights { 257 | &self.required 258 | } 259 | 260 | /// Returns the set of rights that can be granted to the identifier 261 | pub fn optional(&self) -> &AclRights { 262 | &self.optional 263 | } 264 | } 265 | 266 | /// From [section 3.8 of RFC 4313](https://datatracker.ietf.org/doc/html/rfc4314#section-3.8). 267 | /// 268 | /// This is a wrapper around a single [`MyRights`]. 269 | /// 270 | /// The MYRIGHTS response from the [`Session::my_rights`] IMAP command 271 | #[self_referencing] 272 | pub struct MyRightsResponse { 273 | data: Vec, 274 | #[borrows(data)] 275 | #[covariant] 276 | pub(crate) rights: MyRights<'this>, 277 | } 278 | 279 | impl MyRightsResponse { 280 | /// Parse the given input into a [`MyRights`] response. 281 | pub(crate) fn parse( 282 | owned: Vec, 283 | unsolicited: &mut VecDeque, 284 | ) -> Result { 285 | MyRightsResponseTryBuilder { 286 | data: owned, 287 | rights_builder: |input| { 288 | // There should only be ONE single MYRIGHTS response 289 | parse_until_done(input, unsolicited, |response| match response { 290 | Response::MyRights(a) => Ok(MapOrNot::Map(MyRights { 291 | mailbox: a.mailbox, 292 | rights: a.rights.into(), 293 | })), 294 | resp => Ok(MapOrNot::Not(resp)), 295 | }) 296 | }, 297 | } 298 | .try_build() 299 | } 300 | 301 | /// Access to the wrapped [`MyRights`] struct 302 | pub fn parsed(&self) -> &MyRights<'_> { 303 | self.borrow_rights() 304 | } 305 | } 306 | 307 | /// From [section 3.8 of RFC 4313](https://datatracker.ietf.org/doc/html/rfc4314#section-3.8). 308 | /// 309 | /// Used by [`MyRightsResponse`]. 310 | #[derive(Debug, Eq, PartialEq)] 311 | pub struct MyRights<'a> { 312 | /// The mailbox for the rights 313 | pub(crate) mailbox: Cow<'a, str>, 314 | /// The rights for the mailbox 315 | pub(crate) rights: AclRights, 316 | } 317 | 318 | impl MyRights<'_> { 319 | /// Returns the mailbox for the rights 320 | pub fn mailbox(&self) -> &str { 321 | &self.mailbox 322 | } 323 | 324 | /// Returns the rights for the mailbox 325 | pub fn rights(&self) -> &AclRights { 326 | &self.rights 327 | } 328 | } 329 | 330 | #[cfg(test)] 331 | mod tests { 332 | use super::*; 333 | 334 | #[test] 335 | fn test_acl_rights_to_string() { 336 | let rights: AclRights = vec![ 337 | AclRight::Lookup, 338 | AclRight::Read, 339 | AclRight::Seen, 340 | AclRight::Custom('0'), 341 | ] 342 | .into(); 343 | let expected = "0lrs"; 344 | 345 | assert_eq!(rights.to_string(), expected); 346 | } 347 | 348 | #[test] 349 | fn test_str_to_acl_rights() { 350 | let right_string = "lrskx0"; 351 | 352 | let rights: Result = right_string.try_into(); 353 | 354 | assert_eq!( 355 | rights, 356 | Ok(vec![ 357 | AclRight::Lookup, 358 | AclRight::Read, 359 | AclRight::Seen, 360 | AclRight::CreateMailbox, 361 | AclRight::DeleteMailbox, 362 | AclRight::Custom('0'), 363 | ] 364 | .into()) 365 | ); 366 | } 367 | 368 | #[test] 369 | fn test_str_to_acl_rights_invalid_right_character() { 370 | let right_string = "l_"; 371 | 372 | let rights: Result = right_string.try_into(); 373 | 374 | assert_eq!(rights, Err(AclRightError::InvalidRight)); 375 | 376 | assert_eq!( 377 | format!("{}", rights.unwrap_err()), 378 | "Rights may only be lowercase alpha numeric characters" 379 | ); 380 | } 381 | 382 | #[test] 383 | fn test_acl_rights_contains() { 384 | let rights: AclRights = "lrskx".try_into().unwrap(); 385 | 386 | assert!(rights.contains('l')); 387 | assert!(rights.contains(AclRight::Lookup)); 388 | assert!(!rights.contains('0')); 389 | assert!(!rights.contains(AclRight::Custom('0'))); 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/types/appended.rs: -------------------------------------------------------------------------------- 1 | use imap_proto::UidSetMember; 2 | 3 | #[cfg(doc)] 4 | use crate::types::Uid; 5 | 6 | /// Meta-information about a message, as returned by 7 | /// [`APPEND`](https://tools.ietf.org/html/rfc3501#section-6.3.11). 8 | /// Note that `APPEND` only returns any data if certain extensions are enabled, 9 | /// for example [`UIDPLUS`](https://tools.ietf.org/html/rfc4315). 10 | #[derive(Clone, Debug, Eq, PartialEq)] 11 | #[non_exhaustive] 12 | pub struct Appended { 13 | /// The unique identifier validity value of the mailbox that the message was appended to. 14 | /// See [`Uid`] for more details. Only present if server supports [`UIDPLUS`](https://tools.ietf.org/html/rfc4315). 15 | pub uid_validity: Option, 16 | 17 | /// The unique identifier value of the messages that were appended. 18 | /// Only present if server supports [`UIDPLUS`](https://tools.ietf.org/html/rfc4315). 19 | /// Contains only a single value unless the [`MULTIAPPEND`](https://tools.ietf.org/html/rfc3502) extension 20 | /// was used to upload multiple messages. 21 | pub uids: Option>, 22 | } 23 | 24 | #[allow(clippy::derivable_impls)] 25 | impl Default for Appended { 26 | fn default() -> Appended { 27 | Appended { 28 | uid_validity: None, 29 | uids: None, 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/types/capabilities.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::parse::{parse_many_into, MapOrNot}; 3 | use crate::types::UnsolicitedResponse; 4 | use imap_proto::{Capability, Response}; 5 | use ouroboros::self_referencing; 6 | use std::collections::hash_set::Iter; 7 | use std::collections::HashSet; 8 | use std::collections::VecDeque; 9 | 10 | const IMAP4REV1_CAPABILITY: &str = "IMAP4rev1"; 11 | const AUTH_CAPABILITY_PREFIX: &str = "AUTH="; 12 | 13 | /// From [section 7.2.1 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-7.2.1). 14 | /// 15 | /// A list of capabilities that the server supports. 16 | /// The capability list will include the atom "IMAP4rev1". 17 | /// 18 | /// In addition, all servers implement the `STARTTLS`, `LOGINDISABLED`, and `AUTH=PLAIN` (described 19 | /// in [IMAP-TLS](https://tools.ietf.org/html/rfc2595)) capabilities. See the [Security 20 | /// Considerations section of the RFC](https://tools.ietf.org/html/rfc3501#section-11) for 21 | /// important information. 22 | /// 23 | /// A capability name which begins with `AUTH=` indicates that the server supports that particular 24 | /// authentication mechanism. 25 | /// 26 | /// The `LOGINDISABLED` capability indicates that the `LOGIN` command is disabled, and that the 27 | /// server will respond with a [`super::Error::No`] response to any attempt to use the `LOGIN` 28 | /// command even if the user name and password are valid. An IMAP client MUST NOT issue the 29 | /// `LOGIN` command if the server advertises the `LOGINDISABLED` capability. 30 | /// 31 | /// Other capability names indicate that the server supports an extension, revision, or amendment 32 | /// to the IMAP4rev1 protocol. Capability names either begin with `X` or they are standard or 33 | /// standards-track [RFC 3501](https://tools.ietf.org/html/rfc3501) extensions, revisions, or 34 | /// amendments registered with IANA. 35 | /// 36 | /// Client implementations SHOULD NOT require any capability name other than `IMAP4rev1`, and MUST 37 | /// ignore any unknown capability names. 38 | #[self_referencing] 39 | pub struct Capabilities { 40 | data: Vec, 41 | #[borrows(data)] 42 | #[covariant] 43 | pub(crate) capabilities: HashSet>, 44 | } 45 | 46 | impl Capabilities { 47 | /// Parse the given input into one or more [`Capabilitity`] responses. 48 | pub(crate) fn parse( 49 | owned: Vec, 50 | unsolicited: &mut VecDeque, 51 | ) -> Result { 52 | CapabilitiesTryBuilder { 53 | data: owned, 54 | capabilities_builder: |input| { 55 | let mut caps = HashSet::new(); 56 | parse_many_into(input, &mut caps, unsolicited, |response| match response { 57 | Response::Capabilities(c) => Ok(MapOrNot::MapVec(c)), 58 | resp => Ok(MapOrNot::Not(resp)), 59 | })?; 60 | Ok(caps) 61 | }, 62 | } 63 | .try_build() 64 | } 65 | 66 | /// Check if the server has the given capability. 67 | pub fn has(&self, cap: &Capability<'_>) -> bool { 68 | self.borrow_capabilities().contains(cap) 69 | } 70 | 71 | /// Check if the server has the given capability via str. 72 | pub fn has_str>(&self, cap: S) -> bool { 73 | let s = cap.as_ref(); 74 | if s.eq_ignore_ascii_case(IMAP4REV1_CAPABILITY) { 75 | return self.has(&Capability::Imap4rev1); 76 | } 77 | if s.len() > AUTH_CAPABILITY_PREFIX.len() { 78 | let (pre, val) = s.split_at(AUTH_CAPABILITY_PREFIX.len()); 79 | if pre.eq_ignore_ascii_case(AUTH_CAPABILITY_PREFIX) { 80 | return self.has(&Capability::Auth(val.into())); 81 | } 82 | } 83 | self.has(&Capability::Atom(s.into())) 84 | } 85 | 86 | /// Iterate over all the server's capabilities 87 | pub fn iter(&self) -> Iter<'_, Capability<'_>> { 88 | self.borrow_capabilities().iter() 89 | } 90 | 91 | /// Returns how many capabilities the server has. 92 | pub fn len(&self) -> usize { 93 | self.borrow_capabilities().len() 94 | } 95 | 96 | /// Returns true if the server purports to have no capabilities. 97 | pub fn is_empty(&self) -> bool { 98 | self.borrow_capabilities().is_empty() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/types/deleted.rs: -------------------------------------------------------------------------------- 1 | use super::{Seq, Uid}; 2 | use std::ops::RangeInclusive; 3 | 4 | /// A struct containing message sequence numbers or UID sequence sets and a mod 5 | /// sequence returned in response to a `EXPUNGE` command. 6 | /// 7 | /// The `EXPUNGE` command may return several `EXPUNGE` responses referencing 8 | /// message sequence numbers, or it may return a `VANISHED` response referencing 9 | /// multiple UID values in a sequence set if the client has enabled 10 | /// [QRESYNC](https://tools.ietf.org/html/rfc7162#section-3.2.7). If `QRESYNC` is 11 | /// enabled, the server will also return the mod sequence of the completed 12 | /// operation. 13 | /// 14 | /// `Deleted` implements some iterators to make it easy to use. If the caller 15 | /// knows that they should be receiving an `EXPUNGE` or `VANISHED` response, 16 | /// then they can use [`seqs()`](#method.seqs) to get an iterator over `EXPUNGE` 17 | /// message sequence numbers, or [`uids()`](#method.uids) to get an iterator over 18 | /// the `VANISHED` UIDs. As a convenience `Deleted` also implements `IntoIterator` 19 | /// which just returns an iterator over whatever is contained within. 20 | /// 21 | /// # Examples 22 | /// ```no_run 23 | /// # {} #[cfg(feature = "native-tls")] 24 | /// # fn main() { 25 | /// # let client = imap::ClientBuilder::new("imap.example.com", 993) 26 | /// .connect().unwrap(); 27 | /// # let mut session = client.login("name", "pw").unwrap(); 28 | /// // Iterate over whatever is returned 29 | /// if let Ok(deleted) = session.expunge() { 30 | /// for id in &deleted { 31 | /// // Do something with id 32 | /// } 33 | /// } 34 | /// 35 | /// // Expect a VANISHED response with UIDs 36 | /// if let Ok(deleted) = session.expunge() { 37 | /// for uid in deleted.uids() { 38 | /// // Do something with uid 39 | /// } 40 | /// } 41 | /// # } 42 | /// ``` 43 | #[derive(Debug, Clone)] 44 | #[non_exhaustive] 45 | pub struct Deleted { 46 | /// The list of messages that were expunged 47 | pub messages: DeletedMessages, 48 | /// The mod sequence of the performed operation, if the `QRESYNC` extension 49 | /// is enabled. 50 | pub mod_seq: Option, 51 | } 52 | 53 | #[derive(Debug, Clone)] 54 | pub enum DeletedMessages { 55 | /// Message sequence numbers given in an `EXPUNGE` response. 56 | Expunged(Vec), 57 | /// Message UIDs given in a `VANISHED` response. 58 | Vanished(Vec>), 59 | } 60 | 61 | impl Deleted { 62 | /// Construct a new `Deleted` value from a vector of message sequence 63 | /// numbers returned in one or more `EXPUNGE` responses. 64 | pub fn from_expunged(v: Vec, mod_seq: Option) -> Self { 65 | Self { 66 | messages: DeletedMessages::Expunged(v), 67 | mod_seq, 68 | } 69 | } 70 | 71 | /// Construct a new `Deleted` value from a sequence-set of UIDs 72 | /// returned in a `VANISHED` response 73 | pub fn from_vanished(v: Vec>, mod_seq: Option) -> Self { 74 | Self { 75 | messages: DeletedMessages::Vanished(v), 76 | mod_seq, 77 | } 78 | } 79 | 80 | /// Return an iterator over message sequence numbers from an `EXPUNGE` 81 | /// response. If the client is expecting sequence numbers this function 82 | /// can be used to ensure only sequence numbers returned in an `EXPUNGE` 83 | /// response are processed. 84 | pub fn seqs(&self) -> impl Iterator + '_ { 85 | match &self.messages { 86 | DeletedMessages::Expunged(s) => s.iter(), 87 | DeletedMessages::Vanished(_) => [].iter(), 88 | } 89 | .copied() 90 | } 91 | 92 | /// Return an iterator over UIDs returned in a `VANISHED` response. 93 | /// If the client is expecting UIDs this function can be used to ensure 94 | /// only UIDs are processed. 95 | pub fn uids(&self) -> impl Iterator + '_ { 96 | match &self.messages { 97 | DeletedMessages::Expunged(_) => [].iter(), 98 | DeletedMessages::Vanished(s) => s.iter(), 99 | } 100 | .flat_map(|range| range.clone()) 101 | } 102 | 103 | /// Return if the set is empty 104 | pub fn is_empty(&self) -> bool { 105 | match &self.messages { 106 | DeletedMessages::Expunged(v) => v.is_empty(), 107 | DeletedMessages::Vanished(v) => v.is_empty(), 108 | } 109 | } 110 | } 111 | 112 | impl<'a> IntoIterator for &'a Deleted { 113 | type Item = u32; 114 | type IntoIter = Box + 'a>; 115 | 116 | fn into_iter(self) -> Self::IntoIter { 117 | match &self.messages { 118 | DeletedMessages::Expunged(_) => Box::new(self.seqs()), 119 | DeletedMessages::Vanished(_) => Box::new(self.uids()), 120 | } 121 | } 122 | } 123 | 124 | #[cfg(test)] 125 | mod test { 126 | use super::*; 127 | 128 | #[test] 129 | fn seq() { 130 | let seqs = Deleted::from_expunged(vec![3, 6, 9, 12], None); 131 | let mut i = seqs.into_iter(); 132 | assert_eq!(Some(3), i.next()); 133 | assert_eq!(Some(6), i.next()); 134 | assert_eq!(Some(9), i.next()); 135 | assert_eq!(Some(12), i.next()); 136 | assert_eq!(None, i.next()); 137 | 138 | let seqs = Deleted::from_expunged(vec![], None); 139 | let mut i = seqs.into_iter(); 140 | assert_eq!(None, i.next()); 141 | } 142 | 143 | #[test] 144 | fn seq_set() { 145 | let uids = Deleted::from_vanished(vec![1..=1, 3..=5, 8..=9, 12..=12], None); 146 | let mut i = uids.into_iter(); 147 | assert_eq!(Some(1), i.next()); 148 | assert_eq!(Some(3), i.next()); 149 | assert_eq!(Some(4), i.next()); 150 | assert_eq!(Some(5), i.next()); 151 | assert_eq!(Some(8), i.next()); 152 | assert_eq!(Some(9), i.next()); 153 | assert_eq!(Some(12), i.next()); 154 | assert_eq!(None, i.next()); 155 | 156 | let uids = Deleted::from_vanished(vec![], None); 157 | assert_eq!(None, uids.into_iter().next()); 158 | } 159 | 160 | #[test] 161 | fn seqs() { 162 | let seqs: Deleted = Deleted::from_expunged(vec![3, 6, 9, 12], None); 163 | let mut count: u32 = 0; 164 | for seq in seqs.seqs() { 165 | count += 3; 166 | assert_eq!(seq, count); 167 | } 168 | assert_eq!(count, 12); 169 | } 170 | 171 | #[test] 172 | fn uids() { 173 | let uids: Deleted = Deleted::from_vanished(vec![1..=6], None); 174 | let mut count: u32 = 0; 175 | for uid in uids.uids() { 176 | count += 1; 177 | assert_eq!(uid, count); 178 | } 179 | assert_eq!(count, 6); 180 | } 181 | 182 | #[test] 183 | fn generic_iteration() { 184 | let seqs: Deleted = Deleted::from_expunged(vec![3, 6, 9, 12], None); 185 | let mut count: u32 = 0; 186 | for seq in &seqs { 187 | count += 3; 188 | assert_eq!(seq, count); 189 | } 190 | assert_eq!(count, 12); 191 | 192 | let uids: Deleted = Deleted::from_vanished(vec![1..=6], None); 193 | let mut count: u32 = 0; 194 | for uid in &uids { 195 | count += 1; 196 | assert_eq!(uid, count); 197 | } 198 | assert_eq!(count, 6); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/types/fetch.rs: -------------------------------------------------------------------------------- 1 | use super::{Flag, Seq, Uid}; 2 | use crate::error::Error; 3 | use crate::parse::{parse_many_into, MapOrNot}; 4 | use crate::types::UnsolicitedResponse; 5 | use chrono::{DateTime, FixedOffset}; 6 | use imap_proto::types::{ 7 | AttributeValue, BodyStructure, Envelope, MessageSection, Response, SectionPath, 8 | }; 9 | use ouroboros::self_referencing; 10 | use std::collections::VecDeque; 11 | use std::slice::Iter; 12 | 13 | /// Format of Date and Time as defined RFC3501. 14 | /// See `date-time` element in [Formal Syntax](https://tools.ietf.org/html/rfc3501#section-9) 15 | /// chapter of this RFC. 16 | const DATE_TIME_FORMAT: &str = "%d-%b-%Y %H:%M:%S %z"; 17 | 18 | /// A wrapper for one or more [`Fetch`] responses. 19 | #[self_referencing] 20 | pub struct Fetches { 21 | data: Vec, 22 | #[borrows(data)] 23 | #[covariant] 24 | pub(crate) fetches: Vec>, 25 | } 26 | 27 | impl Fetches { 28 | /// Parse one or more [`Fetch`] responses from a response buffer. 29 | pub(crate) fn parse( 30 | owned: Vec, 31 | unsolicited: &mut VecDeque, 32 | ) -> Result { 33 | FetchesTryBuilder { 34 | data: owned, 35 | fetches_builder: |input| { 36 | let mut fetches = Vec::new(); 37 | parse_many_into(input, &mut fetches, unsolicited, |response| { 38 | match response { 39 | Response::Fetch(num, attrs) => { 40 | let mut fetch = Fetch { 41 | message: num, 42 | flags: vec![], 43 | uid: None, 44 | size: None, 45 | fetch: attrs, 46 | }; 47 | 48 | // set some common fields eagerly 49 | for attr in &fetch.fetch { 50 | match attr { 51 | AttributeValue::Flags(flags) => { 52 | fetch.flags.extend(Flag::from_strs(flags)); 53 | } 54 | AttributeValue::Uid(uid) => fetch.uid = Some(*uid), 55 | AttributeValue::Rfc822Size(sz) => fetch.size = Some(*sz), 56 | _ => {} 57 | } 58 | } 59 | Ok(MapOrNot::Map(fetch)) 60 | } 61 | resp => Ok(MapOrNot::Not(resp)), 62 | } 63 | })?; 64 | Ok(fetches) 65 | }, 66 | } 67 | .try_build() 68 | } 69 | 70 | /// Iterate over the contained [`Fetch`]es. 71 | pub fn iter(&self) -> Iter<'_, Fetch<'_>> { 72 | self.borrow_fetches().iter() 73 | } 74 | 75 | /// Get the number of [`Fetch`]es in this container. 76 | pub fn len(&self) -> usize { 77 | self.borrow_fetches().len() 78 | } 79 | 80 | /// Return true if there are no [`Fetch`]es in the container. 81 | pub fn is_empty(&self) -> bool { 82 | self.borrow_fetches().is_empty() 83 | } 84 | 85 | /// Get the element at the given index 86 | pub fn get(&self, index: usize) -> Option<&Fetch<'_>> { 87 | self.borrow_fetches().get(index) 88 | } 89 | } 90 | 91 | /// An IMAP [`FETCH` response](https://tools.ietf.org/html/rfc3501#section-7.4.2) that contains 92 | /// data about a particular message. This response occurs as the result of a `FETCH` or `STORE` 93 | /// command, as well as by unilateral server decision (e.g., flag updates). 94 | #[derive(Debug, Eq, PartialEq)] 95 | pub struct Fetch<'a> { 96 | /// The ordinal number of this message in its containing mailbox. 97 | pub message: Seq, 98 | 99 | /// A number expressing the unique identifier of the message. 100 | /// Only present if `UID` was specified in the query argument to `FETCH` and the server 101 | /// supports UIDs. 102 | pub uid: Option, 103 | 104 | /// A number expressing the [RFC-2822](https://tools.ietf.org/html/rfc2822) size of the message. 105 | /// Only present if `RFC822.SIZE` was specified in the query argument to `FETCH`. 106 | pub size: Option, 107 | 108 | pub(crate) fetch: Vec>, 109 | pub(crate) flags: Vec>, 110 | } 111 | 112 | impl<'a> Fetch<'a> { 113 | /// A list of flags that are set for this message. 114 | pub fn flags(&self) -> &[Flag<'a>] { 115 | &self.flags[..] 116 | } 117 | 118 | /// Contains the mod sequence of the performed operation if in response to a `STORE` or 119 | /// `UID STORE` with `UNCHANGEDSINCE` in the query arguments. 120 | /// `UNCHANGEDSINCE` and the inclusion of the mod sequence in the response are part of the 121 | /// [QRESYNC](https://tools.ietf.org/html/rfc7162#section-3.1.3) extension. 122 | pub fn mod_seq(&self) -> Option { 123 | self.fetch.iter().find_map(|av| match av { 124 | AttributeValue::ModSeq(mod_seq) => Some(*mod_seq), 125 | _ => None, 126 | }) 127 | } 128 | 129 | /// The bytes that make up the header of this message, if `BODY[HEADER]`, `BODY.PEEK[HEADER]`, 130 | /// or `RFC822.HEADER` was included in the `query` argument to `FETCH`. 131 | pub fn header(&self) -> Option<&[u8]> { 132 | self.fetch.iter().find_map(|av| match av { 133 | AttributeValue::BodySection { 134 | section: Some(SectionPath::Full(MessageSection::Header)), 135 | data: Some(hdr), 136 | .. 137 | } 138 | | AttributeValue::Rfc822Header(Some(hdr)) => Some(&**hdr), 139 | _ => None, 140 | }) 141 | } 142 | 143 | /// The bytes that make up this message, included if `BODY[]` or `RFC822` was included in the 144 | /// `query` argument to `FETCH`. The bytes SHOULD be interpreted by the client according to the 145 | /// content transfer encoding, body type, and subtype. 146 | pub fn body(&self) -> Option<&[u8]> { 147 | self.fetch.iter().find_map(|av| match av { 148 | AttributeValue::BodySection { 149 | section: None, 150 | data: Some(body), 151 | .. 152 | } 153 | | AttributeValue::Rfc822(Some(body)) => Some(&**body), 154 | _ => None, 155 | }) 156 | } 157 | 158 | /// The bytes that make up the text of this message, included if `BODY[TEXT]`, `RFC822.TEXT`, 159 | /// or `BODY.PEEK[TEXT]` was included in the `query` argument to `FETCH`. The bytes SHOULD be 160 | /// interpreted by the client according to the content transfer encoding, body type, and 161 | /// subtype. 162 | pub fn text(&self) -> Option<&[u8]> { 163 | self.fetch.iter().find_map(|av| match av { 164 | AttributeValue::BodySection { 165 | section: Some(SectionPath::Full(MessageSection::Text)), 166 | data: Some(body), 167 | .. 168 | } 169 | | AttributeValue::Rfc822Text(Some(body)) => Some(&**body), 170 | _ => None, 171 | }) 172 | } 173 | 174 | /// The envelope of this message, if `ENVELOPE` was included in the `query` argument to 175 | /// `FETCH`. This is computed by the server by parsing the 176 | /// [RFC-2822](https://tools.ietf.org/html/rfc2822) header into the component parts, defaulting 177 | /// various fields as necessary. 178 | /// 179 | /// The full description of the format of the envelope is given in [RFC 3501 section 180 | /// 7.4.2](https://tools.ietf.org/html/rfc3501#section-7.4.2). 181 | pub fn envelope(&self) -> Option<&Envelope<'_>> { 182 | self.fetch.iter().find_map(|av| match av { 183 | AttributeValue::Envelope(env) => Some(&**env), 184 | _ => None, 185 | }) 186 | } 187 | 188 | /// Extract the bytes that makes up the given `BODY[
]` of a `FETCH` response. 189 | /// 190 | /// See [section 7.4.2 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-7.4.2) for 191 | /// details. 192 | pub fn section(&self, path: &SectionPath) -> Option<&[u8]> { 193 | self.fetch.iter().find_map(|av| match av { 194 | AttributeValue::BodySection { 195 | section: Some(sp), 196 | data: Some(data), 197 | .. 198 | } if sp == path => Some(&**data), 199 | _ => None, 200 | }) 201 | } 202 | 203 | /// Extract the `INTERNALDATE` of a `FETCH` response 204 | /// 205 | /// See [section 2.3.3 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.3) for 206 | /// details. 207 | pub fn internal_date(&self) -> Option> { 208 | self.fetch 209 | .iter() 210 | .find_map(|av| match av { 211 | AttributeValue::InternalDate(date_time) => Some(&**date_time), 212 | _ => None, 213 | }) 214 | .and_then( 215 | |date_time| match DateTime::parse_from_str(date_time, DATE_TIME_FORMAT) { 216 | Ok(date_time) => Some(date_time), 217 | Err(_) => None, 218 | }, 219 | ) 220 | } 221 | 222 | /// Extract the `BODYSTRUCTURE` of a `FETCH` response 223 | /// 224 | /// See [section 2.3.6 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.6) for 225 | /// details. 226 | pub fn bodystructure(&self) -> Option<&BodyStructure<'a>> { 227 | self.fetch.iter().find_map(|av| match av { 228 | AttributeValue::BodyStructure(bs) => Some(bs), 229 | _ => None, 230 | }) 231 | } 232 | 233 | /// Extract the `X-GM-LABELS` of a `FETCH` response 234 | /// 235 | /// This is a Gmail-specific extension. See their 236 | /// [developer's page](https://developers.google.com/gmail/imap/imap-extensions) for details. 237 | pub fn gmail_labels(&'a self) -> Option> { 238 | self.fetch.iter().find_map(|av| match av { 239 | AttributeValue::GmailLabels(labels) => Some(labels.iter().map(|cow| cow.as_ref())), 240 | _ => None, 241 | }) 242 | } 243 | 244 | /// Get an owned copy of the [`Fetch`]. 245 | pub fn into_owned(self) -> Fetch<'static> { 246 | Fetch { 247 | message: self.message, 248 | uid: self.uid, 249 | size: self.size, 250 | fetch: self.fetch.into_iter().map(|av| av.into_owned()).collect(), 251 | flags: self.flags.clone(), 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/types/flag.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | #[cfg(doc)] 4 | use crate::types::Mailbox; 5 | 6 | /// With the exception of [`Flag::Custom`], these flags are system flags that are pre-defined in 7 | /// [RFC 3501 section 2.3.2](https://tools.ietf.org/html/rfc3501#section-2.3.2). All system flags 8 | /// begin with `\` in the IMAP protocol. Certain system flags (`\Deleted` and `\Seen`) have 9 | /// special semantics described elsewhere. 10 | /// 11 | /// A flag can be permanent or session-only on a per-flag basis. Permanent flags are those which 12 | /// the client can add or remove from the message flags permanently; that is, concurrent and 13 | /// subsequent sessions will see any change in permanent flags. Changes to session flags are valid 14 | /// only in that session. 15 | /// 16 | /// > Note: The `\Recent` system flag is a special case of a session flag. `\Recent` can not be 17 | /// > used as an argument in a `STORE` or `APPEND` command, and thus can not be changed at all. 18 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 19 | #[non_exhaustive] 20 | pub enum Flag<'a> { 21 | /// Message has been read 22 | Seen, 23 | 24 | /// Message has been answered 25 | Answered, 26 | 27 | /// Message is "flagged" for urgent/special attention 28 | Flagged, 29 | 30 | /// Message is "deleted" for removal by later EXPUNGE 31 | Deleted, 32 | 33 | /// Message has not completed composition (marked as a draft). 34 | Draft, 35 | 36 | /// Message is "recently" arrived in this mailbox. This session is the first session to have 37 | /// been notified about this message; if the session is read-write, subsequent sessions will 38 | /// not see `\Recent` set for this message. This flag can not be altered by the client. 39 | /// 40 | /// If it is not possible to determine whether or not this session is the first session to be 41 | /// notified about a message, then that message will generally be considered recent. 42 | /// 43 | /// If multiple connections have the same mailbox selected simultaneously, it is undefined 44 | /// which of these connections will see newly-arrived messages with `\Recent` set and which 45 | /// will see it without `\Recent` set. 46 | Recent, 47 | 48 | /// The [`Mailbox::permanent_flags`] can include this special flag (`\*`), which indicates that 49 | /// it is possible to create new keywords by attempting to store those flags in the mailbox. 50 | MayCreate, 51 | 52 | /// A non-standard user- or server-defined flag. 53 | Custom(Cow<'a, str>), 54 | } 55 | 56 | impl Flag<'static> { 57 | fn system(s: &str) -> Option { 58 | match s { 59 | "\\Seen" => Some(Flag::Seen), 60 | "\\Answered" => Some(Flag::Answered), 61 | "\\Flagged" => Some(Flag::Flagged), 62 | "\\Deleted" => Some(Flag::Deleted), 63 | "\\Draft" => Some(Flag::Draft), 64 | "\\Recent" => Some(Flag::Recent), 65 | "\\*" => Some(Flag::MayCreate), 66 | _ => None, 67 | } 68 | } 69 | 70 | /// Helper function to transform Strings into owned Flags 71 | pub fn from_strs( 72 | v: impl IntoIterator, 73 | ) -> impl Iterator> { 74 | v.into_iter().map(|s| Flag::from(s.to_string())) 75 | } 76 | } 77 | 78 | impl<'a> Flag<'a> { 79 | /// Get an owned version of the [`Flag`]. 80 | pub fn into_owned(self) -> Flag<'static> { 81 | match self { 82 | Flag::Custom(cow) => Flag::Custom(Cow::Owned(cow.into_owned())), 83 | Flag::Seen => Flag::Seen, 84 | Flag::Answered => Flag::Answered, 85 | Flag::Flagged => Flag::Flagged, 86 | Flag::Deleted => Flag::Deleted, 87 | Flag::Draft => Flag::Draft, 88 | Flag::Recent => Flag::Recent, 89 | Flag::MayCreate => Flag::MayCreate, 90 | } 91 | } 92 | } 93 | 94 | impl<'a> std::fmt::Display for Flag<'a> { 95 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 96 | match *self { 97 | Flag::Seen => write!(f, "\\Seen"), 98 | Flag::Answered => write!(f, "\\Answered"), 99 | Flag::Flagged => write!(f, "\\Flagged"), 100 | Flag::Deleted => write!(f, "\\Deleted"), 101 | Flag::Draft => write!(f, "\\Draft"), 102 | Flag::Recent => write!(f, "\\Recent"), 103 | Flag::MayCreate => write!(f, "\\*"), 104 | Flag::Custom(ref s) => write!(f, "{}", s), 105 | } 106 | } 107 | } 108 | 109 | impl<'a> From for Flag<'a> { 110 | fn from(s: String) -> Self { 111 | if let Some(f) = Flag::system(&s) { 112 | f 113 | } else { 114 | Flag::Custom(Cow::Owned(s)) 115 | } 116 | } 117 | } 118 | 119 | impl<'a> From<&'a str> for Flag<'a> { 120 | fn from(s: &'a str) -> Self { 121 | if let Some(f) = Flag::system(s) { 122 | f 123 | } else { 124 | Flag::Custom(Cow::Borrowed(s)) 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/types/mailbox.rs: -------------------------------------------------------------------------------- 1 | use super::{Flag, Uid}; 2 | use std::fmt; 3 | 4 | /// Meta-information about an IMAP mailbox, as returned by 5 | /// [`SELECT`](https://tools.ietf.org/html/rfc3501#section-6.3.1) and friends. 6 | #[derive(Clone, Debug, Eq, PartialEq, Hash)] 7 | #[non_exhaustive] 8 | pub struct Mailbox { 9 | /// Defined flags in the mailbox. See the description of the [FLAGS 10 | /// response](https://tools.ietf.org/html/rfc3501#section-7.2.6) for more detail. 11 | pub flags: Vec>, 12 | 13 | /// The number of messages in the mailbox. See the description of the [EXISTS 14 | /// response](https://tools.ietf.org/html/rfc3501#section-7.3.1) for more detail. 15 | pub exists: u32, 16 | 17 | /// The number of messages with the \Recent flag set. See the description of the [RECENT 18 | /// response](https://tools.ietf.org/html/rfc3501#section-7.3.2) for more detail. 19 | pub recent: u32, 20 | 21 | /// The message sequence number of the first unseen message in the mailbox. If this is 22 | /// missing, the client can not make any assumptions about the first unseen message in the 23 | /// mailbox, and needs to issue a `SEARCH` command if it wants to find it. 24 | pub unseen: Option, 25 | 26 | /// A list of message flags that the client can change permanently. If this is missing, the 27 | /// client should assume that all flags can be changed permanently. If the client attempts to 28 | /// STORE a flag that is not in this list list, the server will either ignore the change or 29 | /// store the state change for the remainder of the current session only. 30 | pub permanent_flags: Vec>, 31 | 32 | /// The next unique identifier value. If this is missing, the client can not make any 33 | /// assumptions about the next unique identifier value. 34 | pub uid_next: Option, 35 | 36 | /// The unique identifier validity value. See [`Uid`] for more details. If this is missing, 37 | /// the server does not support unique identifiers. 38 | pub uid_validity: Option, 39 | 40 | /// The highest mod sequence for this mailbox. Used with 41 | /// [Conditional STORE](https://tools.ietf.org/html/rfc4551#section-3.1.1). 42 | pub highest_mod_seq: Option, 43 | 44 | /// The mailbox is selected read-only, or its access while selected has changed from read-write 45 | /// to read-only. 46 | pub is_read_only: bool, 47 | } 48 | 49 | #[allow(clippy::derivable_impls)] 50 | impl Default for Mailbox { 51 | fn default() -> Mailbox { 52 | Mailbox { 53 | flags: Vec::new(), 54 | exists: 0, 55 | recent: 0, 56 | unseen: None, 57 | permanent_flags: Vec::new(), 58 | uid_next: None, 59 | uid_validity: None, 60 | highest_mod_seq: None, 61 | is_read_only: false, 62 | } 63 | } 64 | } 65 | 66 | impl fmt::Display for Mailbox { 67 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 68 | write!( 69 | f, 70 | "flags: {:?}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?},\ 71 | uid_next: {:?}, uid_validity: {:?}, highest_mod_seq: {:?}, is_read_only: {:?}", 72 | self.flags, 73 | self.exists, 74 | self.recent, 75 | self.unseen, 76 | self.permanent_flags, 77 | self.uid_next, 78 | self.uid_validity, 79 | self.highest_mod_seq, 80 | self.is_read_only, 81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/types/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains types used throughout the IMAP protocol. 2 | 3 | /// From section [2.3.1.1 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.1.1). 4 | /// 5 | /// A 32-bit value assigned to each message, which when used with the unique identifier validity 6 | /// value (see below) forms a 64-bit value that will not refer to any other message in the mailbox 7 | /// or any subsequent mailbox with the same name forever. Unique identifiers are assigned in a 8 | /// strictly ascending fashion in the mailbox; as each message is added to the mailbox it is 9 | /// assigned a higher UID than the message(s) which were added previously. Unlike message sequence 10 | /// numbers, unique identifiers are not necessarily contiguous. 11 | /// 12 | /// The unique identifier of a message will not change during the session, and will generally not 13 | /// change between sessions. Any change of unique identifiers between sessions will be detectable 14 | /// using the `UIDVALIDITY` mechanism discussed below. Persistent unique identifiers are required 15 | /// for a client to resynchronize its state from a previous session with the server (e.g., 16 | /// disconnected or offline access clients); this is discussed further in 17 | /// [`IMAP-DISC`](https://tools.ietf.org/html/rfc3501#ref-IMAP-DISC). 18 | /// 19 | /// Associated with every mailbox are two values which aid in unique identifier handling: the next 20 | /// unique identifier value and the unique identifier validity value. 21 | /// 22 | /// The next unique identifier value is the predicted value that will be assigned to a new message 23 | /// in the mailbox. Unless the unique identifier validity also changes (see below), the next 24 | /// unique identifier value will have the following two characteristics. First, the next unique 25 | /// identifier value will not change unless new messages are added to the mailbox; and second, the 26 | /// next unique identifier value will change whenever new messages are added to the mailbox, even 27 | /// if those new messages are subsequently expunged. 28 | /// 29 | /// > Note: The next unique identifier value is intended to provide a means for a client to 30 | /// > determine whether any messages have been delivered to the mailbox since the previous time it 31 | /// > checked this value. It is not intended to provide any guarantee that any message will have 32 | /// > this unique identifier. A client can only assume, at the time that it obtains the next 33 | /// > unique identifier value, that messages arriving after that time will have a UID greater than 34 | /// > or equal to that value. 35 | /// 36 | /// The unique identifier validity value is sent in a `UIDVALIDITY` response code in an `OK` 37 | /// untagged response at mailbox selection time. If unique identifiers from an earlier session fail 38 | /// to persist in this session, the unique identifier validity value will be greater than the one 39 | /// used in the earlier session. 40 | /// 41 | /// > Note: Ideally, unique identifiers will persist at all 42 | /// > times. Although this specification recognizes that failure 43 | /// > to persist can be unavoidable in certain server 44 | /// > environments, it STRONGLY ENCOURAGES message store 45 | /// > implementation techniques that avoid this problem. For 46 | /// > example: 47 | /// > 48 | /// > 1. Unique identifiers are strictly ascending in the 49 | /// > mailbox at all times. If the physical message store is 50 | /// > re-ordered by a non-IMAP agent, this requires that the 51 | /// > unique identifiers in the mailbox be regenerated, since 52 | /// > the former unique identifiers are no longer strictly 53 | /// > ascending as a result of the re-ordering. 54 | /// > 2. If the message store has no mechanism to store unique 55 | /// > identifiers, it must regenerate unique identifiers at 56 | /// > each session, and each session must have a unique 57 | /// > `UIDVALIDITY` value. 58 | /// > 3. If the mailbox is deleted and a new mailbox with the 59 | /// > same name is created at a later date, the server must 60 | /// > either keep track of unique identifiers from the 61 | /// > previous instance of the mailbox, or it must assign a 62 | /// > new `UIDVALIDITY` value to the new instance of the 63 | /// > mailbox. A good `UIDVALIDITY` value to use in this case 64 | /// > is a 32-bit representation of the creation date/time of 65 | /// > the mailbox. It is alright to use a constant such as 66 | /// > 1, but only if it guaranteed that unique identifiers 67 | /// > will never be reused, even in the case of a mailbox 68 | /// > being deleted (or renamed) and a new mailbox by the 69 | /// > same name created at some future time. 70 | /// > 4. The combination of mailbox name, `UIDVALIDITY`, and `UID` 71 | /// > must refer to a single immutable message on that server 72 | /// > forever. In particular, the internal date, [RFC 2822](https://tools.ietf.org/html/rfc2822) 73 | /// > size, envelope, body structure, and message texts 74 | /// > (RFC822, RFC822.HEADER, RFC822.TEXT, and all BODY[...] 75 | /// > fetch data items) must never change. This does not 76 | /// > include message numbers, nor does it include attributes 77 | /// > that can be set by a `STORE` command (e.g., `FLAGS`). 78 | pub type Uid = u32; 79 | 80 | /// From section [2.3.1.2 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.1.2). 81 | /// 82 | /// A relative position from 1 to the number of messages in the mailbox. 83 | /// This position is ordered by ascending unique identifier. As 84 | /// each new message is added, it is assigned a message sequence number 85 | /// that is 1 higher than the number of messages in the mailbox before 86 | /// that new message was added. 87 | /// 88 | /// Message sequence numbers can be reassigned during the session. For 89 | /// example, when a message is permanently removed (expunged) from the 90 | /// mailbox, the message sequence number for all subsequent messages is 91 | /// decremented. The number of messages in the mailbox is also 92 | /// decremented. Similarly, a new message can be assigned a message 93 | /// sequence number that was once held by some other message prior to an 94 | /// expunge. 95 | /// 96 | /// In addition to accessing messages by relative position in the 97 | /// mailbox, message sequence numbers can be used in mathematical 98 | /// calculations. For example, if an untagged "11 EXISTS" is received, 99 | /// and previously an untagged "8 EXISTS" was received, three new 100 | /// messages have arrived with message sequence numbers of 9, 10, and 11. 101 | /// Another example, if message 287 in a 523 message mailbox has UID 102 | /// 12345, there are exactly 286 messages which have lesser UIDs and 236 103 | /// messages which have greater UIDs. 104 | pub type Seq = u32; 105 | 106 | mod fetch; 107 | pub use self::fetch::{Fetch, Fetches}; 108 | 109 | mod flag; 110 | pub use self::flag::Flag; 111 | 112 | mod mailbox; 113 | pub use self::mailbox::Mailbox; 114 | 115 | mod name; 116 | pub use self::name::{Name, Names}; 117 | 118 | mod capabilities; 119 | pub use self::capabilities::Capabilities; 120 | 121 | mod deleted; 122 | pub use self::deleted::Deleted; 123 | 124 | mod acls; 125 | pub use self::acls::*; 126 | 127 | mod quota; 128 | pub use self::quota::*; 129 | 130 | mod unsolicited_response; 131 | pub use self::unsolicited_response::{AttributeValue, UnsolicitedResponse}; 132 | 133 | mod appended; 134 | pub use self::appended::Appended; 135 | -------------------------------------------------------------------------------- /src/types/name.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::parse::{parse_many_into, MapOrNot}; 3 | use crate::types::UnsolicitedResponse; 4 | use imap_proto::{MailboxDatum, NameAttribute, Response}; 5 | use ouroboros::self_referencing; 6 | use std::borrow::Cow; 7 | use std::collections::VecDeque; 8 | use std::slice::Iter; 9 | 10 | /// A wrapper for one or more [`Name`] responses. 11 | #[self_referencing] 12 | pub struct Names { 13 | data: Vec, 14 | #[borrows(data)] 15 | #[covariant] 16 | pub(crate) names: Vec>, 17 | } 18 | 19 | impl Names { 20 | /// Parse one or more [`Name`] from a response buffer 21 | pub(crate) fn parse( 22 | owned: Vec, 23 | unsolicited: &mut VecDeque, 24 | ) -> Result { 25 | NamesTryBuilder { 26 | data: owned, 27 | names_builder: |input| { 28 | let mut names = Vec::new(); 29 | parse_many_into(input, &mut names, unsolicited, |response| match response { 30 | Response::MailboxData(MailboxDatum::List { 31 | name_attributes, 32 | delimiter, 33 | name, 34 | }) => Ok(MapOrNot::Map(Name { 35 | attributes: name_attributes, 36 | delimiter, 37 | name, 38 | })), 39 | resp => Ok(MapOrNot::Not(resp)), 40 | })?; 41 | Ok(names) 42 | }, 43 | } 44 | .try_build() 45 | } 46 | 47 | /// Iterate over the contained [`Name`]s 48 | pub fn iter(&self) -> Iter<'_, Name<'_>> { 49 | self.borrow_names().iter() 50 | } 51 | 52 | /// Get the number of [`Name`]s in this container. 53 | pub fn len(&self) -> usize { 54 | self.borrow_names().len() 55 | } 56 | 57 | /// Return true of there are no [`Name`]s in the container. 58 | pub fn is_empty(&self) -> bool { 59 | self.borrow_names().is_empty() 60 | } 61 | 62 | /// Get the element at the given index 63 | pub fn get(&self, index: usize) -> Option<&Name<'_>> { 64 | self.borrow_names().get(index) 65 | } 66 | } 67 | 68 | /// A name that matches a `LIST` or `LSUB` command. 69 | #[derive(Debug, Eq, PartialEq)] 70 | pub struct Name<'a> { 71 | pub(crate) attributes: Vec>, 72 | pub(crate) delimiter: Option>, 73 | pub(crate) name: Cow<'a, str>, 74 | } 75 | 76 | impl<'a> Name<'a> { 77 | /// Attributes of this name. 78 | pub fn attributes(&self) -> &[NameAttribute<'a>] { 79 | &self.attributes[..] 80 | } 81 | 82 | /// The hierarchy delimiter is a character used to delimit levels of hierarchy in a mailbox 83 | /// name. A client can use it to create child mailboxes, and to search higher or lower levels 84 | /// of naming hierarchy. All children of a top-level hierarchy node use the same 85 | /// separator character. `None` means that no hierarchy exists; the name is a "flat" name. 86 | pub fn delimiter(&self) -> Option<&str> { 87 | self.delimiter.as_deref() 88 | } 89 | 90 | /// The name represents an unambiguous left-to-right hierarchy, and are valid for use as a 91 | /// reference in `LIST` and `LSUB` commands. Unless [`NameAttribute::NoSelect`] is indicated, 92 | /// the name is also valid as an argument for commands, such as `SELECT`, that accept mailbox 93 | /// names. 94 | pub fn name(&self) -> &str { 95 | &self.name 96 | } 97 | 98 | /// Get an owned version of this [`Name`]. 99 | pub fn into_owned(self) -> Name<'static> { 100 | Name { 101 | attributes: self 102 | .attributes 103 | .into_iter() 104 | .map(|av| av.into_owned()) 105 | .collect(), 106 | delimiter: self.delimiter.map(|cow| Cow::Owned(cow.into_owned())), 107 | name: Cow::Owned(self.name.into_owned()), 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/types/quota.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, ParseError}; 2 | use crate::parse::{parse_many_into2, parse_until_done_optional, MapOrNot, MapOrNot2}; 3 | use crate::types::UnsolicitedResponse; 4 | use imap_proto::Response; 5 | use ouroboros::self_referencing; 6 | use std::borrow::Cow; 7 | use std::collections::VecDeque; 8 | use std::fmt::{Debug, Display, Formatter}; 9 | 10 | /// From [SETQUOTA Resource limit](https://datatracker.ietf.org/doc/html/rfc2087#section-4.1) 11 | /// 12 | /// Used by [`Session::set_quota`](crate::Session::set_quota). 13 | #[derive(Clone, Debug, Eq, PartialEq)] 14 | #[non_exhaustive] 15 | pub struct QuotaResourceLimit<'a> { 16 | /// The resource type 17 | pub name: QuotaResourceName<'a>, 18 | /// The amount for that resource 19 | pub amount: u64, 20 | } 21 | 22 | impl<'a> QuotaResourceLimit<'a> { 23 | /// Creates a new [`QuotaResourceLimit`] 24 | pub fn new(name: impl Into>, amount: u64) -> Self { 25 | let name = name.into(); 26 | Self { name, amount } 27 | } 28 | } 29 | 30 | impl Display for QuotaResourceLimit<'_> { 31 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 32 | write!(f, "{} {}", self.name, self.amount) 33 | } 34 | } 35 | 36 | /// From [Resources](https://datatracker.ietf.org/doc/html/rfc2087#section-3) 37 | /// 38 | /// Used by [`QuotaResourceLimit`], and [`QuotaResource`] 39 | #[derive(Debug, Eq, PartialEq, Clone)] 40 | #[non_exhaustive] 41 | pub enum QuotaResourceName<'a> { 42 | /// Sum of messages' RFC822.SIZE, in units of 1024 octets 43 | Storage, 44 | /// Number of messages 45 | Message, 46 | /// Any other string (for future RFCs) 47 | Atom(Cow<'a, str>), 48 | } 49 | 50 | impl<'a> From<&'a str> for QuotaResourceName<'a> { 51 | fn from(input: &'a str) -> Self { 52 | match input { 53 | "STORAGE" => QuotaResourceName::Storage, 54 | "MESSAGE" => QuotaResourceName::Message, 55 | _ => QuotaResourceName::Atom(Cow::from(input)), 56 | } 57 | } 58 | } 59 | 60 | impl From for QuotaResourceName<'_> { 61 | fn from(input: String) -> Self { 62 | match input.as_str() { 63 | "STORAGE" => QuotaResourceName::Storage, 64 | "MESSAGE" => QuotaResourceName::Message, 65 | _ => QuotaResourceName::Atom(Cow::from(input)), 66 | } 67 | } 68 | } 69 | 70 | impl Display for QuotaResourceName<'_> { 71 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 72 | match self { 73 | Self::Storage => write!(f, "STORAGE"), 74 | Self::Message => write!(f, "MESSAGE"), 75 | Self::Atom(s) => write!(f, "{}", s), 76 | } 77 | } 78 | } 79 | 80 | impl<'a> QuotaResourceName<'a> { 81 | /// Get an owned version of the [`QuotaResourceName`]. 82 | pub fn into_owned(self) -> QuotaResourceName<'static> { 83 | match self { 84 | QuotaResourceName::Storage => QuotaResourceName::Storage, 85 | QuotaResourceName::Message => QuotaResourceName::Message, 86 | QuotaResourceName::Atom(n) => QuotaResourceName::Atom(Cow::Owned(n.into_owned())), 87 | } 88 | } 89 | } 90 | 91 | /// From [QUOTA Response](https://datatracker.ietf.org/doc/html/rfc2087#section-5.1) 92 | /// 93 | /// This is a wrapper around a single single [`Quota`]. 94 | /// 95 | /// Used by [`Session::get_quota`] and [`Session::set_quota`] 96 | #[self_referencing] 97 | pub struct QuotaResponse { 98 | data: Vec, 99 | #[borrows(data)] 100 | #[covariant] 101 | pub(crate) quota: Option>, 102 | } 103 | 104 | impl QuotaResponse { 105 | /// Parse the [`Quota`] response from a response buffer. 106 | pub(crate) fn parse( 107 | owned: Vec, 108 | unsolicited: &mut VecDeque, 109 | ) -> Result { 110 | QuotaResponseTryBuilder { 111 | data: owned, 112 | quota_builder: |input| { 113 | // There should zero or one QUOTA response 114 | parse_until_done_optional(input, unsolicited, |response| match response { 115 | Response::Quota(q) => Ok(MapOrNot::Map(Quota::from_imap_proto(q))), 116 | resp => Ok(MapOrNot::Not(resp)), 117 | }) 118 | }, 119 | } 120 | .try_build() 121 | } 122 | 123 | /// Access to the wrapped optional [`Quota`] struct 124 | pub fn parsed(&self) -> &Option> { 125 | self.borrow_quota() 126 | } 127 | } 128 | 129 | /// From [QUOTA Response](https://datatracker.ietf.org/doc/html/rfc2087#section-5.1) 130 | /// 131 | /// Used by [`QuotaResponse`] and [`QuotaRootResponse`] 132 | #[derive(Debug, Eq, PartialEq)] 133 | #[non_exhaustive] 134 | pub struct Quota<'a> { 135 | /// The quota root name 136 | pub root_name: Cow<'a, str>, 137 | /// The defined resources with their usage and limits (could be empty) 138 | pub resources: Vec>, 139 | } 140 | 141 | impl<'a> Quota<'a> { 142 | fn from_imap_proto(q: imap_proto::Quota<'a>) -> Self { 143 | Self { 144 | root_name: q.root_name, 145 | resources: q 146 | .resources 147 | .into_iter() 148 | .map(|e| QuotaResource { 149 | name: match e.name { 150 | imap_proto::QuotaResourceName::Storage => QuotaResourceName::Storage, 151 | imap_proto::QuotaResourceName::Message => QuotaResourceName::Message, 152 | imap_proto::QuotaResourceName::Atom(e) => QuotaResourceName::Atom(e), 153 | }, 154 | usage: e.usage, 155 | limit: e.limit, 156 | }) 157 | .collect(), 158 | } 159 | } 160 | } 161 | 162 | /// From [QUOTA Response](https://datatracker.ietf.org/doc/html/rfc2087#section-5.1) 163 | /// 164 | /// The quota resource sub-pieces in a [`Quota`] 165 | #[derive(Clone, Debug, Eq, PartialEq)] 166 | #[non_exhaustive] 167 | pub struct QuotaResource<'a> { 168 | /// The resource type 169 | pub name: QuotaResourceName<'a>, 170 | /// current usage of the resource 171 | pub usage: u64, 172 | /// resource limit 173 | pub limit: u64, 174 | } 175 | 176 | /// From [QUOTAROOT Response](https://datatracker.ietf.org/doc/html/rfc2087#section-5.2) 177 | /// 178 | /// Used by [`Session::get_quota_root`] 179 | #[self_referencing] 180 | pub struct QuotaRootResponse { 181 | data: Vec, 182 | #[borrows(data)] 183 | #[covariant] 184 | pub(crate) inner: InnerQuotaRootResponse<'this>, 185 | } 186 | 187 | impl Debug for QuotaRootResponse { 188 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 189 | write!(f, "{:?}", self.borrow_inner()) 190 | } 191 | } 192 | /// Inner struct to manage storing the references for ouroboros 193 | #[derive(Debug)] 194 | pub(crate) struct InnerQuotaRootResponse<'a> { 195 | pub(crate) quota_root: imap_proto::QuotaRoot<'a>, 196 | pub(crate) quotas: Vec>, 197 | } 198 | 199 | impl QuotaRootResponse { 200 | /// Parse the [`QuotaRoot`] response from a response buffer. 201 | pub(crate) fn parse( 202 | owned: Vec, 203 | unsolicited: &mut VecDeque, 204 | ) -> Result { 205 | QuotaRootResponseTryBuilder { 206 | data: owned, 207 | inner_builder: |input| { 208 | let mut quota_roots = Vec::new(); 209 | let mut quotas = Vec::new(); 210 | 211 | parse_many_into2( 212 | input, 213 | &mut quota_roots, 214 | &mut quotas, 215 | unsolicited, 216 | |response| match response { 217 | Response::QuotaRoot(q) => Ok(MapOrNot2::Map1(q)), 218 | Response::Quota(q) => Ok(MapOrNot2::Map2(Quota::from_imap_proto(q))), 219 | resp => Ok(MapOrNot2::Not(resp)), 220 | }, 221 | )?; 222 | 223 | match quota_roots.len() { 224 | 1 => Ok(InnerQuotaRootResponse { 225 | quota_root: quota_roots.remove(0), 226 | quotas, 227 | }), 228 | _ => Err(Error::Parse(ParseError::Invalid(input.to_vec()))), 229 | } 230 | }, 231 | } 232 | .try_build() 233 | } 234 | 235 | /// The mailbox name 236 | pub fn mailbox_name(&self) -> &str { 237 | &self.borrow_inner().quota_root.mailbox_name 238 | } 239 | 240 | /// The list of quota roots for the mailbox name (could be empty) 241 | pub fn quota_root_names(&self) -> impl Iterator { 242 | self.borrow_inner() 243 | .quota_root 244 | .quota_root_names 245 | .iter() 246 | .map(|e| e.as_ref()) 247 | } 248 | 249 | /// The set of quotas for each named quota root (could be empty) 250 | pub fn quotas(&self) -> &[Quota<'_>] { 251 | &self.borrow_inner().quotas[..] 252 | } 253 | } 254 | 255 | #[cfg(test)] 256 | mod tests { 257 | use super::*; 258 | 259 | #[test] 260 | fn test_quota_resource_name_into_owned() { 261 | let name = "TEST"; 262 | let borrowed = QuotaResourceName::Atom(Cow::Borrowed(name)); 263 | 264 | let new_owned = borrowed.into_owned(); 265 | assert!(matches!(new_owned, QuotaResourceName::Atom(Cow::Owned(_)))); 266 | } 267 | 268 | #[test] 269 | fn test_quota_resource_name_from_str() { 270 | let name = "STORAGE"; 271 | 272 | let name: QuotaResourceName<'_> = name.into(); 273 | 274 | assert!(matches!(name, QuotaResourceName::Storage)); 275 | } 276 | 277 | #[test] 278 | fn test_quota_resource_name_from_string() { 279 | let name = "STORAGE".to_string(); 280 | 281 | let name: QuotaResourceName<'_> = name.into(); 282 | 283 | assert!(matches!(name, QuotaResourceName::Storage)); 284 | } 285 | 286 | #[test] 287 | fn test_quota_resource_limit_new() { 288 | let limit = QuotaResourceLimit::new("STORAGE", 1000); 289 | 290 | assert_eq!(limit.name, QuotaResourceName::Storage); 291 | assert_eq!(limit.amount, 1000); 292 | } 293 | 294 | #[test] 295 | fn test_quota_resource_limit_new_custom() { 296 | let name = "X-NUM-FOLDERS"; 297 | 298 | let limit = QuotaResourceLimit::new(name, 50); 299 | 300 | assert!(matches!( 301 | limit.name, 302 | QuotaResourceName::Atom(x) if x == Cow::from("X-NUM-FOLDERS") 303 | )); 304 | assert_eq!(limit.amount, 50); 305 | } 306 | #[test] 307 | fn test_quota_resource_limit_new_from_string() { 308 | let name = "STORAGE".to_string(); 309 | 310 | // use a function to for use of a dropped string 311 | fn make_limit(name: String) -> QuotaResourceLimit<'static> { 312 | QuotaResourceLimit::new(name, 1000) 313 | } 314 | 315 | let limit = make_limit(name); 316 | 317 | assert_eq!(limit.name, QuotaResourceName::Storage); 318 | assert_eq!(limit.amount, 1000); 319 | } 320 | 321 | #[test] 322 | fn test_quota_resource_limit_new_custom_from_string() { 323 | let name = "X-NUM-FOLDERS".to_string(); 324 | 325 | // use a function to for use of a dropped string 326 | fn make_limit(name: String) -> QuotaResourceLimit<'static> { 327 | QuotaResourceLimit::new(name, 50) 328 | } 329 | 330 | let limit = make_limit(name); 331 | 332 | assert!(matches!( 333 | limit.name, 334 | QuotaResourceName::Atom(x) if x == Cow::from("X-NUM-FOLDERS") 335 | )); 336 | assert_eq!(limit.amount, 50); 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/types/unsolicited_response.rs: -------------------------------------------------------------------------------- 1 | use super::{Flag, Seq}; 2 | 3 | /// re-exported from imap_proto; 4 | pub use imap_proto::AttributeValue; 5 | pub use imap_proto::ResponseCode; 6 | pub use imap_proto::StatusAttribute; 7 | use imap_proto::{MailboxDatum, Response, Status}; 8 | 9 | /// Responses that the server sends that are not related to the current command. 10 | /// [RFC 3501](https://tools.ietf.org/html/rfc3501#section-7) states that clients need to be able 11 | /// to accept any response at any time. 12 | /// 13 | /// Not all possible responses are explicitly enumerated here because in practice only 14 | /// some types of responses are delivered as unsolicited responses. If you encounter an 15 | /// unsolicited response in the wild that is not handled here, please 16 | /// [open an issue](https://github.com/jonhoo/rust-imap/issues) and let us know! 17 | /// 18 | /// Note that `Recent`, `Exists` and `Expunge` responses refer to the currently `SELECT`ed folder, 19 | /// so the user must take care when interpreting these. 20 | #[derive(Debug, PartialEq, Eq)] 21 | #[non_exhaustive] 22 | pub enum UnsolicitedResponse { 23 | /// An unsolicited `BYE` response. 24 | /// 25 | /// The `BYE` response may have an optional `ResponseCode` that provides additional 26 | /// information, per [RFC3501](https://tools.ietf.org/html/rfc3501#section-7.1.5). 27 | Bye { 28 | /// Optional response code. 29 | code: Option>, 30 | /// Information text that may be presented to the user. 31 | information: Option, 32 | }, 33 | 34 | /// An unsolicited [`EXISTS` response](https://tools.ietf.org/html/rfc3501#section-7.3.1) that 35 | /// reports the number of messages in the mailbox. This response occurs if the size of the 36 | /// mailbox changes (e.g., new messages arrive). 37 | Exists(u32), 38 | 39 | /// An unsolicited [`EXPUNGE` response](https://tools.ietf.org/html/rfc3501#section-7.4.1) that 40 | /// reports that the specified message sequence number has been permanently removed from the 41 | /// mailbox. The message sequence number for each successive message in the mailbox is 42 | /// immediately decremented by 1, and this decrement is reflected in message sequence numbers 43 | /// in subsequent responses (including other untagged `EXPUNGE` responses). 44 | /// 45 | /// The EXPUNGE response also decrements the number of messages in the mailbox; it is not 46 | /// necessary to send an `EXISTS` response with the new value. 47 | /// 48 | /// As a result of the immediate decrement rule, message sequence numbers that appear in a set 49 | /// of successive `EXPUNGE` responses depend upon whether the messages are removed starting 50 | /// from lower numbers to higher numbers, or from higher numbers to lower numbers. For 51 | /// example, if the last 5 messages in a 9-message mailbox are expunged, a "lower to higher" 52 | /// server will send five untagged `EXPUNGE` responses for message sequence number 5, whereas a 53 | /// "higher to lower server" will send successive untagged `EXPUNGE` responses for message 54 | /// sequence numbers 9, 8, 7, 6, and 5. 55 | // TODO: the spec doesn't seem to say anything about when these may be received as unsolicited? 56 | Expunge(Seq), 57 | 58 | /// An unsolicited `FETCH` response. 59 | /// 60 | /// The server may unilaterally send `FETCH` responses, as described in 61 | /// [RFC3501](https://tools.ietf.org/html/rfc3501#section-7.4.2). 62 | Fetch { 63 | /// Message identifier. 64 | id: u32, 65 | /// Attribute values for this message. 66 | attributes: Vec>, 67 | }, 68 | 69 | /// An unsolicited [`FLAGS` response](https://tools.ietf.org/html/rfc3501#section-7.2.6) that 70 | /// identifies the flags (at a minimum, the system-defined flags) that are applicable in the 71 | /// mailbox. Flags other than the system flags can also exist, depending on server 72 | /// implementation. 73 | /// 74 | /// See [`Flag`] for details. 75 | // TODO: the spec doesn't seem to say anything about when these may be received as unsolicited? 76 | Flags(Vec>), 77 | 78 | /// An unsolicited [METADATA response](https://tools.ietf.org/html/rfc5464#section-4.4.2) 79 | /// that reports a change in a server or mailbox annotation. 80 | Metadata { 81 | /// Mailbox name for which annotations were changed. 82 | mailbox: String, 83 | /// List of annotations that were changed. 84 | metadata_entries: Vec, 85 | }, 86 | 87 | /// An unsolicited `OK` response. 88 | /// 89 | /// The `OK` response may have an optional `ResponseCode` that provides additional 90 | /// information, per [RFC3501](https://tools.ietf.org/html/rfc3501#section-7.1.1). 91 | Ok { 92 | /// Optional response code. 93 | code: Option>, 94 | /// Information text that may be presented to the user. 95 | information: Option, 96 | }, 97 | 98 | /// An unsolicited [`RECENT` response](https://tools.ietf.org/html/rfc3501#section-7.3.2) 99 | /// indicating the number of messages with the `\Recent` flag set. This response occurs if the 100 | /// size of the mailbox changes (e.g., new messages arrive). 101 | /// 102 | /// > Note: It is not guaranteed that the message sequence 103 | /// > numbers of recent messages will be a contiguous range of 104 | /// > the highest n messages in the mailbox (where n is the 105 | /// > value reported by the `RECENT` response). Examples of 106 | /// > situations in which this is not the case are: multiple 107 | /// > clients having the same mailbox open (the first session 108 | /// > to be notified will see it as recent, others will 109 | /// > probably see it as non-recent), and when the mailbox is 110 | /// > re-ordered by a non-IMAP agent. 111 | /// > 112 | /// > The only reliable way to identify recent messages is to 113 | /// > look at message flags to see which have the `\Recent` flag 114 | /// > set, or to do a `SEARCH RECENT`. 115 | Recent(u32), 116 | 117 | /// An unsolicited [`STATUS response`](https://tools.ietf.org/html/rfc3501#section-7.2.4). 118 | Status { 119 | /// The mailbox that this status response is for. 120 | mailbox: String, 121 | /// The attributes of this mailbox. 122 | attributes: Vec, 123 | }, 124 | 125 | /// An unsolicited [`VANISHED` response](https://tools.ietf.org/html/rfc7162#section-3.2.10) 126 | /// that reports a sequence-set of `UID`s that have been expunged from the mailbox. 127 | /// 128 | /// The `VANISHED` response is similar to the `EXPUNGE` response and can be sent wherever 129 | /// an `EXPUNGE` response can be sent. It can only be sent by the server if the client 130 | /// has enabled [`QRESYNC`](https://tools.ietf.org/html/rfc7162). 131 | /// 132 | /// The `VANISHED` response has two forms, one with the `EARLIER` tag which is used to 133 | /// respond to a `UID FETCH` or `SELECT/EXAMINE` command, and one without an `EARLIER` 134 | /// tag, which is used to announce removals within an already selected mailbox. 135 | /// 136 | /// If using `QRESYNC`, the client can fetch new, updated and deleted `UID`s in a 137 | /// single round trip by including the `(CHANGEDSINCE VANISHED)` 138 | /// modifier to the `UID SEARCH` command, as described in 139 | /// [RFC7162](https://tools.ietf.org/html/rfc7162#section-3.1.4). For example 140 | /// `UID FETCH 1:* (UID FLAGS) (CHANGEDSINCE 1234 VANISHED)` would return `FETCH` 141 | /// results for all `UID`s added or modified since `MODSEQ` `1234`. Deleted `UID`s 142 | /// will be present as a `VANISHED` response in the `Session::unsolicited_responses` 143 | /// channel. 144 | Vanished { 145 | /// Whether the `EARLIER` tag was set on the response 146 | earlier: bool, 147 | /// The list of `UID`s which have been removed 148 | uids: Vec>, 149 | }, 150 | } 151 | 152 | /// Try to convert from a `imap_proto::Response`. 153 | /// 154 | /// Not all `Response` variants are supported - only those which 155 | /// are known or likely to be sent by a server as a unilateral response 156 | /// during normal operations or during an IDLE session are implemented. 157 | /// 158 | /// If the conversion fails, the input `Response` is returned. 159 | impl<'a> TryFrom> for UnsolicitedResponse { 160 | type Error = Response<'a>; 161 | 162 | fn try_from(response: Response<'a>) -> Result { 163 | match response { 164 | Response::Data { 165 | status: Status::Bye, 166 | code, 167 | information, 168 | } => Ok(UnsolicitedResponse::Bye { 169 | code: code.map(|c| c.into_owned()), 170 | information: information.map(|s| s.to_string()), 171 | }), 172 | Response::Data { 173 | status: Status::Ok, 174 | code, 175 | information, 176 | } => Ok(UnsolicitedResponse::Ok { 177 | code: code.map(|c| c.into_owned()), 178 | information: information.map(|s| s.to_string()), 179 | }), 180 | Response::Expunge(n) => Ok(UnsolicitedResponse::Expunge(n)), 181 | Response::Fetch(id, attributes) => Ok(UnsolicitedResponse::Fetch { 182 | id, 183 | attributes: attributes.into_iter().map(|a| a.into_owned()).collect(), 184 | }), 185 | Response::MailboxData(MailboxDatum::Exists(n)) => Ok(UnsolicitedResponse::Exists(n)), 186 | Response::MailboxData(MailboxDatum::Flags(flags)) => { 187 | Ok(UnsolicitedResponse::Flags(Flag::from_strs(flags).collect())) 188 | } 189 | Response::MailboxData(MailboxDatum::MetadataUnsolicited { mailbox, values }) => { 190 | Ok(UnsolicitedResponse::Metadata { 191 | mailbox: mailbox.to_string(), 192 | metadata_entries: values.iter().map(|s| s.to_string()).collect(), 193 | }) 194 | } 195 | Response::MailboxData(MailboxDatum::Recent(n)) => Ok(UnsolicitedResponse::Recent(n)), 196 | Response::MailboxData(MailboxDatum::Status { mailbox, status }) => { 197 | Ok(UnsolicitedResponse::Status { 198 | mailbox: mailbox.into(), 199 | attributes: status, 200 | }) 201 | } 202 | Response::Vanished { earlier, uids } => { 203 | Ok(UnsolicitedResponse::Vanished { earlier, uids }) 204 | } 205 | _ => Err(response), 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | /// Lovingly borrowed from the cargo crate 2 | /// 3 | /// Joins an iterator of [std::fmt::Display]'ables into an output writable 4 | pub(crate) fn iter_join_onto(mut w: W, iter: I, delim: &str) -> std::fmt::Result 5 | where 6 | W: std::fmt::Write, 7 | I: IntoIterator, 8 | T: std::fmt::Display, 9 | { 10 | let mut it = iter.into_iter().peekable(); 11 | while let Some(n) = it.next() { 12 | write!(w, "{}", n)?; 13 | if it.peek().is_some() { 14 | write!(w, "{}", delim)?; 15 | } 16 | } 17 | Ok(()) 18 | } 19 | 20 | /// Lovingly borrowed from the cargo crate 21 | /// 22 | /// Joins an iterator of [std::fmt::Display]'ables to a new [std::string::String]. 23 | pub(crate) fn iter_join(iter: I, delim: &str) -> String 24 | where 25 | I: IntoIterator, 26 | T: std::fmt::Display, 27 | { 28 | let mut s = String::new(); 29 | let _ = iter_join_onto(&mut s, iter, delim); 30 | s 31 | } 32 | -------------------------------------------------------------------------------- /tests/builder_integration.rs: -------------------------------------------------------------------------------- 1 | extern crate imap; 2 | 3 | use imap::ConnectionMode; 4 | 5 | fn test_host() -> String { 6 | std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string()) 7 | } 8 | 9 | fn test_imap_port() -> u16 { 10 | std::env::var("TEST_IMAP_PORT") 11 | .unwrap_or("3143".to_string()) 12 | .parse() 13 | .unwrap_or(3143) 14 | } 15 | 16 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 17 | fn test_imaps_port() -> u16 { 18 | std::env::var("TEST_IMAPS_PORT") 19 | .unwrap_or("3993".to_string()) 20 | .parse() 21 | .unwrap_or(3993) 22 | } 23 | 24 | fn list_mailbox(session: &mut imap::Session) -> Result<(), imap::Error> { 25 | session.select("INBOX")?; 26 | session.search("ALL")?; 27 | Ok(()) 28 | } 29 | 30 | #[cfg(all( 31 | any(feature = "native-tls", feature = "rustls-tls"), 32 | feature = "test-full-imap" 33 | ))] 34 | #[test] 35 | fn starttls_force() { 36 | let user = "starttls@localhost"; 37 | let host = test_host(); 38 | let c = imap::ClientBuilder::new(&host, test_imap_port()) 39 | .danger_skip_tls_verify(true) 40 | .mode(ConnectionMode::StartTls) 41 | .connect() 42 | .unwrap(); 43 | let mut s = c.login(user, user).unwrap(); 44 | s.debug = true; 45 | assert!(list_mailbox(&mut s).is_ok()); 46 | } 47 | 48 | #[cfg(all( 49 | any(feature = "native-tls", feature = "rustls-tls"), 50 | feature = "test-full-imap" 51 | ))] 52 | #[test] 53 | fn tls_force() { 54 | let user = "tls@localhost"; 55 | let host = test_host(); 56 | let c = imap::ClientBuilder::new(&host, test_imaps_port()) 57 | .danger_skip_tls_verify(true) 58 | .mode(ConnectionMode::Tls) 59 | .connect() 60 | .unwrap(); 61 | let mut s = c.login(user, user).unwrap(); 62 | s.debug = true; 63 | assert!(list_mailbox(&mut s).is_ok()); 64 | } 65 | 66 | #[cfg(feature = "rustls-tls")] 67 | #[test] 68 | fn tls_force_rustls() { 69 | let user = "tls@localhost"; 70 | let host = test_host(); 71 | let c = imap::ClientBuilder::new(&host, test_imaps_port()) 72 | .danger_skip_tls_verify(true) 73 | .tls_kind(imap::TlsKind::Rust) 74 | .mode(ConnectionMode::Tls) 75 | .connect() 76 | .unwrap(); 77 | let mut s = c.login(user, user).unwrap(); 78 | s.debug = true; 79 | assert!(list_mailbox(&mut s).is_ok()); 80 | } 81 | 82 | #[cfg(feature = "native-tls")] 83 | #[test] 84 | fn tls_force_native() { 85 | let user = "tls@localhost"; 86 | let host = test_host(); 87 | let c = imap::ClientBuilder::new(&host, test_imaps_port()) 88 | .danger_skip_tls_verify(true) 89 | .tls_kind(imap::TlsKind::Native) 90 | .mode(ConnectionMode::Tls) 91 | .connect() 92 | .unwrap(); 93 | let mut s = c.login(user, user).unwrap(); 94 | s.debug = true; 95 | assert!(list_mailbox(&mut s).is_ok()); 96 | } 97 | 98 | #[test] 99 | #[cfg(all( 100 | feature = "test-full-imap", 101 | any(feature = "native-tls", feature = "rustls-tls") 102 | ))] 103 | fn auto_tls() { 104 | let user = "auto@localhost"; 105 | let host = test_host(); 106 | let builder = imap::ClientBuilder::new(&host, test_imap_port()).danger_skip_tls_verify(true); 107 | 108 | let c = builder.connect().unwrap(); 109 | let mut s = c.login(user, user).unwrap(); 110 | s.debug = true; 111 | assert!(list_mailbox(&mut s).is_ok()); 112 | } 113 | 114 | #[test] 115 | fn auto() { 116 | let user = "auto@localhost"; 117 | let host = test_host(); 118 | let builder = imap::ClientBuilder::new(&host, test_imap_port()).mode(ConnectionMode::Auto); 119 | #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] 120 | let builder = builder.danger_skip_tls_verify(true); 121 | 122 | let c = builder.connect().unwrap(); 123 | let mut s = c.login(user, user).unwrap(); 124 | s.debug = true; 125 | assert!(list_mailbox(&mut s).is_ok()); 126 | } 127 | 128 | #[test] 129 | fn raw_force() { 130 | let user = "raw@localhost"; 131 | let host = test_host(); 132 | let c = imap::ClientBuilder::new(&host, test_imap_port()) 133 | .mode(ConnectionMode::Plaintext) 134 | .connect() 135 | .unwrap(); 136 | let mut s = c.login(user, user).unwrap(); 137 | s.debug = true; 138 | assert!(list_mailbox(&mut s).is_ok()); 139 | } 140 | --------------------------------------------------------------------------------