├── .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 | [](https://crates.io/crates/imap)
4 | [](https://docs.rs/imap/)
5 | [](https://crates.io/crates/imap)
6 | [](https://codecov.io/gh/jonhoo/rust-imap)
7 | [](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 |
--------------------------------------------------------------------------------