├── .cirrus.yml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── Cargo.toml ├── README.md └── src │ └── bin │ ├── basic.rs │ ├── futures.rs │ ├── gmail_oauth2.rs │ ├── idle.rs │ └── integration.rs ├── rustfmt.toml └── src ├── authenticator.rs ├── client.rs ├── error.rs ├── extensions ├── compress.rs ├── id.rs ├── idle.rs ├── mod.rs └── quota.rs ├── imap_stream.rs ├── lib.rs ├── mock_stream.rs ├── parse.rs └── types ├── capabilities.rs ├── fetch.rs ├── id_generator.rs ├── mailbox.rs ├── mod.rs ├── name.rs ├── quota.rs ├── request.rs └── response_data.rs /.cirrus.yml: -------------------------------------------------------------------------------- 1 | task: 2 | name: stable-x86_64-unknown-freebsd 3 | freebsd_instance: 4 | matrix: 5 | - image: freebsd-12-0-release-amd64 6 | - image: freebsd-11-2-release-amd64 7 | env: 8 | RUST_BACKTRACE: 1 9 | setup_script: 10 | - pkg install -y curl git 11 | - curl https://sh.rustup.rs -sSf --output rustup.sh 12 | - sh rustup.sh -y 13 | - . $HOME/.cargo/env 14 | check_script: 15 | - . $HOME/.cargo/env 16 | - cargo check --all-targets 17 | build_script: 18 | - . $HOME/.cargo/env 19 | - cargo build --all-targets --verbose 20 | test_script: 21 | - . $HOME/.cargo/env 22 | - cargo test --examples 23 | - cargo test --doc 24 | - cargo test --lib 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | env: 8 | RUSTFLAGS: -Dwarnings 9 | 10 | jobs: 11 | build_and_test: 12 | name: Build and test 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, windows-latest, macOS-latest] 17 | rust: [nightly, beta, stable] 18 | 19 | steps: 20 | - uses: actions/checkout@master 21 | 22 | - name: Install ${{ matrix.rust }} 23 | uses: actions-rs/toolchain@v1 24 | with: 25 | toolchain: ${{ matrix.rust }} 26 | override: true 27 | 28 | - name: check async-std 29 | run: cargo check --workspace --all-targets --no-default-features --features runtime-async-std 30 | 31 | - name: check tokio 32 | run: cargo check --workspace --all-targets --no-default-features --features runtime-tokio 33 | 34 | - name: check compress feature with tokio 35 | run: cargo check --workspace --all-targets --no-default-features --features runtime-tokio,compress 36 | 37 | - name: check compress feature with async-std 38 | run: cargo check --workspace --all-targets --no-default-features --features runtime-async-std,compress 39 | 40 | - name: check async-std examples 41 | working-directory: examples 42 | run: cargo check --workspace --all-targets --no-default-features --features runtime-async-std 43 | 44 | - name: check tokio examples 45 | working-directory: examples 46 | run: cargo check --workspace --all-targets --no-default-features --features runtime-tokio 47 | 48 | - name: tests 49 | run: cargo test --workspace 50 | 51 | check_fmt_and_docs: 52 | name: Checking fmt and docs 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@master 56 | 57 | - uses: actions-rs/toolchain@v1 58 | with: 59 | profile: minimal 60 | toolchain: nightly 61 | override: true 62 | components: rustfmt 63 | 64 | - name: fmt 65 | run: cargo fmt --all -- --check 66 | 67 | - name: Docs 68 | run: cargo doc 69 | 70 | clippy_check: 71 | name: Clippy check 72 | runs-on: ubuntu-latest 73 | steps: 74 | - uses: actions/checkout@v1 75 | - name: Install rust 76 | run: rustup update beta && rustup default beta 77 | - name: Install clippy 78 | run: rustup component add clippy 79 | - name: clippy 80 | run: cargo clippy --all 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.10.4] - 2025-04-07 9 | 10 | - Remove unused `once_cell` dependency. 11 | - Update base64 from 0.21 to 0.22.1. 12 | - Use write_all() to write space after a tag. 13 | 14 | ## [0.10.3] - 2025-03-10 15 | 16 | - Don't panic if the server returns 0 as the quota limit. [#116](https://github.com/chatmail/async-imap/pull/116) 17 | 18 | ## [0.10.2] - 2024-10-14 19 | 20 | - Implement IMAP COMPRESS 21 | 22 | ## [0.10.1] - 2024-09-09 23 | 24 | - Correctly parse unsolicited APPEND responses 25 | 26 | ## [0.10.0] - 2024-08-30 27 | 28 | - Support optional arguments for APPEND command. [#107](https://github.com/async-email/async-imap/pull/107) 29 | - Print unparseable input as a string. [#106](https://github.com/async-email/async-imap/pull/106) 30 | 31 | ## [0.9.7] - 2023-01-30 32 | 33 | - Fix parsing of METADATA results with NIL values. [#103](https://github.com/async-email/async-imap/pull/103) 34 | 35 | ## [0.9.6] - 2023-01-27 36 | 37 | ### Changes 38 | 39 | - Add GETMETADATA command. [#102](https://github.com/async-email/async-imap/pull/102) 40 | 41 | ## [0.9.5] - 2023-12-11 42 | 43 | ### Fixes 44 | 45 | - Reset IDLE timeout when keepalive is received 46 | 47 | ## [0.9.4] - 2023-11-15 48 | 49 | ### Fixes 50 | 51 | - Do not ignore EOF errors when reading FETCH responses. [#94](https://github.com/async-email/async-imap/pull/94) 52 | 53 | ## [0.9.3] - 2023-10-20 54 | 55 | ### Dependencies 56 | 57 | - Update `async-channel` to version 2. 58 | 59 | ## [0.9.2] - 2023-10-20 60 | 61 | ### Fixes 62 | 63 | - Fix STATUS command response parsing. [#92](https://github.com/async-email/async-imap/pull/92) 64 | 65 | ## [0.9.1] - 2023-08-28 66 | 67 | ### Fixes 68 | 69 | - Replace byte pool with bytes to fix memory leak. [#79](https://github.com/async-email/async-imap/pull/79) 70 | 71 | ### Documentation 72 | 73 | - Remove outdated reference to `rustls.rs` example 74 | 75 | ### Miscellaneous Tasks 76 | 77 | - Fix beta (1.73) clippy 78 | 79 | ## [0.9.0] - 2023-06-13 80 | 81 | ### Fixes 82 | 83 | - Switch from `ouroboros` to `self_cell`. [#86](https://github.com/async-email/async-imap/pull/86) 84 | 85 | `ouroboros` is [no longer maintained](https://github.com/joshua-maros/ouroboros/issues/88) and has a [RUSTSEC-2023-0042 advisory](https://rustsec.org/advisories/RUSTSEC-2023-0042) suggesting switch to [`self_cell`](https://github.com/Voultapher/self_cell). 86 | 87 | ## [0.8.0] - 2023-04-17 88 | 89 | ### Changed 90 | 91 | - Remove `async-native-tls` dependency. TLS streams should be created by the library users as documented in `lib.rs`. #68 92 | - Do not generate artificial "broken pipe" errors when attempting to send a request 93 | after reaching EOF on the response stream. #73 94 | - Do not attempt to track if the stream is closed or not. 95 | `ImapStream` can wrap any kinds of streams, including streams which may become open again later, 96 | like files which can be rewinded after reaching end of file or appended to. 97 | 98 | ### Fixes 99 | 100 | - Update byte-pool to 0.2.4 to fix `ensure_capacity()`. 101 | Previously this bug could have resulted in an attempt to read into a buffer of zero size 102 | and erronous detection of the end of stream. 103 | 104 | ## [0.7.0] - 2023-04-03 105 | 106 | ### Added 107 | 108 | - Added changelog. 109 | - Add `ID` extension support. 110 | 111 | ### Fixed 112 | 113 | - Fix parsing of long responses by correctly setting the `decode_needs` variable. [#74](https://github.com/async-email/async-imap/pull/74). 114 | 115 | ### Changed 116 | 117 | - Make `async-native-tls` dependency optional. 118 | - Update to `base64` 0.21. 119 | 120 | ## [0.6.0] - 2022-06-27 121 | 122 | ### Added 123 | 124 | - Add `QUOTA` support. 125 | - Add `CONDSTORE` support: add `Session.select_condstore()`. 126 | - Full tokio support. 127 | 128 | ### Fixed 129 | 130 | - Do not ignore `SELECT` command errors. 131 | 132 | ### Changed 133 | 134 | - Replace `rental` with `ouroboros`. 135 | - Replace `lazy_static` with `once_cell`. 136 | 137 | ## [0.5.0] - 2021-03-23 138 | 139 | ### Changed 140 | 141 | - Update async-std, stop-token, migrate to stable channels. 142 | 143 | ## [0.4.1] - 2020-10-14 144 | 145 | ### Fixed 146 | 147 | - Fix response handling in authentication. [#36](https://github.com/async-email/async-imap/pull/36) 148 | 149 | ### Changed 150 | 151 | - Update `base64` to 0.13. 152 | 153 | ## [0.3.3] - 2020-08-04 154 | 155 | ### Fixed 156 | 157 | - [Refactor buffering, fixing infinite loop](https://github.com/async-email/async-imap/commit/9a7097dd446784257ad9a546c6f77188e983acd6). [#33](https://github.com/async-email/async-imap/pull/33) 158 | - Updated `byte-pool` from 0.2.1 to 0.2.2 due to important bugfix. 159 | 160 | ### Changed 161 | 162 | - [Do not try to send data when the stream is closed](https://github.com/async-email/async-imap/commit/68f21e5921a002e172d5ffadc45c62bf882a68d6). 163 | 164 | ## [0.3.2] - 2020-06-11 165 | 166 | ### Changed 167 | 168 | - Bump `base64` to 0.12. 169 | 170 | ## [0.3.1] - 2020-05-24 171 | 172 | ### Fixed 173 | 174 | - Ignore unsolicited responses if the channel is full. 175 | 176 | ## [0.3.0] - 2020-05-23 177 | 178 | ### Added 179 | 180 | - Make streams and futures `Send`. 181 | 182 | ## [0.2.0] - 2020-01-04 183 | 184 | ### Added 185 | 186 | - Added tracing logs for traffic. 187 | 188 | ### Fixed 189 | 190 | - Correctly decode incomplete reads of long IMAP messages. 191 | - Avoid infinite loop in decoding. 192 | - Correct response value for manual interrupt in IDLE. 193 | - Handle OAuth responses without challenge. 194 | - Don't crash if we can't read the greeting from the server. 195 | - Improved handling of unsolicited responses and errors. 196 | 197 | ### Changed 198 | 199 | - Use thiserror for error handling. 200 | 201 | ## [0.1.1] - 2019-11-16 202 | 203 | ### Fixed 204 | 205 | - Ensure there is enough space available when encoding. 206 | 207 | ## 0.1.0 - 2019-11-11 208 | 209 | [0.10.4]: https://github.com/async-email/async-imap/compare/v0.10.3...v0.10.4 210 | [0.10.3]: https://github.com/async-email/async-imap/compare/v0.10.2...v0.10.3 211 | [0.10.2]: https://github.com/async-email/async-imap/compare/v0.10.1...v0.10.2 212 | [0.10.1]: https://github.com/async-email/async-imap/compare/v0.10.0...v0.10.1 213 | [0.10.0]: https://github.com/async-email/async-imap/compare/v0.9.7...v0.10.0 214 | [0.9.7]: https://github.com/async-email/async-imap/compare/v0.9.6...v0.9.7 215 | [0.9.6]: https://github.com/async-email/async-imap/compare/v0.9.5...v0.9.6 216 | [0.9.5]: https://github.com/async-email/async-imap/compare/v0.9.4...v0.9.5 217 | [0.9.4]: https://github.com/async-email/async-imap/compare/v0.9.3...v0.9.4 218 | [0.9.3]: https://github.com/async-email/async-imap/compare/v0.9.2...v0.9.3 219 | [0.9.2]: https://github.com/async-email/async-imap/compare/v0.9.1...v0.9.2 220 | [0.9.1]: https://github.com/async-email/async-imap/compare/v0.9.0...v0.9.1 221 | [0.9.0]: https://github.com/async-email/async-imap/compare/v0.8.0...v0.9.0 222 | [0.8.0]: https://github.com/async-email/async-imap/compare/v0.7.0...v0.8.0 223 | [0.7.0]: https://github.com/async-email/async-imap/compare/v0.6.0...v0.7.0 224 | [0.6.0]: https://github.com/async-email/async-imap/compare/v0.5.0...v0.6.0 225 | [0.5.0]: https://github.com/async-email/async-imap/compare/v0.4.1...v0.5.0 226 | [0.4.1]: https://github.com/async-email/async-imap/compare/v0.4.0...v0.4.1 227 | [0.4.0]: https://github.com/async-email/async-imap/compare/v0.3.3...v0.4.0 228 | [0.3.3]: https://github.com/async-email/async-imap/compare/v0.3.2...v0.3.3 229 | [0.3.2]: https://github.com/async-email/async-imap/compare/v0.3.1...v0.3.2 230 | [0.3.1]: https://github.com/async-email/async-imap/compare/v0.3.0...v0.3.1 231 | [0.3.0]: https://github.com/async-email/async-imap/compare/v0.2.0...v0.3.0 232 | [0.2.0]: https://github.com/async-email/async-imap/compare/v0.1.1...v0.2.0 233 | [0.1.1]: https://github.com/async-email/async-imap/compare/v0.1.0...v0.1.1 234 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async-imap" 3 | version = "0.10.4" 4 | authors = ["dignifiedquire "] 5 | documentation = "https://docs.rs/async-imap/" 6 | repository = "https://github.com/async-email/async-imap" 7 | homepage = "https://github.com/async-email/async-imap" 8 | description = "Async IMAP client for Rust" 9 | readme = "README.md" 10 | license = "MIT OR Apache-2.0" 11 | edition = "2021" 12 | 13 | keywords = ["email", "imap"] 14 | categories = ["email", "network-programming"] 15 | 16 | [badges] 17 | maintenance = { status = "actively-developed" } 18 | is-it-maintained-issue-resolution = { repository = "async-email/async-imap" } 19 | is-it-maintained-open-issues = { repository = "async-email/async-imap" } 20 | 21 | [features] 22 | default = ["runtime-async-std"] 23 | compress = ["async-compression"] 24 | 25 | runtime-async-std = ["async-std", "async-compression?/futures-io"] 26 | runtime-tokio = ["tokio", "async-compression?/tokio"] 27 | 28 | [dependencies] 29 | async-channel = "2.0.0" 30 | async-compression = { version = "0.4.15", default-features = false, features = ["deflate"], optional = true } 31 | async-std = { version = "1.8.0", default-features = false, features = ["std", "unstable"], optional = true } 32 | base64 = "0.22.1" 33 | bytes = "1" 34 | chrono = { version = "0.4", default-features = false, features = ["std"] } 35 | futures = "0.3.15" 36 | imap-proto = "0.16.4" 37 | log = "0.4.8" 38 | nom = "7.0" 39 | pin-project = "1" 40 | pin-utils = "0.1.0-alpha.4" 41 | self_cell = "1.0.1" 42 | stop-token = "0.7" 43 | thiserror = "1.0.9" 44 | tokio = { version = "1", features = ["net", "sync", "time", "io-util"], optional = true } 45 | 46 | [dev-dependencies] 47 | async-std = { version = "1.8.0", features = ["std", "attributes"] } 48 | pretty_assertions = "1.2" 49 | tokio = { version = "1", features = ["rt-multi-thread", "macros"] } 50 | -------------------------------------------------------------------------------- /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) 2019 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 |

async-imap

2 |
3 | 4 | Async implementation of IMAP 5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 | 13 | Crates.io version 15 | 16 | 17 | 18 | Download 20 | 21 | 22 | 23 | docs.rs docs 25 | 26 | 27 | 28 | CI status 30 | 31 |
32 | 33 |
34 |

35 | 36 | API Docs 37 | 38 | | 39 | 40 | Releases 41 | 42 |

43 |
44 | 45 |
46 | 47 | > Based on the great [rust-imap](https://crates.io/crates/imap) library. 48 | 49 | This crate lets you connect to and interact with servers that implement the IMAP protocol ([RFC 50 | 3501](https://tools.ietf.org/html/rfc3501) and various extensions). After authenticating with 51 | the server, IMAP lets you list, fetch, and search for e-mails, as well as monitor mailboxes for 52 | changes. It supports at least the latest three stable Rust releases (possibly even older ones; 53 | check the [CI results](https://travis-ci.com/jonhoo/rust-imap)). 54 | 55 | To connect, use the [`connect`] function. This gives you an unauthenticated [`Client`]. You can 56 | then use [`Client::login`] or [`Client::authenticate`] to perform username/password or 57 | challenge/response authentication respectively. This in turn gives you an authenticated 58 | [`Session`], which lets you access the mailboxes at the server. 59 | 60 | The documentation within this crate borrows heavily from the various RFCs, but should not be 61 | considered a complete reference. If anything is unclear, follow the links to the RFCs embedded 62 | in the documentation for the various types and methods and read the raw text there! 63 | 64 | See the `examples/` directory for examples. 65 | 66 | ## Running the test suite 67 | 68 | To run the integration tests, you need to have [GreenMail 69 | running](https://greenmail-mail-test.github.io/greenmail/#deploy_docker_standalone). The 70 | easiest way to do that is with Docker: 71 | 72 | ```console 73 | $ docker pull greenmail/standalone:1.5.9 74 | $ docker run -t -i -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.5.9 75 | ``` 76 | 77 | ## License 78 | 79 | Licensed under either of 80 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 81 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 82 | at your option. 83 | 84 | ## Contribution 85 | 86 | Unless you explicitly state otherwise, any contribution intentionally submitted 87 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall 88 | be dual licensed as above, without any additional terms or conditions. 89 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async-imap-examples" 3 | version = "0.1.0" 4 | publish = false 5 | authors = ["dignifiedquire "] 6 | license = "Apache-2.0/MIT" 7 | edition = "2018" 8 | 9 | [features] 10 | default = ["runtime-async-std"] 11 | 12 | runtime-async-std = ["async-std", "async-native-tls/runtime-async-std", "async-smtp/runtime-async-std", "async-imap/runtime-async-std"] 13 | runtime-tokio = ["tokio", "async-native-tls/runtime-tokio", "async-smtp/runtime-tokio", "async-imap/runtime-tokio"] 14 | 15 | [dependencies] 16 | anyhow = "1" 17 | async-imap = { path = "../", default-features = false } 18 | async-native-tls = { version = "0.5", default-features = false } 19 | async-smtp = { version = "0.8", default-features = false } 20 | 21 | async-std = { version = "1.12.0", features = ["std", "attributes"], optional = true } 22 | futures = "0.3.28" 23 | tokio = { version = "1", features = ["rt-multi-thread", "macros"], optional = true } 24 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains examples of working with the IMAP client. 4 | 5 | Examples: 6 | 7 | * basic - This is a very basic example of using the client. 8 | 9 | * idle - This is a basic example of how to perform an IMAP IDLE call 10 | and interrupt it based on typing a line into stdin. 11 | 12 | * gmail_oauth2 - This is an example using oauth2 for logging into 13 | gmail via the OAUTH2 mechanism. 14 | 15 | * futures - The basic example, but using the `futures` executor. 16 | -------------------------------------------------------------------------------- /examples/src/bin/basic.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use anyhow::{bail, Result}; 4 | use futures::TryStreamExt; 5 | 6 | #[cfg(feature = "runtime-async-std")] 7 | use async_std::net::TcpStream; 8 | #[cfg(feature = "runtime-tokio")] 9 | use tokio::net::TcpStream; 10 | 11 | #[cfg_attr(feature = "runtime-tokio", tokio::main)] 12 | #[cfg_attr(feature = "runtime-async-std", async_std::main)] 13 | async fn main() -> Result<()> { 14 | let args: Vec = env::args().collect(); 15 | if args.len() != 4 { 16 | eprintln!("need three arguments: imap-server login password"); 17 | bail!("need three arguments"); 18 | } else { 19 | let res = fetch_inbox_top(&args[1], &args[2], &args[3]).await?; 20 | println!("**result:\n{}", res.unwrap()); 21 | Ok(()) 22 | } 23 | } 24 | 25 | async fn fetch_inbox_top(imap_server: &str, login: &str, password: &str) -> Result> { 26 | let imap_addr = (imap_server, 993); 27 | let tcp_stream = TcpStream::connect(imap_addr).await?; 28 | let tls = async_native_tls::TlsConnector::new(); 29 | let tls_stream = tls.connect(imap_server, tcp_stream).await?; 30 | 31 | let client = async_imap::Client::new(tls_stream); 32 | println!("-- connected to {}:{}", imap_addr.0, imap_addr.1); 33 | 34 | // the client we have here is unauthenticated. 35 | // to do anything useful with the e-mails, we need to log in 36 | let mut imap_session = client.login(login, password).await.map_err(|e| e.0)?; 37 | println!("-- logged in a {}", login); 38 | 39 | // we want to fetch the first email in the INBOX mailbox 40 | imap_session.select("INBOX").await?; 41 | println!("-- INBOX selected"); 42 | 43 | // fetch message number 1 in this mailbox, along with its RFC822 field. 44 | // RFC 822 dictates the format of the body of e-mails 45 | let messages_stream = imap_session.fetch("1", "RFC822").await?; 46 | let messages: Vec<_> = messages_stream.try_collect().await?; 47 | let message = if let Some(m) = messages.first() { 48 | m 49 | } else { 50 | return Ok(None); 51 | }; 52 | 53 | // extract the message's body 54 | let body = message.body().expect("message did not have a body!"); 55 | let body = std::str::from_utf8(body) 56 | .expect("message was not valid utf-8") 57 | .to_string(); 58 | println!("-- 1 message received, logging out"); 59 | 60 | // be nice to the server and log out 61 | imap_session.logout().await?; 62 | 63 | Ok(Some(body)) 64 | } 65 | -------------------------------------------------------------------------------- /examples/src/bin/futures.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use anyhow::{bail, Result}; 4 | use futures::TryStreamExt; 5 | 6 | #[cfg(feature = "runtime-async-std")] 7 | use async_std::net::TcpStream; 8 | #[cfg(feature = "runtime-tokio")] 9 | use tokio::net::TcpStream; 10 | 11 | fn main() -> Result<()> { 12 | futures::executor::block_on(async { 13 | let args: Vec = env::args().collect(); 14 | if args.len() != 4 { 15 | eprintln!("need three arguments: imap-server login password"); 16 | bail!("need three arguments"); 17 | } else { 18 | let res = fetch_inbox_top(&args[1], &args[2], &args[3]).await?; 19 | println!("**result:\n{}", res.unwrap()); 20 | Ok(()) 21 | } 22 | }) 23 | } 24 | 25 | async fn fetch_inbox_top(imap_server: &str, login: &str, password: &str) -> Result> { 26 | let imap_addr = (imap_server, 993); 27 | let tcp_stream = TcpStream::connect(imap_addr).await?; 28 | let tls = async_native_tls::TlsConnector::new(); 29 | let tls_stream = tls.connect(imap_server, tcp_stream).await?; 30 | 31 | let client = async_imap::Client::new(tls_stream); 32 | println!("-- connected to {}:{}", imap_server, 993); 33 | 34 | // the client we have here is unauthenticated. 35 | // to do anything useful with the e-mails, we need to log in 36 | let mut imap_session = client.login(login, password).await.map_err(|e| e.0)?; 37 | println!("-- logged in a {}", login); 38 | 39 | // we want to fetch the first email in the INBOX mailbox 40 | imap_session.select("INBOX").await?; 41 | println!("-- INBOX selected"); 42 | 43 | // fetch message number 1 in this mailbox, along with its RFC822 field. 44 | // RFC 822 dictates the format of the body of e-mails 45 | let messages_stream = imap_session.fetch("1", "RFC822").await?; 46 | let messages: Vec<_> = messages_stream.try_collect().await?; 47 | let message = if let Some(m) = messages.first() { 48 | m 49 | } else { 50 | return Ok(None); 51 | }; 52 | 53 | // extract the message's body 54 | let body = message.body().expect("message did not have a body!"); 55 | let body = std::str::from_utf8(body) 56 | .expect("message was not valid utf-8") 57 | .to_string(); 58 | println!("-- 1 message received, logging out"); 59 | 60 | // be nice to the server and log out 61 | imap_session.logout().await?; 62 | 63 | Ok(Some(body)) 64 | } 65 | -------------------------------------------------------------------------------- /examples/src/bin/gmail_oauth2.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use futures::StreamExt; 3 | 4 | #[cfg(feature = "runtime-async-std")] 5 | use async_std::net::TcpStream; 6 | #[cfg(feature = "runtime-tokio")] 7 | use tokio::net::TcpStream; 8 | 9 | struct GmailOAuth2 { 10 | user: String, 11 | access_token: String, 12 | } 13 | 14 | impl async_imap::Authenticator for &GmailOAuth2 { 15 | type Response = String; 16 | 17 | fn process(&mut self, _data: &[u8]) -> Self::Response { 18 | format!( 19 | "user={}\x01auth=Bearer {}\x01\x01", 20 | self.user, self.access_token 21 | ) 22 | } 23 | } 24 | 25 | #[cfg_attr(feature = "runtime-tokio", tokio::main)] 26 | #[cfg_attr(feature = "runtime-async-std", async_std::main)] 27 | async fn main() -> Result<()> { 28 | let gmail_auth = GmailOAuth2 { 29 | user: String::from("sombody@gmail.com"), 30 | access_token: String::from(""), 31 | }; 32 | let domain = "imap.gmail.com"; 33 | let port = 993; 34 | let socket_addr = (domain, port); 35 | let tcp_stream = TcpStream::connect(socket_addr).await?; 36 | let tls = async_native_tls::TlsConnector::new(); 37 | let tls_stream = tls.connect(domain, tcp_stream).await?; 38 | let client = async_imap::Client::new(tls_stream); 39 | 40 | let mut imap_session = match client.authenticate("XOAUTH2", &gmail_auth).await { 41 | Ok(c) => c, 42 | Err((e, _unauth_client)) => { 43 | println!("error authenticating: {}", e); 44 | return Err(e.into()); 45 | } 46 | }; 47 | 48 | match imap_session.select("INBOX").await { 49 | Ok(mailbox) => println!("{}", mailbox), 50 | Err(e) => println!("Error selecting INBOX: {}", e), 51 | }; 52 | 53 | { 54 | let mut msgs = imap_session.fetch("2", "body[text]").await.map_err(|e| { 55 | eprintln!("Error Fetching email 2: {}", e); 56 | e 57 | })?; 58 | 59 | while let Some(msg) = msgs.next().await { 60 | print!("{:?}", msg?); 61 | } 62 | } 63 | 64 | imap_session.logout().await?; 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /examples/src/bin/idle.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::time::Duration; 3 | 4 | use anyhow::{bail, Result}; 5 | use async_imap::extensions::idle::IdleResponse::*; 6 | use futures::StreamExt; 7 | 8 | #[cfg(feature = "runtime-async-std")] 9 | use async_std::{net::TcpStream, task, task::sleep}; 10 | 11 | #[cfg(feature = "runtime-tokio")] 12 | use tokio::{net::TcpStream, task, time::sleep}; 13 | 14 | #[cfg_attr(feature = "runtime-tokio", tokio::main)] 15 | #[cfg_attr(feature = "runtime-async-std", async_std::main)] 16 | async fn main() -> Result<()> { 17 | let args: Vec = env::args().collect(); 18 | if args.len() != 4 { 19 | eprintln!("need three arguments: imap-server login password"); 20 | bail!("need three arguments"); 21 | } else { 22 | fetch_and_idle(&args[1], &args[2], &args[3]).await?; 23 | Ok(()) 24 | } 25 | } 26 | 27 | async fn fetch_and_idle(imap_server: &str, login: &str, password: &str) -> Result<()> { 28 | let imap_addr = (imap_server, 993); 29 | let tcp_stream = TcpStream::connect(imap_addr).await?; 30 | let tls = async_native_tls::TlsConnector::new(); 31 | let tls_stream = tls.connect(imap_server, tcp_stream).await?; 32 | 33 | let client = async_imap::Client::new(tls_stream); 34 | println!("-- connected to {}:{}", imap_addr.0, imap_addr.1); 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 session = client.login(login, password).await.map_err(|e| e.0)?; 39 | println!("-- logged in a {}", login); 40 | 41 | // we want to fetch some messages from the INBOX 42 | session.select("INBOX").await?; 43 | println!("-- INBOX selected"); 44 | 45 | // fetch flags from all messages 46 | let msg_stream = session.fetch("1:*", "(FLAGS )").await?; 47 | let msgs = msg_stream.collect::>().await; 48 | println!("-- number of fetched msgs: {:?}", msgs.len()); 49 | 50 | // init idle session 51 | println!("-- initializing idle"); 52 | let mut idle = session.idle(); 53 | idle.init().await?; 54 | 55 | println!("-- idle async wait"); 56 | let (idle_wait, interrupt) = idle.wait(); 57 | 58 | /* 59 | let stdin = io::stdin(); 60 | let mut line = String::new(); 61 | stdin.read_line(&mut line).await?; 62 | println!("-- read line: {}", line); 63 | */ 64 | 65 | task::spawn(async move { 66 | println!("-- thread: waiting for 30s"); 67 | sleep(Duration::from_secs(30)).await; 68 | println!("-- thread: waited 30 secs, now interrupting idle"); 69 | drop(interrupt); 70 | }); 71 | 72 | let idle_result = idle_wait.await?; 73 | match idle_result { 74 | ManualInterrupt => { 75 | println!("-- IDLE manually interrupted"); 76 | } 77 | Timeout => { 78 | println!("-- IDLE timed out"); 79 | } 80 | NewData(data) => { 81 | let s = String::from_utf8(data.borrow_owner().to_vec()).unwrap(); 82 | println!("-- IDLE data:\n{}", s); 83 | } 84 | } 85 | 86 | // return the session after we are done with it 87 | println!("-- sending DONE"); 88 | let mut session = idle.done().await?; 89 | 90 | // be nice to the server and log out 91 | println!("-- logging out"); 92 | session.logout().await?; 93 | Ok(()) 94 | } 95 | -------------------------------------------------------------------------------- /examples/src/bin/integration.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::time::Duration; 3 | 4 | use anyhow::{Context as _, Result}; 5 | use async_imap::Session; 6 | use async_native_tls::TlsConnector; 7 | use async_smtp::{SendableEmail, SmtpClient, SmtpTransport}; 8 | #[cfg(feature = "runtime-async-std")] 9 | use async_std::{net::TcpStream, task, task::sleep}; 10 | use futures::{StreamExt, TryStreamExt}; 11 | #[cfg(feature = "runtime-tokio")] 12 | use tokio::{net::TcpStream, task, time::sleep}; 13 | 14 | fn tls() -> TlsConnector { 15 | TlsConnector::new() 16 | .danger_accept_invalid_hostnames(true) 17 | .danger_accept_invalid_certs(true) 18 | } 19 | 20 | fn test_host() -> String { 21 | std::env::var("TEST_HOST").unwrap_or_else(|_| "127.0.0.1".into()) 22 | } 23 | 24 | async fn session(user: &str) -> Result>> { 25 | let host = test_host(); 26 | let addr = (host.as_str(), 3993); 27 | let tcp_stream = TcpStream::connect(addr).await?; 28 | let tls = tls(); 29 | let tls_stream = tls.connect("imap.example.com", tcp_stream).await?; 30 | 31 | let mut client = async_imap::Client::new(tls_stream); 32 | let _greeting = client 33 | .read_response() 34 | .await 35 | .context("unexpected end of stream, expected greeting")?; 36 | 37 | let session = client 38 | .login(user, user) 39 | .await 40 | .map_err(|(err, _client)| err)?; 41 | Ok(session) 42 | } 43 | 44 | // ignored because of https://github.com/greenmail-mail-test/greenmail/issues/135 45 | async fn _connect_insecure_then_secure() -> Result<()> { 46 | let tcp_stream = TcpStream::connect((test_host().as_ref(), 3143)).await?; 47 | let tls = tls(); 48 | let mut client = async_imap::Client::new(tcp_stream); 49 | let _greeting = client 50 | .read_response() 51 | .await 52 | .context("unexpected end of stream, expected greeting")?; 53 | client.run_command_and_check_ok("STARTTLS", None).await?; 54 | let stream = client.into_inner(); 55 | let tls_stream = tls.connect("imap.example.com", stream).await?; 56 | let _client = async_imap::Client::new(tls_stream); 57 | Ok(()) 58 | } 59 | 60 | async fn smtp(user: &str) -> Result>> { 61 | let host = test_host(); 62 | let tcp_stream = TcpStream::connect((host.as_str(), 3465)).await?; 63 | 64 | let tls = tls(); 65 | let tls_stream = tls.connect("localhost", tcp_stream).await?; 66 | let client = SmtpClient::new().smtp_utf8(true); 67 | let mut transport = SmtpTransport::new(client, tls_stream).await?; 68 | let credentials = 69 | async_smtp::authentication::Credentials::new(user.to_string(), user.to_string()); 70 | let mechanism = vec![ 71 | async_smtp::authentication::Mechanism::Plain, 72 | async_smtp::authentication::Mechanism::Login, 73 | ]; 74 | transport.try_login(&credentials, &mechanism).await?; 75 | Ok(transport) 76 | } 77 | 78 | async fn login() -> Result<()> { 79 | session("readonly-test@localhost").await?; 80 | Ok(()) 81 | } 82 | 83 | async fn logout() -> Result<()> { 84 | let mut s = session("readonly-test@localhost").await?; 85 | s.logout().await?; 86 | Ok(()) 87 | } 88 | 89 | async fn inbox_zero() -> Result<()> { 90 | // https://github.com/greenmail-mail-test/greenmail/issues/265 91 | let mut s = session("readonly-test@localhost").await?; 92 | s.select("INBOX").await?; 93 | let inbox = s.search("ALL").await?; 94 | assert_eq!(inbox.len(), 0); 95 | Ok(()) 96 | } 97 | 98 | fn make_email(to: &str) -> SendableEmail { 99 | let message_id = "abc"; 100 | async_smtp::SendableEmail::new( 101 | async_smtp::Envelope::new( 102 | Some("sender@localhost".parse().unwrap()), 103 | vec![to.parse().unwrap()], 104 | ) 105 | .unwrap(), 106 | format!("To: <{}>\r\nFrom: \r\nMessage-ID: <{}.msg@localhost>\r\nSubject: My first e-mail\r\n\r\nHello world from SMTP", to, message_id), 107 | ) 108 | } 109 | 110 | async fn inbox() -> Result<()> { 111 | let to = "inbox@localhost"; 112 | 113 | // first log in so we'll see the unsolicited e-mails 114 | let mut c = session(to).await?; 115 | c.select("INBOX").await?; 116 | 117 | println!("sending"); 118 | let mut s = smtp(to).await?; 119 | 120 | // then send the e-mail 121 | let mail = make_email(to); 122 | s.send(mail).await?; 123 | 124 | println!("searching"); 125 | 126 | // now we should see the e-mail! 127 | let inbox = c.search("ALL").await?; 128 | // and the one message should have the first message sequence number 129 | assert_eq!(inbox.len(), 1); 130 | assert!(inbox.contains(&1)); 131 | 132 | // we should also get two unsolicited responses: Exists and Recent 133 | c.noop().await.unwrap(); 134 | println!("noop done"); 135 | let mut unsolicited = Vec::new(); 136 | while !c.unsolicited_responses.is_empty() { 137 | unsolicited.push(c.unsolicited_responses.recv().await.unwrap()); 138 | } 139 | 140 | assert_eq!(unsolicited.len(), 2); 141 | assert!(unsolicited 142 | .iter() 143 | .any(|m| m == &async_imap::types::UnsolicitedResponse::Exists(1))); 144 | assert!(unsolicited 145 | .iter() 146 | .any(|m| m == &async_imap::types::UnsolicitedResponse::Recent(1))); 147 | 148 | println!("fetching"); 149 | 150 | // let's see that we can also fetch the e-mail 151 | let fetch: Vec<_> = c 152 | .fetch("1", "(ALL UID)") 153 | .await 154 | .unwrap() 155 | .try_collect() 156 | .await 157 | .unwrap(); 158 | assert_eq!(fetch.len(), 1); 159 | let fetch = &fetch[0]; 160 | assert_eq!(fetch.message, 1); 161 | assert_ne!(fetch.uid, None); 162 | assert_eq!(fetch.size, Some(21)); 163 | let e = fetch.envelope().unwrap(); 164 | assert_eq!(e.subject, Some(Cow::Borrowed(&b"My first e-mail"[..]))); 165 | assert_ne!(e.from, None); 166 | assert_eq!(e.from.as_ref().unwrap().len(), 1); 167 | let from = &e.from.as_ref().unwrap()[0]; 168 | assert_eq!(from.mailbox, Some(Cow::Borrowed(&b"sender"[..]))); 169 | assert_eq!(from.host, Some(Cow::Borrowed(&b"localhost"[..]))); 170 | assert_ne!(e.to, None); 171 | assert_eq!(e.to.as_ref().unwrap().len(), 1); 172 | let to = &e.to.as_ref().unwrap()[0]; 173 | assert_eq!(to.mailbox, Some(Cow::Borrowed(&b"inbox"[..]))); 174 | assert_eq!(to.host, Some(Cow::Borrowed(&b"localhost"[..]))); 175 | let date_opt = fetch.internal_date(); 176 | assert!(date_opt.is_some()); 177 | 178 | // and let's delete it to clean up 179 | c.store("1", "+FLAGS (\\Deleted)") 180 | .await 181 | .unwrap() 182 | .collect::>() 183 | .await; 184 | c.expunge().await.unwrap().collect::>().await; 185 | 186 | // the e-mail should be gone now 187 | let inbox = c.search("ALL").await?; 188 | assert_eq!(inbox.len(), 0); 189 | Ok(()) 190 | } 191 | 192 | async fn inbox_uid() -> Result<()> { 193 | let to = "inbox-uid@localhost"; 194 | 195 | // first log in so we'll see the unsolicited e-mails 196 | let mut c = session(to).await?; 197 | c.select("INBOX").await?; 198 | 199 | // then send the e-mail 200 | let mut s = smtp(to).await?; 201 | let e = make_email(to); 202 | s.send(e).await?; 203 | 204 | // now we should see the e-mail! 205 | let inbox = c.uid_search("ALL").await?; 206 | // and the one message should have the first message sequence number 207 | assert_eq!(inbox.len(), 1); 208 | let uid = inbox.into_iter().next().unwrap(); 209 | 210 | // we should also get two unsolicited responses: Exists and Recent 211 | c.noop().await?; 212 | let mut unsolicited = Vec::new(); 213 | while !c.unsolicited_responses.is_empty() { 214 | unsolicited.push(c.unsolicited_responses.recv().await?); 215 | } 216 | 217 | assert_eq!(unsolicited.len(), 2); 218 | assert!(unsolicited 219 | .iter() 220 | .any(|m| m == &async_imap::types::UnsolicitedResponse::Exists(1))); 221 | assert!(unsolicited 222 | .iter() 223 | .any(|m| m == &async_imap::types::UnsolicitedResponse::Recent(1))); 224 | 225 | // let's see that we can also fetch the e-mail 226 | let fetch: Vec<_> = c 227 | .uid_fetch(format!("{}", uid), "(ALL UID)") 228 | .await? 229 | .try_collect() 230 | .await?; 231 | assert_eq!(fetch.len(), 1); 232 | let fetch = &fetch[0]; 233 | assert_eq!(fetch.uid, Some(uid)); 234 | let e = fetch.envelope().unwrap(); 235 | assert_eq!(e.subject, Some(Cow::Borrowed(&b"My first e-mail"[..]))); 236 | let date_opt = fetch.internal_date(); 237 | assert!(date_opt.is_some()); 238 | 239 | // and let's delete it to clean up 240 | c.uid_store(format!("{}", uid), "+FLAGS (\\Deleted)") 241 | .await? 242 | .collect::>() 243 | .await; 244 | c.expunge().await?.collect::>().await; 245 | 246 | // the e-mail should be gone now 247 | let inbox = c.search("ALL").await?; 248 | assert_eq!(inbox.len(), 0); 249 | Ok(()) 250 | } 251 | 252 | async fn _list() -> Result<()> { 253 | let mut s = session("readonly-test@localhost").await?; 254 | s.select("INBOX").await?; 255 | let subdirs: Vec<_> = s.list(None, Some("%")).await?.collect().await; 256 | assert_eq!(subdirs.len(), 0); 257 | 258 | // TODO: make a subdir 259 | Ok(()) 260 | } 261 | 262 | // Greenmail does not support IDLE :( 263 | async fn _idle() -> Result<()> { 264 | let mut session = session("idle-test@localhost").await?; 265 | 266 | // get that inbox 267 | let res = session.select("INBOX").await?; 268 | println!("selected: {:#?}", res); 269 | 270 | // fetchy fetch 271 | let msg_stream = session.fetch("1:3", "(FLAGS BODY.PEEK[])").await?; 272 | let msgs = msg_stream.collect::>().await; 273 | println!("msgs: {:?}", msgs.len()); 274 | 275 | // Idle session 276 | println!("starting idle"); 277 | let mut idle = session.idle(); 278 | idle.init().await?; 279 | 280 | let (idle_wait, interrupt) = idle.wait_with_timeout(std::time::Duration::from_secs(30)); 281 | println!("idle wait"); 282 | 283 | task::spawn(async move { 284 | println!("waiting for 1s"); 285 | sleep(Duration::from_secs(2)).await; 286 | println!("interrupting idle"); 287 | drop(interrupt); 288 | }); 289 | 290 | let idle_result = idle_wait.await; 291 | println!("idle result: {:#?}", &idle_result); 292 | 293 | // return the session after we are done with it 294 | let mut session = idle.done().await?; 295 | 296 | println!("logging out"); 297 | session.logout().await?; 298 | 299 | Ok(()) 300 | } 301 | 302 | #[cfg_attr(feature = "runtime-tokio", tokio::main)] 303 | #[cfg_attr(feature = "runtime-async-std", async_std::main)] 304 | async fn main() -> Result<()> { 305 | login().await?; 306 | logout().await?; 307 | inbox_zero().await?; 308 | inbox().await?; 309 | inbox_uid().await?; 310 | Ok(()) 311 | } 312 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | -------------------------------------------------------------------------------- /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 | /// 4 | /// [`Client::authenticate`]: crate::Client::authenticate 5 | pub trait Authenticator { 6 | /// The type of the response to the challenge. This will usually be a `Vec` or `String`. 7 | type Response: AsRef<[u8]>; 8 | 9 | /// Each base64-decoded server challenge is passed to `process`. 10 | /// The returned byte-string is base64-encoded and then sent back to the server. 11 | fn process(&mut self, challenge: &[u8]) -> Self::Response; 12 | } 13 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! IMAP error types. 2 | 3 | use std::io::Error as IoError; 4 | use std::str::Utf8Error; 5 | 6 | use base64::DecodeError; 7 | 8 | /// A convenience wrapper around `Result` for `imap::Error`. 9 | pub type Result = std::result::Result; 10 | 11 | /// A set of errors that can occur in the IMAP client 12 | #[derive(thiserror::Error, Debug)] 13 | #[non_exhaustive] 14 | pub enum Error { 15 | /// An `io::Error` that occurred while trying to read or write to a network stream. 16 | #[error("io: {0}")] 17 | Io(#[from] IoError), 18 | /// A BAD response from the IMAP server. 19 | #[error("bad response: {0}")] 20 | Bad(String), 21 | /// A NO response from the IMAP server. 22 | #[error("no response: {0}")] 23 | No(String), 24 | /// The connection was terminated unexpectedly. 25 | #[error("connection lost")] 26 | ConnectionLost, 27 | /// Error parsing a server response. 28 | #[error("parse: {0}")] 29 | Parse(#[from] ParseError), 30 | /// Command inputs were not valid [IMAP 31 | /// strings](https://tools.ietf.org/html/rfc3501#section-4.3). 32 | #[error("validate: {0}")] 33 | Validate(#[from] ValidateError), 34 | /// Error appending an e-mail. 35 | #[error("could not append mail to mailbox")] 36 | Append, 37 | } 38 | 39 | /// An error occured while trying to parse a server response. 40 | #[derive(thiserror::Error, Debug)] 41 | pub enum ParseError { 42 | /// Indicates an error parsing the status response. Such as OK, NO, and BAD. 43 | #[error("unable to parse status response")] 44 | Invalid(Vec), 45 | /// An unexpected response was encountered. 46 | #[error("encountered unexpected parsed response: {0}")] 47 | Unexpected(String), 48 | /// The client could not find or decode the server's authentication challenge. 49 | #[error("unable to parse authentication response: {0} - {1:?}")] 50 | Authentication(String, Option), 51 | /// The client received data that was not UTF-8 encoded. 52 | #[error("unable to parse data ({0:?}) as UTF-8 text: {1:?}")] 53 | DataNotUtf8(Vec, #[source] Utf8Error), 54 | /// The expected response for X was not found 55 | #[error("expected response not found for: {0}")] 56 | ExpectedResponseNotFound(String), 57 | } 58 | 59 | /// An [invalid character](https://tools.ietf.org/html/rfc3501#section-4.3) was found in an input 60 | /// string. 61 | #[derive(thiserror::Error, Debug)] 62 | #[error("invalid character in input: '{0}'")] 63 | pub struct ValidateError(pub char); 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use super::*; 68 | 69 | fn is_send(_t: T) {} 70 | 71 | #[test] 72 | fn test_send() { 73 | is_send::>(Ok(3)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/extensions/compress.rs: -------------------------------------------------------------------------------- 1 | //! IMAP COMPRESS extension specified in [RFC4978](https://www.rfc-editor.org/rfc/rfc4978.html). 2 | 3 | use std::fmt; 4 | use std::pin::Pin; 5 | use std::task::{Context, Poll}; 6 | 7 | use pin_project::pin_project; 8 | 9 | use crate::client::Session; 10 | use crate::error::Result; 11 | use crate::imap_stream::ImapStream; 12 | use crate::types::IdGenerator; 13 | use crate::Connection; 14 | 15 | #[cfg(feature = "runtime-async-std")] 16 | use async_std::io::{IoSlice, IoSliceMut, Read, Write}; 17 | #[cfg(feature = "runtime-async-std")] 18 | use futures::io::BufReader; 19 | #[cfg(feature = "runtime-tokio")] 20 | use tokio::io::{AsyncRead as Read, AsyncWrite as Write, BufReader, ReadBuf}; 21 | 22 | #[cfg(feature = "runtime-tokio")] 23 | use async_compression::tokio::bufread::DeflateDecoder; 24 | #[cfg(feature = "runtime-tokio")] 25 | use async_compression::tokio::write::DeflateEncoder; 26 | 27 | #[cfg(feature = "runtime-async-std")] 28 | use async_compression::futures::bufread::DeflateDecoder; 29 | #[cfg(feature = "runtime-async-std")] 30 | use async_compression::futures::write::DeflateEncoder; 31 | 32 | /// Network stream compressed with DEFLATE. 33 | #[derive(Debug)] 34 | #[pin_project] 35 | pub struct DeflateStream { 36 | #[pin] 37 | inner: DeflateDecoder>>, 38 | } 39 | 40 | impl DeflateStream { 41 | pub(crate) fn new(stream: T) -> Self { 42 | let stream = DeflateEncoder::new(stream); 43 | let stream = BufReader::new(stream); 44 | let stream = DeflateDecoder::new(stream); 45 | Self { inner: stream } 46 | } 47 | 48 | /// Gets a reference to the underlying stream. 49 | pub fn get_ref(&self) -> &T { 50 | self.inner.get_ref().get_ref().get_ref() 51 | } 52 | 53 | /// Gets a mutable reference to the underlying stream. 54 | pub fn get_mut(&mut self) -> &mut T { 55 | self.inner.get_mut().get_mut().get_mut() 56 | } 57 | 58 | /// Consumes `DeflateStream` and returns underlying stream. 59 | pub fn into_inner(self) -> T { 60 | self.inner.into_inner().into_inner().into_inner() 61 | } 62 | } 63 | 64 | #[cfg(feature = "runtime-tokio")] 65 | impl Read for DeflateStream { 66 | fn poll_read( 67 | self: Pin<&mut Self>, 68 | cx: &mut Context<'_>, 69 | buf: &mut ReadBuf<'_>, 70 | ) -> Poll> { 71 | self.project().inner.poll_read(cx, buf) 72 | } 73 | } 74 | 75 | #[cfg(feature = "runtime-async-std")] 76 | impl Read for DeflateStream { 77 | fn poll_read( 78 | self: Pin<&mut Self>, 79 | cx: &mut Context<'_>, 80 | buf: &mut [u8], 81 | ) -> Poll> { 82 | self.project().inner.poll_read(cx, buf) 83 | } 84 | 85 | fn poll_read_vectored( 86 | self: Pin<&mut Self>, 87 | cx: &mut Context<'_>, 88 | bufs: &mut [IoSliceMut<'_>], 89 | ) -> Poll> { 90 | self.project().inner.poll_read_vectored(cx, bufs) 91 | } 92 | } 93 | 94 | #[cfg(feature = "runtime-tokio")] 95 | impl Write for DeflateStream { 96 | fn poll_write( 97 | self: Pin<&mut Self>, 98 | cx: &mut std::task::Context<'_>, 99 | buf: &[u8], 100 | ) -> Poll> { 101 | self.project().inner.get_pin_mut().poll_write(cx, buf) 102 | } 103 | 104 | fn poll_flush( 105 | self: Pin<&mut Self>, 106 | cx: &mut std::task::Context<'_>, 107 | ) -> Poll> { 108 | self.project().inner.poll_flush(cx) 109 | } 110 | 111 | fn poll_shutdown( 112 | self: Pin<&mut Self>, 113 | cx: &mut std::task::Context<'_>, 114 | ) -> Poll> { 115 | self.project().inner.poll_shutdown(cx) 116 | } 117 | 118 | fn poll_write_vectored( 119 | self: Pin<&mut Self>, 120 | cx: &mut Context<'_>, 121 | bufs: &[std::io::IoSlice<'_>], 122 | ) -> Poll> { 123 | self.project().inner.poll_write_vectored(cx, bufs) 124 | } 125 | 126 | fn is_write_vectored(&self) -> bool { 127 | self.inner.is_write_vectored() 128 | } 129 | } 130 | 131 | #[cfg(feature = "runtime-async-std")] 132 | impl Write for DeflateStream { 133 | fn poll_write( 134 | self: Pin<&mut Self>, 135 | cx: &mut std::task::Context<'_>, 136 | buf: &[u8], 137 | ) -> Poll> { 138 | self.project().inner.as_mut().poll_write(cx, buf) 139 | } 140 | 141 | fn poll_flush( 142 | self: Pin<&mut Self>, 143 | cx: &mut std::task::Context<'_>, 144 | ) -> Poll> { 145 | self.project().inner.poll_flush(cx) 146 | } 147 | 148 | fn poll_close( 149 | self: Pin<&mut Self>, 150 | cx: &mut std::task::Context<'_>, 151 | ) -> Poll> { 152 | self.project().inner.poll_close(cx) 153 | } 154 | 155 | fn poll_write_vectored( 156 | self: Pin<&mut Self>, 157 | cx: &mut Context<'_>, 158 | bufs: &[IoSlice<'_>], 159 | ) -> Poll> { 160 | self.project().inner.poll_write_vectored(cx, bufs) 161 | } 162 | } 163 | 164 | impl Session { 165 | /// Runs `COMPRESS DEFLATE` command. 166 | pub async fn compress(self, f: F) -> Result> 167 | where 168 | S: Read + Write + Unpin + fmt::Debug, 169 | F: FnOnce(DeflateStream) -> S, 170 | { 171 | let Self { 172 | mut conn, 173 | unsolicited_responses_tx, 174 | unsolicited_responses, 175 | } = self; 176 | conn.run_command_and_check_ok("COMPRESS DEFLATE", Some(unsolicited_responses_tx.clone())) 177 | .await?; 178 | 179 | let stream = conn.into_inner(); 180 | let deflate_stream = DeflateStream::new(stream); 181 | let stream = ImapStream::new(f(deflate_stream)); 182 | let conn = Connection { 183 | stream, 184 | request_ids: IdGenerator::new(), 185 | }; 186 | let session = Session { 187 | conn, 188 | unsolicited_responses_tx, 189 | unsolicited_responses, 190 | }; 191 | Ok(session) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/extensions/id.rs: -------------------------------------------------------------------------------- 1 | //! IMAP ID extension specified in [RFC2971](https://datatracker.ietf.org/doc/html/rfc2971) 2 | 3 | use async_channel as channel; 4 | use futures::io; 5 | use futures::prelude::*; 6 | use imap_proto::{self, RequestId, Response}; 7 | use std::collections::HashMap; 8 | 9 | use crate::types::ResponseData; 10 | use crate::types::*; 11 | use crate::{ 12 | error::Result, 13 | parse::{filter, handle_unilateral}, 14 | }; 15 | 16 | fn escape(s: &str) -> String { 17 | s.replace('\\', r"\\").replace('\"', "\\\"") 18 | } 19 | 20 | /// Formats list of key-value pairs for ID command. 21 | /// 22 | /// Returned list is not wrapped in parenthesis, the caller should do it. 23 | pub(crate) fn format_identification<'a, 'b>( 24 | id: impl IntoIterator)>, 25 | ) -> String { 26 | id.into_iter() 27 | .map(|(k, v)| { 28 | format!( 29 | "\"{}\" {}", 30 | escape(k), 31 | v.map_or("NIL".to_string(), |v| format!("\"{}\"", escape(v))) 32 | ) 33 | }) 34 | .collect::>() 35 | .join(" ") 36 | } 37 | 38 | pub(crate) async fn parse_id> + Unpin>( 39 | stream: &mut T, 40 | unsolicited: channel::Sender, 41 | command_tag: RequestId, 42 | ) -> Result>> { 43 | let mut id = None; 44 | while let Some(resp) = stream 45 | .take_while(|res| filter(res, &command_tag)) 46 | .next() 47 | .await 48 | { 49 | let resp = resp?; 50 | match resp.parsed() { 51 | Response::Id(res) => { 52 | id = res.as_ref().map(|m| { 53 | m.iter() 54 | .map(|(k, v)| (k.to_string(), v.to_string())) 55 | .collect() 56 | }) 57 | } 58 | _ => { 59 | handle_unilateral(resp, unsolicited.clone()); 60 | } 61 | } 62 | } 63 | 64 | Ok(id) 65 | } 66 | 67 | #[cfg(test)] 68 | mod tests { 69 | use super::*; 70 | 71 | #[test] 72 | fn test_format_identification() { 73 | assert_eq!( 74 | format_identification([("name", Some("MyClient"))]), 75 | r#""name" "MyClient""# 76 | ); 77 | 78 | assert_eq!( 79 | format_identification([("name", Some(r#""MyClient"\"#))]), 80 | r#""name" "\"MyClient\"\\""# 81 | ); 82 | 83 | assert_eq!( 84 | format_identification([("name", Some("MyClient")), ("version", Some("2.0"))]), 85 | r#""name" "MyClient" "version" "2.0""# 86 | ); 87 | 88 | assert_eq!( 89 | format_identification([("name", None), ("version", Some("2.0"))]), 90 | r#""name" NIL "version" "2.0""# 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/extensions/idle.rs: -------------------------------------------------------------------------------- 1 | //! Adds support for the IMAP IDLE command specificed in [RFC2177](https://tools.ietf.org/html/rfc2177). 2 | 3 | use std::fmt; 4 | use std::pin::Pin; 5 | use std::time::Duration; 6 | 7 | #[cfg(feature = "runtime-async-std")] 8 | use async_std::{ 9 | future::timeout, 10 | io::{Read, Write}, 11 | }; 12 | use futures::prelude::*; 13 | use futures::task::{Context, Poll}; 14 | use imap_proto::{RequestId, Response, Status}; 15 | use stop_token::prelude::*; 16 | #[cfg(feature = "runtime-tokio")] 17 | use tokio::{ 18 | io::{AsyncRead as Read, AsyncWrite as Write}, 19 | time::timeout, 20 | }; 21 | 22 | use crate::client::Session; 23 | use crate::error::Result; 24 | use crate::parse::handle_unilateral; 25 | use crate::types::ResponseData; 26 | 27 | /// `Handle` allows a client to block waiting for changes to the remote mailbox. 28 | /// 29 | /// The handle blocks using the [`IDLE` command](https://tools.ietf.org/html/rfc2177#section-3) 30 | /// specificed in [RFC 2177](https://tools.ietf.org/html/rfc2177) until the underlying server state 31 | /// changes in some way. While idling does inform the client what changes happened on the server, 32 | /// this implementation will currently just block until _anything_ changes, and then notify the 33 | /// 34 | /// Note that the server MAY consider a client inactive if it has an IDLE command running, and if 35 | /// such a server has an inactivity timeout it MAY log the client off implicitly at the end of its 36 | /// timeout period. Because of that, clients using IDLE are advised to terminate the IDLE and 37 | /// re-issue it at least every 29 minutes to avoid being logged off. [`Handle::wait`] 38 | /// does this. This still allows a client to receive immediate mailbox updates even though it need 39 | /// only "poll" at half hour intervals. 40 | /// 41 | /// As long as a [`Handle`] is active, the mailbox cannot be otherwise accessed. 42 | #[derive(Debug)] 43 | pub struct Handle { 44 | session: Session, 45 | id: Option, 46 | } 47 | 48 | impl Unpin for Handle {} 49 | 50 | impl Stream for Handle { 51 | type Item = std::io::Result; 52 | 53 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 54 | self.as_mut().session().get_stream().poll_next(cx) 55 | } 56 | } 57 | 58 | /// A stream of server responses after sending `IDLE`. 59 | #[derive(Debug)] 60 | #[must_use = "futures do nothing unless polled"] 61 | pub struct IdleStream<'a, St> { 62 | stream: &'a mut St, 63 | } 64 | 65 | impl Unpin for IdleStream<'_, St> {} 66 | 67 | impl<'a, St: Stream + Unpin> IdleStream<'a, St> { 68 | unsafe_pinned!(stream: &'a mut St); 69 | 70 | pub(crate) fn new(stream: &'a mut St) -> Self { 71 | IdleStream { stream } 72 | } 73 | } 74 | 75 | impl futures::stream::FusedStream for IdleStream<'_, St> { 76 | fn is_terminated(&self) -> bool { 77 | self.stream.is_terminated() 78 | } 79 | } 80 | 81 | impl Stream for IdleStream<'_, St> { 82 | type Item = St::Item; 83 | 84 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 85 | self.stream().poll_next(cx) 86 | } 87 | } 88 | 89 | /// Possible responses that happen on an open idle connection. 90 | #[derive(Debug, PartialEq, Eq)] 91 | pub enum IdleResponse { 92 | /// The manual interrupt was used to interrupt the idle connection.. 93 | ManualInterrupt, 94 | /// The idle connection timed out, because of the user set timeout. 95 | Timeout, 96 | /// The server has indicated that some new action has happened. 97 | NewData(ResponseData), 98 | } 99 | 100 | // Make it possible to access the inner connection and modify its settings, such as read/write 101 | // timeouts. 102 | impl AsMut for Handle { 103 | fn as_mut(&mut self) -> &mut T { 104 | self.session.conn.stream.as_mut() 105 | } 106 | } 107 | 108 | impl Handle { 109 | unsafe_pinned!(session: Session); 110 | 111 | pub(crate) fn new(session: Session) -> Handle { 112 | Handle { session, id: None } 113 | } 114 | 115 | /// Start listening to the server side responses. 116 | /// Must be called after [`Handle::init`]. 117 | pub fn wait( 118 | &mut self, 119 | ) -> ( 120 | impl Future> + '_, 121 | stop_token::StopSource, 122 | ) { 123 | self.wait_with_timeout(Duration::from_secs(24 * 60 * 60)) 124 | } 125 | 126 | /// Start listening to the server side responses. 127 | /// 128 | /// Stops after the passed in `timeout` without any response from the server. 129 | /// Timeout is reset by any response, including `* OK Still here` keepalives. 130 | /// 131 | /// Must be called after [Handle::init]. 132 | pub fn wait_with_timeout( 133 | &mut self, 134 | dur: Duration, 135 | ) -> ( 136 | impl Future> + '_, 137 | stop_token::StopSource, 138 | ) { 139 | assert!( 140 | self.id.is_some(), 141 | "Cannot listen to response without starting IDLE" 142 | ); 143 | 144 | let sender = self.session.unsolicited_responses_tx.clone(); 145 | 146 | let interrupt = stop_token::StopSource::new(); 147 | let raw_stream = IdleStream::new(self); 148 | let mut interruptible_stream = raw_stream.timeout_at(interrupt.token()); 149 | 150 | let fut = async move { 151 | loop { 152 | let Ok(res) = timeout(dur, interruptible_stream.next()).await else { 153 | return Ok(IdleResponse::Timeout); 154 | }; 155 | 156 | let Some(Ok(resp)) = res else { 157 | return Ok(IdleResponse::ManualInterrupt); 158 | }; 159 | 160 | let resp = resp?; 161 | match resp.parsed() { 162 | Response::Data { 163 | status: Status::Ok, .. 164 | } => { 165 | // all good continue 166 | } 167 | Response::Continue { .. } => { 168 | // continuation, wait for it 169 | } 170 | Response::Done { .. } => { 171 | handle_unilateral(resp, sender.clone()); 172 | } 173 | _ => return Ok(IdleResponse::NewData(resp)), 174 | } 175 | } 176 | }; 177 | 178 | (fut, interrupt) 179 | } 180 | 181 | /// Initialise the idle connection by sending the `IDLE` command to the server. 182 | pub async fn init(&mut self) -> Result<()> { 183 | let id = self.session.run_command("IDLE").await?; 184 | self.id = Some(id); 185 | while let Some(res) = self.session.stream.next().await { 186 | let res = res?; 187 | match res.parsed() { 188 | Response::Continue { .. } => { 189 | return Ok(()); 190 | } 191 | Response::Done { 192 | tag, 193 | status, 194 | information, 195 | .. 196 | } => { 197 | if tag == self.id.as_ref().unwrap() { 198 | if let Status::Bad = status { 199 | return Err(std::io::Error::new( 200 | std::io::ErrorKind::ConnectionRefused, 201 | information.as_ref().unwrap().to_string(), 202 | ) 203 | .into()); 204 | } 205 | } 206 | handle_unilateral(res, self.session.unsolicited_responses_tx.clone()); 207 | } 208 | _ => { 209 | handle_unilateral(res, self.session.unsolicited_responses_tx.clone()); 210 | } 211 | } 212 | } 213 | 214 | Err(std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "").into()) 215 | } 216 | 217 | /// Signal that we want to exit the idle connection, by sending the `DONE` 218 | /// command to the server. 219 | pub async fn done(mut self) -> Result> { 220 | assert!( 221 | self.id.is_some(), 222 | "Cannot call DONE on a non initialized idle connection" 223 | ); 224 | self.session.run_command_untagged("DONE").await?; 225 | let sender = self.session.unsolicited_responses_tx.clone(); 226 | self.session 227 | .check_done_ok(&self.id.expect("invalid setup"), Some(sender)) 228 | .await?; 229 | 230 | Ok(self.session) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/extensions/mod.rs: -------------------------------------------------------------------------------- 1 | //! Implementations of various IMAP extensions. 2 | #[cfg(feature = "compress")] 3 | pub mod compress; 4 | 5 | pub mod idle; 6 | 7 | pub mod quota; 8 | 9 | pub mod id; 10 | -------------------------------------------------------------------------------- /src/extensions/quota.rs: -------------------------------------------------------------------------------- 1 | //! Adds support for the GETQUOTA and GETQUOTAROOT commands specificed in [RFC2087](https://tools.ietf.org/html/rfc2087). 2 | 3 | use async_channel as channel; 4 | use futures::io; 5 | use futures::prelude::*; 6 | use imap_proto::{self, RequestId, Response}; 7 | 8 | use crate::types::*; 9 | use crate::{ 10 | error::Result, 11 | parse::{filter, handle_unilateral}, 12 | }; 13 | use crate::{ 14 | error::{Error, ParseError}, 15 | types::{Quota, QuotaRoot, ResponseData}, 16 | }; 17 | 18 | pub(crate) async fn parse_get_quota> + Unpin>( 19 | stream: &mut T, 20 | unsolicited: channel::Sender, 21 | command_tag: RequestId, 22 | ) -> Result { 23 | let mut quota = None; 24 | while let Some(resp) = stream 25 | .take_while(|res| filter(res, &command_tag)) 26 | .next() 27 | .await 28 | { 29 | let resp = resp?; 30 | match resp.parsed() { 31 | Response::Quota(q) => quota = Some(q.clone().into()), 32 | _ => { 33 | handle_unilateral(resp, unsolicited.clone()); 34 | } 35 | } 36 | } 37 | 38 | match quota { 39 | Some(q) => Ok(q), 40 | None => Err(Error::Parse(ParseError::ExpectedResponseNotFound( 41 | "Quota, no quota response found".to_string(), 42 | ))), 43 | } 44 | } 45 | 46 | pub(crate) async fn parse_get_quota_root> + Unpin>( 47 | stream: &mut T, 48 | unsolicited: channel::Sender, 49 | command_tag: RequestId, 50 | ) -> Result<(Vec, Vec)> { 51 | let mut roots: Vec = Vec::new(); 52 | let mut quotas: Vec = Vec::new(); 53 | 54 | while let Some(resp) = stream 55 | .take_while(|res| filter(res, &command_tag)) 56 | .next() 57 | .await 58 | { 59 | let resp = resp?; 60 | match resp.parsed() { 61 | Response::QuotaRoot(qr) => { 62 | roots.push(qr.clone().into()); 63 | } 64 | Response::Quota(q) => { 65 | quotas.push(q.clone().into()); 66 | } 67 | _ => { 68 | handle_unilateral(resp, unsolicited.clone()); 69 | } 70 | } 71 | } 72 | 73 | Ok((roots, quotas)) 74 | } 75 | -------------------------------------------------------------------------------- /src/imap_stream.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::pin::Pin; 3 | 4 | #[cfg(feature = "runtime-async-std")] 5 | use async_std::io::{Read, Write, WriteExt}; 6 | use bytes::BytesMut; 7 | use futures::stream::Stream; 8 | use futures::task::{Context, Poll}; 9 | use futures::{io, ready}; 10 | use nom::Needed; 11 | #[cfg(feature = "runtime-tokio")] 12 | use tokio::io::{AsyncRead as Read, AsyncWrite as Write, AsyncWriteExt}; 13 | 14 | use crate::types::{Request, ResponseData}; 15 | 16 | /// Wraps a stream, and parses incoming data as imap server messages. Writes outgoing data 17 | /// as imap client messages. 18 | #[derive(Debug)] 19 | pub struct ImapStream { 20 | /// The underlying stream 21 | pub(crate) inner: R, 22 | /// Number of bytes the next decode operation needs if known. 23 | /// If the buffer contains less than this, it is a waste of time to try to parse it. 24 | /// If unknown, set it to 0, so decoding is always attempted. 25 | decode_needs: usize, 26 | /// The buffer. 27 | buffer: Buffer, 28 | } 29 | 30 | impl ImapStream { 31 | /// Creates a new `ImapStream` based on the given `Read`er. 32 | pub fn new(inner: R) -> Self { 33 | ImapStream { 34 | inner, 35 | buffer: Buffer::new(), 36 | decode_needs: 0, 37 | } 38 | } 39 | 40 | pub async fn encode(&mut self, msg: Request) -> Result<(), io::Error> { 41 | log::trace!( 42 | "encode: input: {:?}, {:?}", 43 | msg.0, 44 | std::str::from_utf8(&msg.1) 45 | ); 46 | 47 | if let Some(tag) = msg.0 { 48 | self.inner.write_all(tag.as_bytes()).await?; 49 | self.inner.write_all(b" ").await?; 50 | } 51 | self.inner.write_all(&msg.1).await?; 52 | self.inner.write_all(b"\r\n").await?; 53 | 54 | Ok(()) 55 | } 56 | 57 | pub fn into_inner(self) -> R { 58 | self.inner 59 | } 60 | 61 | /// Flushes the underlying stream. 62 | pub async fn flush(&mut self) -> Result<(), io::Error> { 63 | self.inner.flush().await 64 | } 65 | 66 | pub fn as_mut(&mut self) -> &mut R { 67 | &mut self.inner 68 | } 69 | 70 | /// Attempts to decode a single response from the buffer. 71 | /// 72 | /// Returns `None` if the buffer does not contain enough data. 73 | fn decode(&mut self) -> io::Result> { 74 | if self.buffer.used() < self.decode_needs { 75 | // We know that there is not enough data to decode anything 76 | // from previous attempts. 77 | return Ok(None); 78 | } 79 | 80 | let block = self.buffer.take_block(); 81 | // Be aware, now self.buffer is invalid until block is returned or reset! 82 | 83 | let res = ResponseData::try_new_or_recover(block, |buf| { 84 | let buf = &buf[..self.buffer.used()]; 85 | log::trace!("decode: input: {:?}", std::str::from_utf8(buf)); 86 | match imap_proto::parser::parse_response(buf) { 87 | Ok((remaining, response)) => { 88 | // TODO: figure out if we can use a minimum required size for a response. 89 | self.decode_needs = 0; 90 | self.buffer.reset_with_data(remaining); 91 | Ok(response) 92 | } 93 | Err(nom::Err::Incomplete(Needed::Size(min))) => { 94 | log::trace!("decode: incomplete data, need minimum {min} bytes"); 95 | self.decode_needs = self.buffer.used() + usize::from(min); 96 | Err(None) 97 | } 98 | Err(nom::Err::Incomplete(_)) => { 99 | log::trace!("decode: incomplete data, need unknown number of bytes"); 100 | self.decode_needs = 0; 101 | Err(None) 102 | } 103 | Err(other) => { 104 | self.decode_needs = 0; 105 | Err(Some(io::Error::other(format!( 106 | "{:?} during parsing of {:?}", 107 | other, 108 | String::from_utf8_lossy(buf) 109 | )))) 110 | } 111 | } 112 | }); 113 | match res { 114 | Ok(response) => Ok(Some(response)), 115 | Err((heads, err)) => { 116 | self.buffer.return_block(heads); 117 | match err { 118 | Some(err) => Err(err), 119 | None => Ok(None), 120 | } 121 | } 122 | } 123 | } 124 | } 125 | 126 | /// Abstraction around needed buffer management. 127 | struct Buffer { 128 | /// The buffer itself. 129 | block: BytesMut, 130 | /// Offset where used bytes range ends. 131 | offset: usize, 132 | } 133 | 134 | impl Buffer { 135 | const BLOCK_SIZE: usize = 1024 * 4; 136 | const MAX_CAPACITY: usize = 512 * 1024 * 1024; // 512 MiB 137 | 138 | fn new() -> Self { 139 | Self { 140 | block: BytesMut::zeroed(Self::BLOCK_SIZE), 141 | offset: 0, 142 | } 143 | } 144 | 145 | /// Returns the number of bytes in the buffer containing data. 146 | fn used(&self) -> usize { 147 | self.offset 148 | } 149 | 150 | /// Returns the unused part of the buffer to which new data can be written. 151 | fn free_as_mut_slice(&mut self) -> &mut [u8] { 152 | &mut self.block[self.offset..] 153 | } 154 | 155 | /// Indicate how many new bytes were written into the buffer. 156 | /// 157 | /// When new bytes are written into the slice returned by [`free_as_mut_slice`] this method 158 | /// should be called to extend the used portion of the buffer to include the new data. 159 | /// 160 | /// You can not write past the end of the buffer, so extending more then there is free 161 | /// space marks the entire buffer as used. 162 | /// 163 | /// [`free_as_mut_slice`]: Self::free_as_mut_slice 164 | // aka advance()? 165 | fn extend_used(&mut self, num_bytes: usize) { 166 | self.offset += num_bytes; 167 | if self.offset > self.block.len() { 168 | self.offset = self.block.len(); 169 | } 170 | } 171 | 172 | /// Ensure the buffer has free capacity, optionally ensuring minimum buffer size. 173 | fn ensure_capacity(&mut self, required: usize) -> io::Result<()> { 174 | let free_bytes: usize = self.block.len() - self.offset; 175 | let extra_bytes_needed: usize = required.saturating_sub(self.block.len()); 176 | if free_bytes == 0 || extra_bytes_needed > 0 { 177 | let increase = std::cmp::max(Buffer::BLOCK_SIZE, extra_bytes_needed); 178 | self.grow(increase)?; 179 | } 180 | 181 | // Assert that the buffer at least one free byte. 182 | debug_assert!(self.offset < self.block.len()); 183 | 184 | // Assert that the buffer has at least the required capacity. 185 | debug_assert!(self.block.len() >= required); 186 | Ok(()) 187 | } 188 | 189 | /// Grows the buffer, ensuring there are free bytes in the tail. 190 | /// 191 | /// The specified number of bytes is only a minimum. The buffer could grow by more as 192 | /// it will always grow in multiples of [`BLOCK_SIZE`]. 193 | /// 194 | /// If the size would be larger than [`MAX_CAPACITY`] an error is returned. 195 | /// 196 | /// [`BLOCK_SIZE`]: Self::BLOCK_SIZE 197 | /// [`MAX_CAPACITY`]: Self::MAX_CAPACITY 198 | fn grow(&mut self, num_bytes: usize) -> io::Result<()> { 199 | let min_size = self.block.len() + num_bytes; 200 | let new_size = match min_size % Self::BLOCK_SIZE { 201 | 0 => min_size, 202 | n => min_size + (Self::BLOCK_SIZE - n), 203 | }; 204 | if new_size > Self::MAX_CAPACITY { 205 | Err(io::Error::other("incoming data too large")) 206 | } else { 207 | self.block.resize(new_size, 0); 208 | Ok(()) 209 | } 210 | } 211 | 212 | /// Return the block backing the buffer. 213 | /// 214 | /// Next you *must* either return this block using [`return_block`] or call 215 | /// [`reset_with_data`]. 216 | /// 217 | /// [`return_block`]: Self::return_block 218 | /// [`reset_with_data`]: Self::reset_with_data 219 | // TODO: Enforce this with typestate. 220 | fn take_block(&mut self) -> BytesMut { 221 | std::mem::replace(&mut self.block, BytesMut::zeroed(Self::BLOCK_SIZE)) 222 | } 223 | 224 | /// Reset the buffer to be a new allocation with given data copied in. 225 | /// 226 | /// This allows the previously returned block from `get_block` to be used in and owned 227 | /// by the [ResponseData]. 228 | /// 229 | /// This does not do any bounds checking to see if the new buffer would exceed the 230 | /// maximum size. It will however ensure that there is at least some free space at the 231 | /// end of the buffer so that the next reading operation won't need to realloc right 232 | /// away. This could be wasteful if the next action on the buffer is another decode 233 | /// rather than a read, but we don't know. 234 | fn reset_with_data(&mut self, data: &[u8]) { 235 | let min_size = data.len(); 236 | let new_size = match min_size % Self::BLOCK_SIZE { 237 | 0 => min_size + Self::BLOCK_SIZE, 238 | n => min_size + (Self::BLOCK_SIZE - n), 239 | }; 240 | self.block = BytesMut::zeroed(new_size); 241 | self.block[..data.len()].copy_from_slice(data); 242 | 243 | self.offset = data.len(); 244 | } 245 | 246 | /// Return the block which backs this buffer. 247 | fn return_block(&mut self, block: BytesMut) { 248 | self.block = block; 249 | } 250 | } 251 | 252 | impl fmt::Debug for Buffer { 253 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 254 | f.debug_struct("Buffer") 255 | .field("used", &self.used()) 256 | .field("capacity", &self.block.capacity()) 257 | .finish() 258 | } 259 | } 260 | 261 | impl Stream for ImapStream { 262 | type Item = io::Result; 263 | 264 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 265 | let this = &mut *self; 266 | if let Some(response) = this.decode()? { 267 | return Poll::Ready(Some(Ok(response))); 268 | } 269 | loop { 270 | this.buffer.ensure_capacity(this.decode_needs)?; 271 | let buf = this.buffer.free_as_mut_slice(); 272 | 273 | // The buffer should have at least one byte free 274 | // before we try reading into it 275 | // so we can treat 0 bytes read as EOF. 276 | // This is guaranteed by `ensure_capacity()` above 277 | // even if it is called with 0 as an argument. 278 | debug_assert!(!buf.is_empty()); 279 | 280 | #[cfg(feature = "runtime-async-std")] 281 | let num_bytes_read = ready!(Pin::new(&mut this.inner).poll_read(cx, buf))?; 282 | 283 | #[cfg(feature = "runtime-tokio")] 284 | let num_bytes_read = { 285 | let buf = &mut tokio::io::ReadBuf::new(buf); 286 | let start = buf.filled().len(); 287 | ready!(Pin::new(&mut this.inner).poll_read(cx, buf))?; 288 | buf.filled().len() - start 289 | }; 290 | 291 | if num_bytes_read == 0 { 292 | if this.buffer.used() > 0 { 293 | return Poll::Ready(Some(Err(io::Error::new( 294 | io::ErrorKind::UnexpectedEof, 295 | "bytes remaining in stream", 296 | )))); 297 | } 298 | return Poll::Ready(None); 299 | } 300 | this.buffer.extend_used(num_bytes_read); 301 | if let Some(response) = this.decode()? { 302 | return Poll::Ready(Some(Ok(response))); 303 | } 304 | } 305 | } 306 | } 307 | 308 | #[cfg(test)] 309 | mod tests { 310 | use super::*; 311 | 312 | use std::io::Write; 313 | 314 | #[test] 315 | fn test_buffer_empty() { 316 | let buf = Buffer::new(); 317 | assert_eq!(buf.used(), 0); 318 | 319 | let mut buf = Buffer::new(); 320 | let slice: &[u8] = buf.free_as_mut_slice(); 321 | assert_eq!(slice.len(), Buffer::BLOCK_SIZE); 322 | assert_eq!(slice.len(), buf.block.len()); 323 | } 324 | 325 | #[test] 326 | fn test_buffer_extend_use() { 327 | let mut buf = Buffer::new(); 328 | buf.extend_used(3); 329 | assert_eq!(buf.used(), 3); 330 | let slice = buf.free_as_mut_slice(); 331 | assert_eq!(slice.len(), Buffer::BLOCK_SIZE - 3); 332 | 333 | // Extend past the end of the buffer. 334 | buf.extend_used(Buffer::BLOCK_SIZE); 335 | assert_eq!(buf.used(), Buffer::BLOCK_SIZE); 336 | assert_eq!(buf.offset, Buffer::BLOCK_SIZE); 337 | assert_eq!(buf.block.len(), buf.offset); 338 | let slice = buf.free_as_mut_slice(); 339 | assert_eq!(slice.len(), 0); 340 | } 341 | 342 | #[test] 343 | fn test_buffer_write_read() { 344 | let mut buf = Buffer::new(); 345 | let mut slice = buf.free_as_mut_slice(); 346 | slice.write_all(b"hello").unwrap(); 347 | buf.extend_used(b"hello".len()); 348 | 349 | let slice = &buf.block[..buf.used()]; 350 | assert_eq!(slice, b"hello"); 351 | assert_eq!(buf.free_as_mut_slice().len(), buf.block.len() - buf.offset); 352 | } 353 | 354 | #[test] 355 | fn test_buffer_grow() { 356 | let mut buf = Buffer::new(); 357 | assert_eq!(buf.block.len(), Buffer::BLOCK_SIZE); 358 | buf.grow(1).unwrap(); 359 | assert_eq!(buf.block.len(), 2 * Buffer::BLOCK_SIZE); 360 | 361 | buf.grow(Buffer::BLOCK_SIZE + 1).unwrap(); 362 | assert_eq!(buf.block.len(), 4 * Buffer::BLOCK_SIZE); 363 | 364 | let ret = buf.grow(Buffer::MAX_CAPACITY); 365 | assert!(ret.is_err()); 366 | } 367 | 368 | #[test] 369 | fn test_buffer_ensure_capacity() { 370 | // Initial state: 1 byte capacity left, initial size. 371 | let mut buf = Buffer::new(); 372 | buf.extend_used(Buffer::BLOCK_SIZE - 1); 373 | assert_eq!(buf.free_as_mut_slice().len(), 1); 374 | assert_eq!(buf.block.len(), Buffer::BLOCK_SIZE); 375 | 376 | // Still has capacity, no size request. 377 | buf.ensure_capacity(0).unwrap(); 378 | assert_eq!(buf.free_as_mut_slice().len(), 1); 379 | assert_eq!(buf.block.len(), Buffer::BLOCK_SIZE); 380 | 381 | // No more capacity, initial size. 382 | buf.extend_used(1); 383 | assert_eq!(buf.free_as_mut_slice().len(), 0); 384 | assert_eq!(buf.block.len(), Buffer::BLOCK_SIZE); 385 | 386 | // No capacity, no size request. 387 | buf.ensure_capacity(0).unwrap(); 388 | assert_eq!(buf.free_as_mut_slice().len(), Buffer::BLOCK_SIZE); 389 | assert_eq!(buf.block.len(), 2 * Buffer::BLOCK_SIZE); 390 | 391 | // Some capacity, size request. 392 | buf.extend_used(5); 393 | assert_eq!(buf.offset, Buffer::BLOCK_SIZE + 5); 394 | buf.ensure_capacity(3 * Buffer::BLOCK_SIZE - 6).unwrap(); 395 | assert_eq!(buf.free_as_mut_slice().len(), 2 * Buffer::BLOCK_SIZE - 5); 396 | assert_eq!(buf.block.len(), 3 * Buffer::BLOCK_SIZE); 397 | } 398 | 399 | /// Regression test for a bug in ensure_capacity() caused 400 | /// by a bug in byte-pool crate 0.2.2 dependency. 401 | /// 402 | /// ensure_capacity() sometimes did not ensure that 403 | /// at least one byte is available, which in turn 404 | /// resulted in attempt to read into a buffer of zero size. 405 | /// When poll_read() reads into a buffer of zero size, 406 | /// it can only read zero bytes, which is indistinguishable 407 | /// from EOF and resulted in an erroneous detection of EOF 408 | /// when in fact the stream was not closed. 409 | #[test] 410 | fn test_ensure_capacity_loop() { 411 | let mut buf = Buffer::new(); 412 | 413 | for i in 1..500 { 414 | // Ask for `i` bytes. 415 | buf.ensure_capacity(i).unwrap(); 416 | 417 | // Test that we can read at least 1 byte. 418 | let free = buf.free_as_mut_slice(); 419 | let used = free.len(); 420 | assert!(used > 0); 421 | 422 | // Use as much as allowed. 423 | buf.extend_used(used); 424 | 425 | // Test that we can read at least as much as requested. 426 | let block = buf.take_block(); 427 | assert!(block.len() >= i); 428 | buf.return_block(block); 429 | } 430 | } 431 | 432 | #[test] 433 | fn test_buffer_take_and_return_block() { 434 | // This test identifies blocks by their size. 435 | let mut buf = Buffer::new(); 436 | buf.grow(1).unwrap(); 437 | let block_size = buf.block.len(); 438 | 439 | let block = buf.take_block(); 440 | assert_eq!(block.len(), block_size); 441 | assert_ne!(buf.block.len(), block_size); 442 | 443 | buf.return_block(block); 444 | assert_eq!(buf.block.len(), block_size); 445 | } 446 | 447 | #[test] 448 | fn test_buffer_reset_with_data() { 449 | // This test identifies blocks by their size. 450 | let data: [u8; 2 * Buffer::BLOCK_SIZE] = [b'a'; 2 * Buffer::BLOCK_SIZE]; 451 | let mut buf = Buffer::new(); 452 | let block_size = buf.block.len(); 453 | assert_eq!(block_size, Buffer::BLOCK_SIZE); 454 | buf.reset_with_data(&data); 455 | assert_ne!(buf.block.len(), block_size); 456 | assert_eq!(buf.block.len(), 3 * Buffer::BLOCK_SIZE); 457 | assert!(!buf.free_as_mut_slice().is_empty()); 458 | 459 | let data: [u8; 0] = []; 460 | let mut buf = Buffer::new(); 461 | buf.reset_with_data(&data); 462 | assert_eq!(buf.block.len(), Buffer::BLOCK_SIZE); 463 | } 464 | 465 | #[test] 466 | fn test_buffer_debug() { 467 | assert_eq!( 468 | format!("{:?}", Buffer::new()), 469 | format!(r#"Buffer {{ used: 0, capacity: {} }}"#, Buffer::BLOCK_SIZE) 470 | ); 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Async IMAP 2 | //! 3 | //! This crate lets you connect to and interact with servers 4 | //! that implement the IMAP protocol ([RFC 3501](https://tools.ietf.org/html/rfc3501) and extensions). 5 | //! After authenticating with the server, 6 | //! IMAP lets you list, fetch, and search for e-mails, 7 | //! as well as monitor mailboxes for changes. 8 | //! 9 | //! ## Connecting 10 | //! 11 | //! Connect to the server, for example using TLS connection on port 993 12 | //! or plain TCP connection on port 143 if you plan to use STARTTLS. 13 | //! can be used. 14 | //! Pass the stream to [`Client::new()`]. 15 | //! This gives you an unauthenticated [`Client`]. 16 | //! 17 | //! Then read the server greeting: 18 | //! ```ignore 19 | //! let _greeting = client 20 | //! .read_response().await? 21 | //! .expect("unexpected end of stream, expected greeting"); 22 | //! ``` 23 | //! 24 | //! ## STARTTLS 25 | //! 26 | //! If you connected on a non-TLS port, upgrade the connection using STARTTLS: 27 | //! ```ignore 28 | //! client.run_command_and_check_ok("STARTTLS", None).await?; 29 | //! let stream = client.into_inner(); 30 | //! ``` 31 | //! Convert this stream into a TLS stream using a library 32 | //! such as [`async-native-tls`](https://crates.io/crates/async-native-tls) 33 | //! or [Rustls](`https://crates.io/crates/rustls`). 34 | //! Once you have a TLS stream, wrap it back into a [`Client`]: 35 | //! ```ignore 36 | //! let client = Client::new(tls_stream); 37 | //! ``` 38 | //! Note that there is no server greeting after STARTTLS. 39 | //! 40 | //! ## Authentication and session usage 41 | //! 42 | //! Once you have an established connection, 43 | //! authenticate using [`Client::login`] or [`Client::authenticate`] 44 | //! to perform username/password or challenge/response authentication respectively. 45 | //! This in turn gives you an authenticated 46 | //! [`Session`], which lets you access the mailboxes at the server. 47 | //! For example: 48 | //! ```ignore 49 | //! let mut session = client 50 | //! .login("alice@example.org", "password").await 51 | //! .map_err(|(err, _client)| err)?; 52 | //! session.select("INBOX").await?; 53 | //! 54 | //! // Fetch message number 1 in this mailbox, along with its RFC 822 field. 55 | //! // RFC 822 dictates the format of the body of e-mails. 56 | //! let messages_stream = imap_session.fetch("1", "RFC822").await?; 57 | //! let messages: Vec<_> = messages_stream.try_collect().await?; 58 | //! let message = messages.first().expect("found no messages in the INBOX"); 59 | //! 60 | //! // Extract the message body. 61 | //! let body = message.body().expect("message did not have a body!"); 62 | //! let body = std::str::from_utf8(body) 63 | //! .expect("message was not valid utf-8") 64 | //! .to_string(); 65 | //! 66 | //! session.logout().await?; 67 | //! ``` 68 | //! 69 | //! The documentation within this crate borrows heavily from the various RFCs, 70 | //! but should not be considered a complete reference. 71 | //! If anything is unclear, 72 | //! follow the links to the RFCs embedded in the documentation 73 | //! for the various types and methods and read the raw text there! 74 | //! 75 | //! See the `examples/` directory for usage examples. 76 | #![warn(missing_docs)] 77 | #![deny(rust_2018_idioms, unsafe_code)] 78 | 79 | #[cfg(not(any(feature = "runtime-tokio", feature = "runtime-async-std")))] 80 | compile_error!("one of 'runtime-async-std' or 'runtime-tokio' features must be enabled"); 81 | 82 | #[cfg(all(feature = "runtime-tokio", feature = "runtime-async-std"))] 83 | compile_error!("only one of 'runtime-async-std' or 'runtime-tokio' features must be enabled"); 84 | #[macro_use] 85 | extern crate pin_utils; 86 | 87 | // Reexport imap_proto for easier access. 88 | pub use imap_proto; 89 | 90 | mod authenticator; 91 | mod client; 92 | pub mod error; 93 | pub mod extensions; 94 | mod imap_stream; 95 | mod parse; 96 | pub mod types; 97 | 98 | #[cfg(feature = "compress")] 99 | pub use crate::extensions::compress::DeflateStream; 100 | 101 | pub use crate::authenticator::Authenticator; 102 | pub use crate::client::*; 103 | 104 | #[cfg(test)] 105 | mod mock_stream; 106 | -------------------------------------------------------------------------------- /src/mock_stream.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | use std::io::{Error, ErrorKind, Result}; 3 | use std::pin::Pin; 4 | use std::task::{Context, Poll}; 5 | 6 | #[cfg(feature = "runtime-async-std")] 7 | use async_std::io::{Read, Write}; 8 | #[cfg(feature = "runtime-tokio")] 9 | use tokio::io::{AsyncRead as Read, AsyncWrite as Write}; 10 | 11 | #[derive(Default, Clone, Debug, Eq, PartialEq, Hash)] 12 | pub struct MockStream { 13 | read_buf: Vec, 14 | read_pos: usize, 15 | pub written_buf: Vec, 16 | err_on_read: bool, 17 | eof_on_read: bool, 18 | read_delay: usize, 19 | } 20 | 21 | impl MockStream { 22 | pub fn new(read_buf: Vec) -> MockStream { 23 | MockStream::default().with_buf(read_buf) 24 | } 25 | 26 | pub fn with_buf(mut self, read_buf: Vec) -> MockStream { 27 | self.read_buf = read_buf; 28 | self 29 | } 30 | 31 | pub fn with_eof(mut self) -> MockStream { 32 | self.eof_on_read = true; 33 | self 34 | } 35 | 36 | pub fn with_err(mut self) -> MockStream { 37 | self.err_on_read = true; 38 | self 39 | } 40 | 41 | pub fn with_delay(mut self) -> MockStream { 42 | self.read_delay = 1; 43 | self 44 | } 45 | } 46 | 47 | #[cfg(feature = "runtime-tokio")] 48 | impl Read for MockStream { 49 | fn poll_read( 50 | mut self: Pin<&mut Self>, 51 | _cx: &mut Context<'_>, 52 | buf: &mut tokio::io::ReadBuf<'_>, 53 | ) -> Poll> { 54 | if self.eof_on_read { 55 | return Poll::Ready(Ok(())); 56 | } 57 | if self.err_on_read { 58 | return Poll::Ready(Err(Error::new(ErrorKind::Other, "MockStream Error"))); 59 | } 60 | if self.read_pos >= self.read_buf.len() { 61 | return Poll::Ready(Err(Error::new(ErrorKind::UnexpectedEof, "EOF"))); 62 | } 63 | let mut write_len = min(buf.remaining(), self.read_buf.len() - self.read_pos); 64 | if self.read_delay > 0 { 65 | self.read_delay -= 1; 66 | write_len = min(write_len, 1); 67 | } 68 | let max_pos = self.read_pos + write_len; 69 | buf.put_slice(&self.read_buf[self.read_pos..max_pos]); 70 | self.read_pos += write_len; 71 | Poll::Ready(Ok(())) 72 | } 73 | } 74 | 75 | #[cfg(feature = "runtime-tokio")] 76 | impl Write for MockStream { 77 | fn poll_write( 78 | mut self: Pin<&mut Self>, 79 | _cx: &mut Context<'_>, 80 | buf: &[u8], 81 | ) -> Poll> { 82 | self.written_buf.extend_from_slice(buf); 83 | Poll::Ready(Ok(buf.len())) 84 | } 85 | 86 | fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 87 | Poll::Ready(Ok(())) 88 | } 89 | 90 | fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 91 | Poll::Ready(Ok(())) 92 | } 93 | } 94 | 95 | #[cfg(feature = "runtime-async-std")] 96 | impl Read for MockStream { 97 | fn poll_read( 98 | mut self: Pin<&mut Self>, 99 | _cx: &mut Context<'_>, 100 | buf: &mut [u8], 101 | ) -> Poll> { 102 | if self.eof_on_read { 103 | return Poll::Ready(Ok(0)); 104 | } 105 | if self.err_on_read { 106 | return Poll::Ready(Err(Error::other("MockStream Error"))); 107 | } 108 | if self.read_pos >= self.read_buf.len() { 109 | return Poll::Ready(Err(Error::new(ErrorKind::UnexpectedEof, "EOF"))); 110 | } 111 | let mut write_len = min(buf.len(), self.read_buf.len() - self.read_pos); 112 | if self.read_delay > 0 { 113 | self.read_delay -= 1; 114 | write_len = min(write_len, 1); 115 | } 116 | let max_pos = self.read_pos + write_len; 117 | for x in self.read_pos..max_pos { 118 | buf[x - self.read_pos] = self.read_buf[x]; 119 | } 120 | self.read_pos += write_len; 121 | Poll::Ready(Ok(write_len)) 122 | } 123 | } 124 | 125 | #[cfg(feature = "runtime-async-std")] 126 | impl Write for MockStream { 127 | fn poll_write( 128 | mut self: Pin<&mut Self>, 129 | _cx: &mut Context<'_>, 130 | buf: &[u8], 131 | ) -> Poll> { 132 | self.written_buf.extend_from_slice(buf); 133 | Poll::Ready(Ok(buf.len())) 134 | } 135 | 136 | fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 137 | Poll::Ready(Ok(())) 138 | } 139 | 140 | fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 141 | Poll::Ready(Ok(())) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/parse.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use async_channel as channel; 4 | use futures::io; 5 | use futures::prelude::*; 6 | use futures::stream::Stream; 7 | use imap_proto::{self, MailboxDatum, Metadata, RequestId, Response}; 8 | 9 | use crate::error::{Error, Result}; 10 | use crate::types::ResponseData; 11 | use crate::types::*; 12 | 13 | pub(crate) fn parse_names> + Unpin + Send>( 14 | stream: &mut T, 15 | unsolicited: channel::Sender, 16 | command_tag: RequestId, 17 | ) -> impl Stream> + '_ + Send + Unpin { 18 | use futures::{FutureExt, StreamExt}; 19 | 20 | StreamExt::filter_map( 21 | StreamExt::take_while(stream, move |res| filter(res, &command_tag)), 22 | move |resp| { 23 | let unsolicited = unsolicited.clone(); 24 | async move { 25 | match resp { 26 | Ok(resp) => match resp.parsed() { 27 | Response::MailboxData(MailboxDatum::List { .. }) => { 28 | let name = Name::from_mailbox_data(resp); 29 | Some(Ok(name)) 30 | } 31 | _ => { 32 | handle_unilateral(resp, unsolicited); 33 | None 34 | } 35 | }, 36 | Err(err) => Some(Err(err.into())), 37 | } 38 | } 39 | .boxed() 40 | }, 41 | ) 42 | } 43 | 44 | pub(crate) fn filter( 45 | res: &io::Result, 46 | command_tag: &RequestId, 47 | ) -> impl Future { 48 | let val = filter_sync(res, command_tag); 49 | futures::future::ready(val) 50 | } 51 | 52 | pub(crate) fn filter_sync(res: &io::Result, command_tag: &RequestId) -> bool { 53 | match res { 54 | Ok(res) => match res.parsed() { 55 | Response::Done { tag, .. } => tag != command_tag, 56 | _ => true, 57 | }, 58 | Err(_err) => { 59 | // Do not filter out the errors such as unexpected EOF. 60 | true 61 | } 62 | } 63 | } 64 | 65 | pub(crate) fn parse_fetches> + Unpin + Send>( 66 | stream: &mut T, 67 | unsolicited: channel::Sender, 68 | command_tag: RequestId, 69 | ) -> impl Stream> + '_ + Send + Unpin { 70 | use futures::{FutureExt, StreamExt}; 71 | 72 | StreamExt::filter_map( 73 | StreamExt::take_while(stream, move |res| filter(res, &command_tag)), 74 | move |resp| { 75 | let unsolicited = unsolicited.clone(); 76 | 77 | async move { 78 | match resp { 79 | Ok(resp) => match resp.parsed() { 80 | Response::Fetch(..) => Some(Ok(Fetch::new(resp))), 81 | _ => { 82 | handle_unilateral(resp, unsolicited); 83 | None 84 | } 85 | }, 86 | Err(err) => Some(Err(err.into())), 87 | } 88 | } 89 | .boxed() 90 | }, 91 | ) 92 | } 93 | 94 | pub(crate) async fn parse_status> + Unpin + Send>( 95 | stream: &mut T, 96 | expected_mailbox: &str, 97 | unsolicited: channel::Sender, 98 | command_tag: RequestId, 99 | ) -> Result { 100 | let mut mbox = Mailbox::default(); 101 | 102 | while let Some(resp) = stream.next().await { 103 | let resp = resp?; 104 | match resp.parsed() { 105 | Response::Done { 106 | tag, 107 | status, 108 | code, 109 | information, 110 | .. 111 | } if tag == &command_tag => { 112 | use imap_proto::Status; 113 | match status { 114 | Status::Ok => { 115 | break; 116 | } 117 | Status::Bad => { 118 | return Err(Error::Bad(format!("code: {code:?}, info: {information:?}"))) 119 | } 120 | Status::No => { 121 | return Err(Error::No(format!("code: {code:?}, info: {information:?}"))) 122 | } 123 | _ => { 124 | return Err(Error::Io(io::Error::other(format!( 125 | "status: {status:?}, code: {code:?}, information: {information:?}" 126 | )))); 127 | } 128 | } 129 | } 130 | Response::MailboxData(MailboxDatum::Status { mailbox, status }) 131 | if mailbox == expected_mailbox => 132 | { 133 | for attribute in status { 134 | match attribute { 135 | StatusAttribute::HighestModSeq(highest_modseq) => { 136 | mbox.highest_modseq = Some(*highest_modseq) 137 | } 138 | StatusAttribute::Messages(exists) => mbox.exists = *exists, 139 | StatusAttribute::Recent(recent) => mbox.recent = *recent, 140 | StatusAttribute::UidNext(uid_next) => mbox.uid_next = Some(*uid_next), 141 | StatusAttribute::UidValidity(uid_validity) => { 142 | mbox.uid_validity = Some(*uid_validity) 143 | } 144 | StatusAttribute::Unseen(unseen) => mbox.unseen = Some(*unseen), 145 | _ => {} 146 | } 147 | } 148 | } 149 | _ => { 150 | handle_unilateral(resp, unsolicited.clone()); 151 | } 152 | } 153 | } 154 | 155 | Ok(mbox) 156 | } 157 | 158 | pub(crate) fn parse_expunge> + Unpin + Send>( 159 | stream: &mut T, 160 | unsolicited: channel::Sender, 161 | command_tag: RequestId, 162 | ) -> impl Stream> + '_ + Send { 163 | use futures::StreamExt; 164 | 165 | StreamExt::filter_map( 166 | StreamExt::take_while(stream, move |res| filter(res, &command_tag)), 167 | move |resp| { 168 | let unsolicited = unsolicited.clone(); 169 | 170 | async move { 171 | match resp { 172 | Ok(resp) => match resp.parsed() { 173 | Response::Expunge(id) => Some(Ok(*id)), 174 | _ => { 175 | handle_unilateral(resp, unsolicited); 176 | None 177 | } 178 | }, 179 | Err(err) => Some(Err(err.into())), 180 | } 181 | } 182 | }, 183 | ) 184 | } 185 | 186 | pub(crate) async fn parse_capabilities> + Unpin>( 187 | stream: &mut T, 188 | unsolicited: channel::Sender, 189 | command_tag: RequestId, 190 | ) -> Result { 191 | let mut caps: HashSet = HashSet::new(); 192 | 193 | while let Some(resp) = stream 194 | .take_while(|res| filter(res, &command_tag)) 195 | .next() 196 | .await 197 | { 198 | let resp = resp?; 199 | match resp.parsed() { 200 | Response::Capabilities(cs) => { 201 | for c in cs { 202 | caps.insert(Capability::from(c)); // TODO: avoid clone 203 | } 204 | } 205 | _ => { 206 | handle_unilateral(resp, unsolicited.clone()); 207 | } 208 | } 209 | } 210 | 211 | Ok(Capabilities(caps)) 212 | } 213 | 214 | pub(crate) async fn parse_noop> + Unpin>( 215 | stream: &mut T, 216 | unsolicited: channel::Sender, 217 | command_tag: RequestId, 218 | ) -> Result<()> { 219 | while let Some(resp) = stream 220 | .take_while(|res| filter(res, &command_tag)) 221 | .next() 222 | .await 223 | { 224 | let resp = resp?; 225 | handle_unilateral(resp, unsolicited.clone()); 226 | } 227 | 228 | Ok(()) 229 | } 230 | 231 | pub(crate) async fn parse_mailbox> + Unpin>( 232 | stream: &mut T, 233 | unsolicited: channel::Sender, 234 | command_tag: RequestId, 235 | ) -> Result { 236 | let mut mailbox = Mailbox::default(); 237 | 238 | while let Some(resp) = stream.next().await { 239 | let resp = resp?; 240 | match resp.parsed() { 241 | Response::Done { 242 | tag, 243 | status, 244 | code, 245 | information, 246 | .. 247 | } if tag == &command_tag => { 248 | use imap_proto::Status; 249 | match status { 250 | Status::Ok => { 251 | break; 252 | } 253 | Status::Bad => { 254 | return Err(Error::Bad(format!("code: {code:?}, info: {information:?}"))) 255 | } 256 | Status::No => { 257 | return Err(Error::No(format!("code: {code:?}, info: {information:?}"))) 258 | } 259 | _ => { 260 | return Err(Error::Io(io::Error::other(format!( 261 | "status: {status:?}, code: {code:?}, information: {information:?}" 262 | )))); 263 | } 264 | } 265 | } 266 | Response::Data { 267 | status, 268 | code, 269 | information, 270 | } => { 271 | use imap_proto::Status; 272 | 273 | match status { 274 | Status::Ok => { 275 | use imap_proto::ResponseCode; 276 | match code { 277 | Some(ResponseCode::UidValidity(uid)) => { 278 | mailbox.uid_validity = Some(*uid); 279 | } 280 | Some(ResponseCode::UidNext(unext)) => { 281 | mailbox.uid_next = Some(*unext); 282 | } 283 | Some(ResponseCode::HighestModSeq(highest_modseq)) => { 284 | mailbox.highest_modseq = Some(*highest_modseq); 285 | } 286 | Some(ResponseCode::Unseen(n)) => { 287 | mailbox.unseen = Some(*n); 288 | } 289 | Some(ResponseCode::PermanentFlags(flags)) => { 290 | mailbox 291 | .permanent_flags 292 | .extend(flags.iter().map(|s| (*s).to_string()).map(Flag::from)); 293 | } 294 | _ => {} 295 | } 296 | } 297 | Status::Bad => { 298 | return Err(Error::Bad(format!("code: {code:?}, info: {information:?}"))) 299 | } 300 | Status::No => { 301 | return Err(Error::No(format!("code: {code:?}, info: {information:?}"))) 302 | } 303 | _ => { 304 | return Err(Error::Io(io::Error::other(format!( 305 | "status: {status:?}, code: {code:?}, information: {information:?}" 306 | )))); 307 | } 308 | } 309 | } 310 | Response::MailboxData(m) => match m { 311 | MailboxDatum::Status { .. } => handle_unilateral(resp, unsolicited.clone()), 312 | MailboxDatum::Exists(e) => { 313 | mailbox.exists = *e; 314 | } 315 | MailboxDatum::Recent(r) => { 316 | mailbox.recent = *r; 317 | } 318 | MailboxDatum::Flags(flags) => { 319 | mailbox 320 | .flags 321 | .extend(flags.iter().map(|s| (*s).to_string()).map(Flag::from)); 322 | } 323 | MailboxDatum::List { .. } => {} 324 | MailboxDatum::MetadataSolicited { .. } => {} 325 | MailboxDatum::MetadataUnsolicited { .. } => {} 326 | MailboxDatum::Search { .. } => {} 327 | MailboxDatum::Sort { .. } => {} 328 | _ => {} 329 | }, 330 | _ => { 331 | handle_unilateral(resp, unsolicited.clone()); 332 | } 333 | } 334 | } 335 | 336 | Ok(mailbox) 337 | } 338 | 339 | pub(crate) async fn parse_ids> + Unpin>( 340 | stream: &mut T, 341 | unsolicited: channel::Sender, 342 | command_tag: RequestId, 343 | ) -> Result> { 344 | let mut ids: HashSet = HashSet::new(); 345 | 346 | while let Some(resp) = stream 347 | .take_while(|res| filter(res, &command_tag)) 348 | .next() 349 | .await 350 | { 351 | let resp = resp?; 352 | match resp.parsed() { 353 | Response::MailboxData(MailboxDatum::Search(cs)) => { 354 | for c in cs { 355 | ids.insert(*c); 356 | } 357 | } 358 | _ => { 359 | handle_unilateral(resp, unsolicited.clone()); 360 | } 361 | } 362 | } 363 | 364 | Ok(ids) 365 | } 366 | 367 | /// Parses [GETMETADATA](https://www.rfc-editor.org/info/rfc5464) response. 368 | pub(crate) async fn parse_metadata> + Unpin>( 369 | stream: &mut T, 370 | mailbox_name: &str, 371 | unsolicited: channel::Sender, 372 | command_tag: RequestId, 373 | ) -> Result> { 374 | let mut res_values = Vec::new(); 375 | while let Some(resp) = stream 376 | .take_while(|res| filter(res, &command_tag)) 377 | .next() 378 | .await 379 | { 380 | let resp = resp?; 381 | match resp.parsed() { 382 | // METADATA Response with Values 383 | // 384 | Response::MailboxData(MailboxDatum::MetadataSolicited { mailbox, values }) 385 | if mailbox == mailbox_name => 386 | { 387 | res_values.extend_from_slice(values.as_slice()); 388 | } 389 | 390 | // We are not interested in 391 | // [Unsolicited METADATA Response without Values](https://datatracker.ietf.org/doc/html/rfc5464.html#section-4.4.2), 392 | // they go to unsolicited channel with other unsolicited responses. 393 | _ => { 394 | handle_unilateral(resp, unsolicited.clone()); 395 | } 396 | } 397 | } 398 | Ok(res_values) 399 | } 400 | 401 | /// Sends unilateral server response 402 | /// (see Section 7 of RFC 3501) 403 | /// into the channel. 404 | /// 405 | /// If the channel is full or closed, 406 | /// i.e. the responses are not being consumed, 407 | /// ignores new responses. 408 | pub(crate) fn handle_unilateral( 409 | res: ResponseData, 410 | unsolicited: channel::Sender, 411 | ) { 412 | match res.parsed() { 413 | Response::MailboxData(MailboxDatum::Status { mailbox, status }) => { 414 | unsolicited 415 | .try_send(UnsolicitedResponse::Status { 416 | mailbox: (mailbox.as_ref()).into(), 417 | attributes: status.to_vec(), 418 | }) 419 | .ok(); 420 | } 421 | Response::MailboxData(MailboxDatum::Recent(n)) => { 422 | unsolicited.try_send(UnsolicitedResponse::Recent(*n)).ok(); 423 | } 424 | Response::MailboxData(MailboxDatum::Exists(n)) => { 425 | unsolicited.try_send(UnsolicitedResponse::Exists(*n)).ok(); 426 | } 427 | Response::Expunge(n) => { 428 | unsolicited.try_send(UnsolicitedResponse::Expunge(*n)).ok(); 429 | } 430 | _ => { 431 | unsolicited.try_send(UnsolicitedResponse::Other(res)).ok(); 432 | } 433 | } 434 | } 435 | 436 | #[cfg(test)] 437 | mod tests { 438 | use super::*; 439 | use async_channel::bounded; 440 | use bytes::BytesMut; 441 | 442 | fn input_stream(data: &[&str]) -> Vec> { 443 | data.iter() 444 | .map(|line| { 445 | let block = BytesMut::from(line.as_bytes()); 446 | ResponseData::try_new(block, |bytes| -> io::Result<_> { 447 | let (remaining, response) = imap_proto::parser::parse_response(bytes).unwrap(); 448 | assert_eq!(remaining.len(), 0); 449 | Ok(response) 450 | }) 451 | }) 452 | .collect() 453 | } 454 | 455 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 456 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 457 | async fn parse_capability_test() { 458 | let expected_capabilities = &["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"]; 459 | let responses = 460 | input_stream(&["* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"]); 461 | 462 | let mut stream = async_std::stream::from_iter(responses); 463 | let (send, recv) = bounded(10); 464 | let id = RequestId("A0001".into()); 465 | let capabilities = parse_capabilities(&mut stream, send, id).await.unwrap(); 466 | // shouldn't be any unexpected responses parsed 467 | assert!(recv.is_empty()); 468 | assert_eq!(capabilities.len(), 4); 469 | for e in expected_capabilities { 470 | assert!(capabilities.has_str(e)); 471 | } 472 | } 473 | 474 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 475 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 476 | async fn parse_capability_case_insensitive_test() { 477 | // Test that "IMAP4REV1" (instead of "IMAP4rev1") is accepted 478 | let expected_capabilities = &["IMAP4rev1", "STARTTLS"]; 479 | let responses = input_stream(&["* CAPABILITY IMAP4REV1 STARTTLS\r\n"]); 480 | let mut stream = async_std::stream::from_iter(responses); 481 | 482 | let (send, recv) = bounded(10); 483 | let id = RequestId("A0001".into()); 484 | let capabilities = parse_capabilities(&mut stream, send, id).await.unwrap(); 485 | 486 | // shouldn't be any unexpected responses parsed 487 | assert!(recv.is_empty()); 488 | assert_eq!(capabilities.len(), 2); 489 | for e in expected_capabilities { 490 | assert!(capabilities.has_str(e)); 491 | } 492 | } 493 | 494 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 495 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 496 | #[should_panic] 497 | async fn parse_capability_invalid_test() { 498 | let (send, recv) = bounded(10); 499 | let responses = input_stream(&["* JUNK IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"]); 500 | let mut stream = async_std::stream::from_iter(responses); 501 | 502 | let id = RequestId("A0001".into()); 503 | parse_capabilities(&mut stream, send.clone(), id) 504 | .await 505 | .unwrap(); 506 | assert!(recv.is_empty()); 507 | } 508 | 509 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 510 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 511 | async fn parse_names_test() { 512 | let (send, recv) = bounded(10); 513 | let responses = input_stream(&["* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n"]); 514 | let mut stream = async_std::stream::from_iter(responses); 515 | 516 | let id = RequestId("A0001".into()); 517 | let names: Vec<_> = parse_names(&mut stream, send, id) 518 | .try_collect::>() 519 | .await 520 | .unwrap(); 521 | assert!(recv.is_empty()); 522 | assert_eq!(names.len(), 1); 523 | assert_eq!( 524 | names[0].attributes(), 525 | &[NameAttribute::Extension("\\HasNoChildren".into())] 526 | ); 527 | assert_eq!(names[0].delimiter(), Some(".")); 528 | assert_eq!(names[0].name(), "INBOX"); 529 | } 530 | 531 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 532 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 533 | async fn parse_fetches_empty() { 534 | let (send, recv) = bounded(10); 535 | let responses = input_stream(&[]); 536 | let mut stream = async_std::stream::from_iter(responses); 537 | let id = RequestId("a".into()); 538 | 539 | let fetches = parse_fetches(&mut stream, send, id) 540 | .try_collect::>() 541 | .await 542 | .unwrap(); 543 | assert!(recv.is_empty()); 544 | assert!(fetches.is_empty()); 545 | } 546 | 547 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 548 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 549 | async fn parse_fetches_test() { 550 | let (send, recv) = bounded(10); 551 | let responses = input_stream(&[ 552 | "* 24 FETCH (FLAGS (\\Seen) UID 4827943)\r\n", 553 | "* 25 FETCH (FLAGS (\\Seen))\r\n", 554 | ]); 555 | let mut stream = async_std::stream::from_iter(responses); 556 | let id = RequestId("a".into()); 557 | 558 | let fetches = parse_fetches(&mut stream, send, id) 559 | .try_collect::>() 560 | .await 561 | .unwrap(); 562 | assert!(recv.is_empty()); 563 | 564 | assert_eq!(fetches.len(), 2); 565 | assert_eq!(fetches[0].message, 24); 566 | assert_eq!(fetches[0].flags().collect::>(), vec![Flag::Seen]); 567 | assert_eq!(fetches[0].uid, Some(4827943)); 568 | assert_eq!(fetches[0].body(), None); 569 | assert_eq!(fetches[0].header(), None); 570 | assert_eq!(fetches[1].message, 25); 571 | assert_eq!(fetches[1].flags().collect::>(), vec![Flag::Seen]); 572 | assert_eq!(fetches[1].uid, None); 573 | assert_eq!(fetches[1].body(), None); 574 | assert_eq!(fetches[1].header(), None); 575 | } 576 | 577 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 578 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 579 | async fn parse_fetches_w_unilateral() { 580 | // https://github.com/mattnenterprise/rust-imap/issues/81 581 | let (send, recv) = bounded(10); 582 | let responses = input_stream(&["* 37 FETCH (UID 74)\r\n", "* 1 RECENT\r\n"]); 583 | let mut stream = async_std::stream::from_iter(responses); 584 | let id = RequestId("a".into()); 585 | 586 | let fetches = parse_fetches(&mut stream, send, id) 587 | .try_collect::>() 588 | .await 589 | .unwrap(); 590 | assert_eq!(recv.recv().await.unwrap(), UnsolicitedResponse::Recent(1)); 591 | 592 | assert_eq!(fetches.len(), 1); 593 | assert_eq!(fetches[0].message, 37); 594 | assert_eq!(fetches[0].uid, Some(74)); 595 | } 596 | 597 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 598 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 599 | async fn parse_names_w_unilateral() { 600 | let (send, recv) = bounded(10); 601 | let responses = input_stream(&[ 602 | "* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n", 603 | "* 4 EXPUNGE\r\n", 604 | ]); 605 | let mut stream = async_std::stream::from_iter(responses); 606 | 607 | let id = RequestId("A0001".into()); 608 | let names = parse_names(&mut stream, send, id) 609 | .try_collect::>() 610 | .await 611 | .unwrap(); 612 | 613 | assert_eq!(recv.recv().await.unwrap(), UnsolicitedResponse::Expunge(4)); 614 | 615 | assert_eq!(names.len(), 1); 616 | assert_eq!( 617 | names[0].attributes(), 618 | &[NameAttribute::Extension("\\HasNoChildren".into())] 619 | ); 620 | assert_eq!(names[0].delimiter(), Some(".")); 621 | assert_eq!(names[0].name(), "INBOX"); 622 | } 623 | 624 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 625 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 626 | async fn parse_capabilities_w_unilateral() { 627 | let (send, recv) = bounded(10); 628 | let responses = input_stream(&[ 629 | "* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n", 630 | "* STATUS dev.github (MESSAGES 10 UIDNEXT 11 UIDVALIDITY 1408806928 UNSEEN 0)\r\n", 631 | "* 4 EXISTS\r\n", 632 | ]); 633 | let mut stream = async_std::stream::from_iter(responses); 634 | 635 | let expected_capabilities = &["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"]; 636 | 637 | let id = RequestId("A0001".into()); 638 | let capabilities = parse_capabilities(&mut stream, send, id).await.unwrap(); 639 | 640 | assert_eq!(capabilities.len(), 4); 641 | for e in expected_capabilities { 642 | assert!(capabilities.has_str(e)); 643 | } 644 | 645 | assert_eq!( 646 | recv.recv().await.unwrap(), 647 | UnsolicitedResponse::Status { 648 | mailbox: "dev.github".to_string(), 649 | attributes: vec![ 650 | StatusAttribute::Messages(10), 651 | StatusAttribute::UidNext(11), 652 | StatusAttribute::UidValidity(1408806928), 653 | StatusAttribute::Unseen(0) 654 | ] 655 | } 656 | ); 657 | assert_eq!(recv.recv().await.unwrap(), UnsolicitedResponse::Exists(4)); 658 | } 659 | 660 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 661 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 662 | async fn parse_ids_w_unilateral() { 663 | let (send, recv) = bounded(10); 664 | let responses = input_stream(&[ 665 | "* SEARCH 23 42 4711\r\n", 666 | "* 1 RECENT\r\n", 667 | "* STATUS INBOX (MESSAGES 10 UIDNEXT 11 UIDVALIDITY 1408806928 UNSEEN 0)\r\n", 668 | ]); 669 | let mut stream = async_std::stream::from_iter(responses); 670 | 671 | let id = RequestId("A0001".into()); 672 | let ids = parse_ids(&mut stream, send, id).await.unwrap(); 673 | 674 | assert_eq!(ids, [23, 42, 4711].iter().cloned().collect()); 675 | 676 | assert_eq!(recv.recv().await.unwrap(), UnsolicitedResponse::Recent(1)); 677 | assert_eq!( 678 | recv.recv().await.unwrap(), 679 | UnsolicitedResponse::Status { 680 | mailbox: "INBOX".to_string(), 681 | attributes: vec![ 682 | StatusAttribute::Messages(10), 683 | StatusAttribute::UidNext(11), 684 | StatusAttribute::UidValidity(1408806928), 685 | StatusAttribute::Unseen(0) 686 | ] 687 | } 688 | ); 689 | } 690 | 691 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 692 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 693 | async fn parse_ids_test() { 694 | let (send, recv) = bounded(10); 695 | let responses = input_stream(&[ 696 | "* SEARCH 1600 1698 1739 1781 1795 1885 1891 1892 1893 1898 1899 1901 1911 1926 1932 1933 1993 1994 2007 2032 2033 2041 2053 2062 2063 2065 2066 2072 2078 2079 2082 2084 2095 2100 2101 2102 2103 2104 2107 2116 2120 2135 2138 2154 2163 2168 2172 2189 2193 2198 2199 2205 2212 2213 2221 2227 2267 2275 2276 2295 2300 2328 2330 2332 2333 2334\r\n", 697 | "* SEARCH 2335 2336 2337 2338 2339 2341 2342 2347 2349 2350 2358 2359 2362 2369 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2390 2392 2397 2400 2401 2403 2405 2409 2411 2414 2417 2419 2420 2424 2426 2428 2439 2454 2456 2467 2468 2469 2490 2515 2519 2520 2521\r\n", 698 | ]); 699 | let mut stream = async_std::stream::from_iter(responses); 700 | 701 | let id = RequestId("A0001".into()); 702 | let ids = parse_ids(&mut stream, send, id).await.unwrap(); 703 | 704 | assert!(recv.is_empty()); 705 | let ids: HashSet = ids.iter().cloned().collect(); 706 | assert_eq!( 707 | ids, 708 | [ 709 | 1600, 1698, 1739, 1781, 1795, 1885, 1891, 1892, 1893, 1898, 1899, 1901, 1911, 1926, 710 | 1932, 1933, 1993, 1994, 2007, 2032, 2033, 2041, 2053, 2062, 2063, 2065, 2066, 2072, 711 | 2078, 2079, 2082, 2084, 2095, 2100, 2101, 2102, 2103, 2104, 2107, 2116, 2120, 2135, 712 | 2138, 2154, 2163, 2168, 2172, 2189, 2193, 2198, 2199, 2205, 2212, 2213, 2221, 2227, 713 | 2267, 2275, 2276, 2295, 2300, 2328, 2330, 2332, 2333, 2334, 2335, 2336, 2337, 2338, 714 | 2339, 2341, 2342, 2347, 2349, 2350, 2358, 2359, 2362, 2369, 2371, 2372, 2373, 2374, 715 | 2375, 2376, 2377, 2378, 2379, 2380, 2381, 2382, 2383, 2384, 2385, 2386, 2390, 2392, 716 | 2397, 2400, 2401, 2403, 2405, 2409, 2411, 2414, 2417, 2419, 2420, 2424, 2426, 2428, 717 | 2439, 2454, 2456, 2467, 2468, 2469, 2490, 2515, 2519, 2520, 2521 718 | ] 719 | .iter() 720 | .cloned() 721 | .collect() 722 | ); 723 | } 724 | 725 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 726 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 727 | async fn parse_ids_search() { 728 | let (send, recv) = bounded(10); 729 | let responses = input_stream(&["* SEARCH\r\n"]); 730 | let mut stream = async_std::stream::from_iter(responses); 731 | 732 | let id = RequestId("A0001".into()); 733 | let ids = parse_ids(&mut stream, send, id).await.unwrap(); 734 | 735 | assert!(recv.is_empty()); 736 | let ids: HashSet = ids.iter().cloned().collect(); 737 | assert_eq!(ids, HashSet::::new()); 738 | } 739 | 740 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 741 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 742 | async fn parse_mailbox_does_not_exist_error() { 743 | let (send, recv) = bounded(10); 744 | let responses = input_stream(&[ 745 | "A0003 NO Mailbox doesn't exist: DeltaChat (0.001 + 0.140 + 0.139 secs).\r\n", 746 | ]); 747 | let mut stream = async_std::stream::from_iter(responses); 748 | 749 | let id = RequestId("A0003".into()); 750 | let mailbox = parse_mailbox(&mut stream, send, id).await; 751 | assert!(recv.is_empty()); 752 | 753 | assert!(matches!(mailbox, Err(Error::No(_)))); 754 | } 755 | } 756 | -------------------------------------------------------------------------------- /src/types/capabilities.rs: -------------------------------------------------------------------------------- 1 | use imap_proto::types::Capability as CapabilityRef; 2 | use std::collections::hash_set::Iter; 3 | use std::collections::HashSet; 4 | 5 | const IMAP4REV1_CAPABILITY: &str = "IMAP4rev1"; 6 | const AUTH_CAPABILITY_PREFIX: &str = "AUTH="; 7 | 8 | /// List of available Capabilities. 9 | #[derive(Debug, Eq, PartialEq, Hash)] 10 | pub enum Capability { 11 | /// The crucial imap capability. 12 | Imap4rev1, 13 | /// Auth type capability. 14 | Auth(String), 15 | /// Any other atoms. 16 | Atom(String), 17 | } 18 | 19 | impl From<&CapabilityRef<'_>> for Capability { 20 | fn from(c: &CapabilityRef<'_>) -> Self { 21 | match c { 22 | CapabilityRef::Imap4rev1 => Capability::Imap4rev1, 23 | CapabilityRef::Auth(s) => Capability::Auth(s.clone().into_owned()), 24 | CapabilityRef::Atom(s) => Capability::Atom(s.clone().into_owned()), 25 | } 26 | } 27 | } 28 | 29 | /// From [section 7.2.1 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-7.2.1). 30 | /// 31 | /// A list of capabilities that the server supports. 32 | /// The capability list will include the atom "IMAP4rev1". 33 | /// 34 | /// In addition, all servers implement the `STARTTLS`, `LOGINDISABLED`, and `AUTH=PLAIN` (described 35 | /// in [IMAP-TLS](https://tools.ietf.org/html/rfc2595)) capabilities. See the [Security 36 | /// Considerations section of the RFC](https://tools.ietf.org/html/rfc3501#section-11) for 37 | /// important information. 38 | /// 39 | /// A capability name which begins with `AUTH=` indicates that the server supports that particular 40 | /// authentication mechanism. 41 | /// 42 | /// The `LOGINDISABLED` capability indicates that the `LOGIN` command is disabled, and that the 43 | /// server will respond with a [`crate::error::Error::No`] response to any attempt to use the `LOGIN` 44 | /// command even if the user name and password are valid. An IMAP client MUST NOT issue the 45 | /// `LOGIN` command if the server advertises the `LOGINDISABLED` capability. 46 | /// 47 | /// Other capability names indicate that the server supports an extension, revision, or amendment 48 | /// to the IMAP4rev1 protocol. Capability names either begin with `X` or they are standard or 49 | /// standards-track [RFC 3501](https://tools.ietf.org/html/rfc3501) extensions, revisions, or 50 | /// amendments registered with IANA. 51 | /// 52 | /// Client implementations SHOULD NOT require any capability name other than `IMAP4rev1`, and MUST 53 | /// ignore any unknown capability names. 54 | pub struct Capabilities(pub(crate) HashSet); 55 | 56 | impl Capabilities { 57 | /// Check if the server has the given capability. 58 | pub fn has(&self, cap: &Capability) -> bool { 59 | self.0.contains(cap) 60 | } 61 | 62 | /// Check if the server has the given capability via str. 63 | pub fn has_str>(&self, cap: S) -> bool { 64 | let s = cap.as_ref(); 65 | if s.eq_ignore_ascii_case(IMAP4REV1_CAPABILITY) { 66 | return self.has(&Capability::Imap4rev1); 67 | } 68 | if s.len() > AUTH_CAPABILITY_PREFIX.len() { 69 | let (pre, val) = s.split_at(AUTH_CAPABILITY_PREFIX.len()); 70 | if pre.eq_ignore_ascii_case(AUTH_CAPABILITY_PREFIX) { 71 | return self.has(&Capability::Auth(val.into())); // TODO: avoid clone 72 | } 73 | } 74 | self.has(&Capability::Atom(s.into())) // TODO: avoid clone 75 | } 76 | 77 | /// Iterate over all the server's capabilities 78 | pub fn iter(&self) -> Iter<'_, Capability> { 79 | self.0.iter() 80 | } 81 | 82 | /// Returns how many capabilities the server has. 83 | pub fn len(&self) -> usize { 84 | self.0.len() 85 | } 86 | 87 | /// Returns true if the server purports to have no capabilities. 88 | pub fn is_empty(&self) -> bool { 89 | self.0.is_empty() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/types/fetch.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, FixedOffset}; 2 | use imap_proto::types::{ 3 | AttributeValue, BodyStructure, Envelope, MessageSection, Response, SectionPath, 4 | }; 5 | 6 | use super::{Flag, Seq, Uid}; 7 | use crate::types::ResponseData; 8 | 9 | /// Format of Date and Time as defined RFC3501. 10 | /// See `date-time` element in [Formal Syntax](https://tools.ietf.org/html/rfc3501#section-9) 11 | /// chapter of this RFC. 12 | const DATE_TIME_FORMAT: &str = "%d-%b-%Y %H:%M:%S %z"; 13 | 14 | /// An IMAP [`FETCH` response](https://tools.ietf.org/html/rfc3501#section-7.4.2) that contains 15 | /// data about a particular message. 16 | /// 17 | /// This response occurs as the result of a `FETCH` or `STORE` 18 | /// command, as well as by unilateral server decision (e.g., flag updates). 19 | #[derive(Debug)] 20 | pub struct Fetch { 21 | response: ResponseData, 22 | /// The ordinal number of this message in its containing mailbox. 23 | pub message: Seq, 24 | 25 | /// A number expressing the unique identifier of the message. 26 | /// Only present if `UID` was specified in the query argument to `FETCH` and the server 27 | /// supports UIDs. 28 | pub uid: Option, 29 | 30 | /// A number expressing the [RFC-2822](https://tools.ietf.org/html/rfc2822) size of the message. 31 | /// Only present if `RFC822.SIZE` was specified in the query argument to `FETCH`. 32 | pub size: Option, 33 | 34 | /// A number expressing the [RFC-7162](https://tools.ietf.org/html/rfc7162) mod-sequence 35 | /// of the message. 36 | pub modseq: Option, 37 | } 38 | 39 | impl Fetch { 40 | pub(crate) fn new(response: ResponseData) -> Self { 41 | let (message, uid, size, modseq) = 42 | if let Response::Fetch(message, attrs) = response.parsed() { 43 | let mut uid = None; 44 | let mut size = None; 45 | let mut modseq = None; 46 | 47 | for attr in attrs { 48 | match attr { 49 | AttributeValue::Uid(id) => uid = Some(*id), 50 | AttributeValue::Rfc822Size(sz) => size = Some(*sz), 51 | AttributeValue::ModSeq(ms) => modseq = Some(*ms), 52 | _ => {} 53 | } 54 | } 55 | (*message, uid, size, modseq) 56 | } else { 57 | unreachable!() 58 | }; 59 | 60 | Fetch { 61 | response, 62 | message, 63 | uid, 64 | size, 65 | modseq, 66 | } 67 | } 68 | 69 | /// A list of flags that are set for this message. 70 | pub fn flags(&self) -> impl Iterator> { 71 | if let Response::Fetch(_, attrs) = self.response.parsed() { 72 | attrs 73 | .iter() 74 | .filter_map(|attr| match attr { 75 | AttributeValue::Flags(raw_flags) => { 76 | Some(raw_flags.iter().map(|s| Flag::from(s.as_ref()))) 77 | } 78 | _ => None, 79 | }) 80 | .flatten() 81 | } else { 82 | unreachable!() 83 | } 84 | } 85 | 86 | /// The bytes that make up the header of this message, if `BODY[HEADER]`, `BODY.PEEK[HEADER]`, 87 | /// or `RFC822.HEADER` was included in the `query` argument to `FETCH`. 88 | pub fn header(&self) -> Option<&[u8]> { 89 | if let Response::Fetch(_, attrs) = self.response.parsed() { 90 | attrs 91 | .iter() 92 | .filter_map(|av| match av { 93 | AttributeValue::BodySection { 94 | section: Some(SectionPath::Full(MessageSection::Header)), 95 | data: Some(hdr), 96 | .. 97 | } 98 | | AttributeValue::Rfc822Header(Some(hdr)) => Some(hdr.as_ref()), 99 | _ => None, 100 | }) 101 | .next() 102 | } else { 103 | unreachable!() 104 | } 105 | } 106 | 107 | /// The bytes that make up this message, included if `BODY[]` or `RFC822` was included in the 108 | /// `query` argument to `FETCH`. The bytes SHOULD be interpreted by the client according to the 109 | /// content transfer encoding, body type, and subtype. 110 | pub fn body(&self) -> Option<&[u8]> { 111 | if let Response::Fetch(_, attrs) = self.response.parsed() { 112 | attrs 113 | .iter() 114 | .filter_map(|av| match av { 115 | AttributeValue::BodySection { 116 | section: None, 117 | data: Some(body), 118 | .. 119 | } 120 | | AttributeValue::Rfc822(Some(body)) => Some(body.as_ref()), 121 | _ => None, 122 | }) 123 | .next() 124 | } else { 125 | unreachable!() 126 | } 127 | } 128 | 129 | /// The bytes that make up the text of this message, included if `BODY[TEXT]`, `RFC822.TEXT`, 130 | /// or `BODY.PEEK[TEXT]` was included in the `query` argument to `FETCH`. The bytes SHOULD be 131 | /// interpreted by the client according to the content transfer encoding, body type, and 132 | /// subtype. 133 | pub fn text(&self) -> Option<&[u8]> { 134 | if let Response::Fetch(_, attrs) = self.response.parsed() { 135 | attrs 136 | .iter() 137 | .filter_map(|av| match av { 138 | AttributeValue::BodySection { 139 | section: Some(SectionPath::Full(MessageSection::Text)), 140 | data: Some(body), 141 | .. 142 | } 143 | | AttributeValue::Rfc822Text(Some(body)) => Some(body.as_ref()), 144 | _ => None, 145 | }) 146 | .next() 147 | } else { 148 | unreachable!() 149 | } 150 | } 151 | 152 | /// The envelope of this message, if `ENVELOPE` was included in the `query` argument to 153 | /// `FETCH`. This is computed by the server by parsing the 154 | /// [RFC-2822](https://tools.ietf.org/html/rfc2822) header into the component parts, defaulting 155 | /// various fields as necessary. 156 | /// 157 | /// The full description of the format of the envelope is given in [RFC 3501 section 158 | /// 7.4.2](https://tools.ietf.org/html/rfc3501#section-7.4.2). 159 | pub fn envelope(&self) -> Option<&Envelope<'_>> { 160 | if let Response::Fetch(_, attrs) = self.response.parsed() { 161 | attrs 162 | .iter() 163 | .filter_map(|av| match av { 164 | AttributeValue::Envelope(env) => Some(&**env), 165 | _ => None, 166 | }) 167 | .next() 168 | } else { 169 | unreachable!() 170 | } 171 | } 172 | 173 | /// Extract the bytes that makes up the given `BOD[
]` of a `FETCH` response. 174 | /// 175 | /// See [section 7.4.2 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-7.4.2) for 176 | /// details. 177 | pub fn section(&self, path: &SectionPath) -> Option<&[u8]> { 178 | if let Response::Fetch(_, attrs) = self.response.parsed() { 179 | attrs 180 | .iter() 181 | .filter_map(|av| match av { 182 | AttributeValue::BodySection { 183 | section: Some(sp), 184 | data: Some(data), 185 | .. 186 | } if sp == path => Some(data.as_ref()), 187 | _ => None, 188 | }) 189 | .next() 190 | } else { 191 | unreachable!() 192 | } 193 | } 194 | 195 | /// Extract the `INTERNALDATE` of a `FETCH` response 196 | /// 197 | /// See [section 2.3.3 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.3) for 198 | /// details. 199 | pub fn internal_date(&self) -> Option> { 200 | if let Response::Fetch(_, attrs) = self.response.parsed() { 201 | attrs 202 | .iter() 203 | .filter_map(|av| match av { 204 | AttributeValue::InternalDate(date_time) => Some(date_time.as_ref()), 205 | _ => None, 206 | }) 207 | .next() 208 | .and_then(|date_time| DateTime::parse_from_str(date_time, DATE_TIME_FORMAT).ok()) 209 | } else { 210 | unreachable!() 211 | } 212 | } 213 | 214 | /// Extract the `BODYSTRUCTURE` of a `FETCH` response 215 | /// 216 | /// See [section 2.3.6 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.6) for 217 | /// details. 218 | pub fn bodystructure(&self) -> Option<&BodyStructure<'_>> { 219 | if let Response::Fetch(_, attrs) = self.response.parsed() { 220 | attrs 221 | .iter() 222 | .filter_map(|av| match av { 223 | AttributeValue::BodyStructure(bs) => Some(bs), 224 | _ => None, 225 | }) 226 | .next() 227 | } else { 228 | unreachable!() 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/types/id_generator.rs: -------------------------------------------------------------------------------- 1 | use imap_proto::RequestId; 2 | 3 | /// Request ID generator. 4 | #[derive(Debug)] 5 | pub struct IdGenerator { 6 | /// Last returned ID. 7 | next: u64, 8 | } 9 | 10 | impl IdGenerator { 11 | /// Creates a new request ID generator. 12 | pub fn new() -> Self { 13 | Self { next: 0 } 14 | } 15 | } 16 | 17 | impl Default for IdGenerator { 18 | fn default() -> Self { 19 | Self::new() 20 | } 21 | } 22 | 23 | impl Iterator for IdGenerator { 24 | type Item = RequestId; 25 | fn next(&mut self) -> Option { 26 | self.next += 1; 27 | Some(RequestId(format!("A{:04}", self.next % 10_000))) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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, Default)] 7 | pub struct Mailbox { 8 | /// Defined flags in the mailbox. See the description of the [FLAGS 9 | /// response](https://tools.ietf.org/html/rfc3501#section-7.2.6) for more detail. 10 | pub flags: Vec>, 11 | 12 | /// The number of messages in the mailbox. See the description of the [EXISTS 13 | /// response](https://tools.ietf.org/html/rfc3501#section-7.3.1) for more detail. 14 | pub exists: u32, 15 | 16 | /// The number of messages with the \Recent flag set. See the description of the [RECENT 17 | /// response](https://tools.ietf.org/html/rfc3501#section-7.3.2) for more detail. 18 | pub recent: u32, 19 | 20 | /// The message sequence number of the first unseen message in the mailbox. If this is 21 | /// missing, the client can not make any assumptions about the first unseen message in the 22 | /// mailbox, and needs to issue a `SEARCH` command if it wants to find it. 23 | pub unseen: Option, 24 | 25 | /// A list of message flags that the client can change permanently. If this is missing, the 26 | /// client should assume that all flags can be changed permanently. If the client attempts to 27 | /// STORE a flag that is not in this list list, the server will either ignore the change or 28 | /// store the state change for the remainder of the current session only. 29 | pub permanent_flags: Vec>, 30 | 31 | /// The next unique identifier value. If this is missing, the client can not make any 32 | /// assumptions about the next unique identifier value. 33 | pub uid_next: Option, 34 | 35 | /// The unique identifier validity value. See [`Uid`] for more details. If this is missing, 36 | /// the server does not support unique identifiers. 37 | pub uid_validity: Option, 38 | 39 | /// Highest mailbox mod-sequence as defined in [RFC-7162](https://tools.ietf.org/html/rfc7162). 40 | pub highest_modseq: Option, 41 | } 42 | 43 | impl fmt::Display for Mailbox { 44 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 45 | write!( 46 | f, 47 | "flags: {:?}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?},\ 48 | uid_next: {:?}, uid_validity: {:?}", 49 | self.flags, 50 | self.exists, 51 | self.recent, 52 | self.unseen, 53 | self.permanent_flags, 54 | self.uid_next, 55 | self.uid_validity 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/types/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains types used throughout the IMAP protocol. 2 | 3 | use std::borrow::Cow; 4 | 5 | /// From section [2.3.1.1 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.1.1). 6 | /// 7 | /// A 32-bit value assigned to each message, which when used with the unique identifier validity 8 | /// value (see below) forms a 64-bit value that will not refer to any other message in the mailbox 9 | /// or any subsequent mailbox with the same name forever. Unique identifiers are assigned in a 10 | /// strictly ascending fashion in the mailbox; as each message is added to the mailbox it is 11 | /// assigned a higher UID than the message(s) which were added previously. Unlike message sequence 12 | /// numbers, unique identifiers are not necessarily contiguous. 13 | /// 14 | /// The unique identifier of a message will not change during the session, and will generally not 15 | /// change between sessions. Any change of unique identifiers between sessions will be detectable 16 | /// using the `UIDVALIDITY` mechanism discussed below. Persistent unique identifiers are required 17 | /// for a client to resynchronize its state from a previous session with the server (e.g., 18 | /// disconnected or offline access clients); this is discussed further in 19 | /// [`IMAP-DISC`](https://tools.ietf.org/html/rfc3501#ref-IMAP-DISC). 20 | /// 21 | /// Associated with every mailbox are two values which aid in unique identifier handling: the next 22 | /// unique identifier value and the unique identifier validity value. 23 | /// 24 | /// The next unique identifier value is the predicted value that will be assigned to a new message 25 | /// in the mailbox. Unless the unique identifier validity also changes (see below), the next 26 | /// unique identifier value will have the following two characteristics. First, the next unique 27 | /// identifier value will not change unless new messages are added to the mailbox; and second, the 28 | /// next unique identifier value will change whenever new messages are added to the mailbox, even 29 | /// if those new messages are subsequently expunged. 30 | /// 31 | /// > Note: The next unique identifier value is intended to provide a means for a client to 32 | /// > determine whether any messages have been delivered to the mailbox since the previous time it 33 | /// > checked this value. It is not intended to provide any guarantee that any message will have 34 | /// > this unique identifier. A client can only assume, at the time that it obtains the next 35 | /// > unique identifier value, that messages arriving after that time will have a UID greater than 36 | /// > or equal to that value. 37 | /// 38 | /// The unique identifier validity value is sent in a `UIDVALIDITY` response code in an `OK` 39 | /// untagged response at mailbox selection time. If unique identifiers from an earlier session fail 40 | /// to persist in this session, the unique identifier validity value will be greater than the one 41 | /// used in the earlier session. 42 | /// 43 | /// > Note: Ideally, unique identifiers will persist at all 44 | /// > times. Although this specification recognizes that failure 45 | /// > to persist can be unavoidable in certain server 46 | /// > environments, it STRONGLY ENCOURAGES message store 47 | /// > implementation techniques that avoid this problem. For 48 | /// > example: 49 | /// > 50 | /// > 1. Unique identifiers are strictly ascending in the 51 | /// > mailbox at all times. If the physical message store is 52 | /// > re-ordered by a non-IMAP agent, this requires that the 53 | /// > unique identifiers in the mailbox be regenerated, since 54 | /// > the former unique identifiers are no longer strictly 55 | /// > ascending as a result of the re-ordering. 56 | /// > 2. If the message store has no mechanism to store unique 57 | /// > identifiers, it must regenerate unique identifiers at 58 | /// > each session, and each session must have a unique 59 | /// > `UIDVALIDITY` value. 60 | /// > 3. If the mailbox is deleted and a new mailbox with the 61 | /// > same name is created at a later date, the server must 62 | /// > either keep track of unique identifiers from the 63 | /// > previous instance of the mailbox, or it must assign a 64 | /// > new `UIDVALIDITY` value to the new instance of the 65 | /// > mailbox. A good `UIDVALIDITY` value to use in this case 66 | /// > is a 32-bit representation of the creation date/time of 67 | /// > the mailbox. It is alright to use a constant such as 68 | /// > 1, but only if it guaranteed that unique identifiers 69 | /// > will never be reused, even in the case of a mailbox 70 | /// > being deleted (or renamed) and a new mailbox by the 71 | /// > same name created at some future time. 72 | /// > 4. The combination of mailbox name, `UIDVALIDITY`, and `UID` 73 | /// > must refer to a single immutable message on that server 74 | /// > forever. In particular, the internal date, [RFC 2822](https://tools.ietf.org/html/rfc2822) 75 | /// > size, envelope, body structure, and message texts 76 | /// > (RFC822, RFC822.HEADER, RFC822.TEXT, and all BODY[...] 77 | /// > fetch data items) must never change. This does not 78 | /// > include message numbers, nor does it include attributes 79 | /// > that can be set by a `STORE` command (e.g., `FLAGS`). 80 | pub type Uid = u32; 81 | 82 | /// From section [2.3.1.2 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.1.2). 83 | /// 84 | /// A relative position from 1 to the number of messages in the mailbox. 85 | /// This position is ordered by ascending unique identifier. As 86 | /// each new message is added, it is assigned a message sequence number 87 | /// that is 1 higher than the number of messages in the mailbox before 88 | /// that new message was added. 89 | /// 90 | /// Message sequence numbers can be reassigned during the session. For 91 | /// example, when a message is permanently removed (expunged) from the 92 | /// mailbox, the message sequence number for all subsequent messages is 93 | /// decremented. The number of messages in the mailbox is also 94 | /// decremented. Similarly, a new message can be assigned a message 95 | /// sequence number that was once held by some other message prior to an 96 | /// expunge. 97 | /// 98 | /// In addition to accessing messages by relative position in the 99 | /// mailbox, message sequence numbers can be used in mathematical 100 | /// calculations. For example, if an untagged "11 EXISTS" is received, 101 | /// and previously an untagged "8 EXISTS" was received, three new 102 | /// messages have arrived with message sequence numbers of 9, 10, and 11. 103 | /// Another example, if message 287 in a 523 message mailbox has UID 104 | /// 12345, there are exactly 286 messages which have lesser UIDs and 236 105 | /// messages which have greater UIDs. 106 | pub type Seq = u32; 107 | 108 | /// Message flags. 109 | /// 110 | /// With the exception of [`Flag::Custom`], these flags are system flags that are pre-defined in 111 | /// [RFC 3501 section 2.3.2](https://tools.ietf.org/html/rfc3501#section-2.3.2). All system flags 112 | /// begin with `\` in the IMAP protocol. Certain system flags (`\Deleted` and `\Seen`) have 113 | /// special semantics described elsewhere. 114 | /// 115 | /// A flag can be permanent or session-only on a per-flag basis. Permanent flags are those which 116 | /// the client can add or remove from the message flags permanently; that is, concurrent and 117 | /// subsequent sessions will see any change in permanent flags. Changes to session flags are valid 118 | /// only in that session. 119 | /// 120 | /// > Note: The `\Recent` system flag is a special case of a session flag. `\Recent` can not be 121 | /// > used as an argument in a `STORE` or `APPEND` command, and thus can not be changed at all. 122 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 123 | pub enum Flag<'a> { 124 | /// Message has been read 125 | Seen, 126 | 127 | /// Message has been answered 128 | Answered, 129 | 130 | /// Message is "flagged" for urgent/special attention 131 | Flagged, 132 | 133 | /// Message is "deleted" for removal by later EXPUNGE 134 | Deleted, 135 | 136 | /// Message has not completed composition (marked as a draft). 137 | Draft, 138 | 139 | /// Message is "recently" arrived in this mailbox. This session is the first session to have 140 | /// been notified about this message; if the session is read-write, subsequent sessions will 141 | /// not see `\Recent` set for this message. This flag can not be altered by the client. 142 | /// 143 | /// If it is not possible to determine whether or not this session is the first session to be 144 | /// notified about a message, then that message will generally be considered recent. 145 | /// 146 | /// If multiple connections have the same mailbox selected simultaneously, it is undefined 147 | /// which of these connections will see newly-arrived messages with `\Recent` set and which 148 | /// will see it without `\Recent` set. 149 | Recent, 150 | 151 | /// The [`Mailbox::permanent_flags`] can include this special flag (`\*`), which indicates that 152 | /// it is possible to create new keywords by attempting to store those flags in the mailbox. 153 | MayCreate, 154 | 155 | /// A non-standard user- or server-defined flag. 156 | Custom(Cow<'a, str>), 157 | } 158 | 159 | impl Flag<'static> { 160 | fn system(s: &str) -> Option { 161 | match s { 162 | "\\Seen" => Some(Flag::Seen), 163 | "\\Answered" => Some(Flag::Answered), 164 | "\\Flagged" => Some(Flag::Flagged), 165 | "\\Deleted" => Some(Flag::Deleted), 166 | "\\Draft" => Some(Flag::Draft), 167 | "\\Recent" => Some(Flag::Recent), 168 | "\\*" => Some(Flag::MayCreate), 169 | _ => None, 170 | } 171 | } 172 | } 173 | 174 | impl From for Flag<'_> { 175 | fn from(s: String) -> Self { 176 | if let Some(f) = Flag::system(&s) { 177 | f 178 | } else { 179 | Flag::Custom(Cow::Owned(s)) 180 | } 181 | } 182 | } 183 | 184 | impl<'a> From<&'a str> for Flag<'a> { 185 | fn from(s: &'a str) -> Self { 186 | if let Some(f) = Flag::system(s) { 187 | f 188 | } else { 189 | Flag::Custom(Cow::Borrowed(s)) 190 | } 191 | } 192 | } 193 | 194 | mod mailbox; 195 | pub use self::mailbox::Mailbox; 196 | 197 | mod fetch; 198 | pub use self::fetch::Fetch; 199 | 200 | mod name; 201 | pub use self::name::{Name, NameAttribute}; 202 | 203 | mod capabilities; 204 | pub use self::capabilities::{Capabilities, Capability}; 205 | 206 | /// re-exported from imap_proto; 207 | pub use imap_proto::StatusAttribute; 208 | 209 | mod id_generator; 210 | pub(crate) use self::id_generator::IdGenerator; 211 | 212 | mod response_data; 213 | pub(crate) use self::response_data::ResponseData; 214 | 215 | mod request; 216 | pub(crate) use self::request::Request; 217 | 218 | mod quota; 219 | pub use self::quota::*; 220 | 221 | /// Responses that the server sends that are not related to the current command. 222 | /// 223 | /// [RFC 3501](https://tools.ietf.org/html/rfc3501#section-7) states that clients need to be able 224 | /// to accept any response at any time. These are the ones we've encountered in the wild. 225 | /// 226 | /// Note that `Recent`, `Exists` and `Expunge` responses refer to the currently `SELECT`ed folder, 227 | /// so the user must take care when interpreting these. 228 | #[derive(Debug, PartialEq, Eq)] 229 | pub enum UnsolicitedResponse { 230 | /// An unsolicited [`STATUS response`](https://tools.ietf.org/html/rfc3501#section-7.2.4). 231 | Status { 232 | /// The mailbox that this status response is for. 233 | mailbox: String, 234 | /// The attributes of this mailbox. 235 | attributes: Vec, 236 | }, 237 | 238 | /// An unsolicited [`RECENT` response](https://tools.ietf.org/html/rfc3501#section-7.3.2) 239 | /// indicating the number of messages with the `\Recent` flag set. This response occurs if the 240 | /// size of the mailbox changes (e.g., new messages arrive). 241 | /// 242 | /// > Note: It is not guaranteed that the message sequence 243 | /// > numbers of recent messages will be a contiguous range of 244 | /// > the highest n messages in the mailbox (where n is the 245 | /// > value reported by the `RECENT` response). Examples of 246 | /// > situations in which this is not the case are: multiple 247 | /// > clients having the same mailbox open (the first session 248 | /// > to be notified will see it as recent, others will 249 | /// > probably see it as non-recent), and when the mailbox is 250 | /// > re-ordered by a non-IMAP agent. 251 | /// > 252 | /// > The only reliable way to identify recent messages is to 253 | /// > look at message flags to see which have the `\Recent` flag 254 | /// > set, or to do a `SEARCH RECENT`. 255 | Recent(u32), 256 | 257 | /// An unsolicited [`EXISTS` response](https://tools.ietf.org/html/rfc3501#section-7.3.1) that 258 | /// reports the number of messages in the mailbox. This response occurs if the size of the 259 | /// mailbox changes (e.g., new messages arrive). 260 | Exists(u32), 261 | 262 | /// An unsolicited [`EXPUNGE` response](https://tools.ietf.org/html/rfc3501#section-7.4.1) that 263 | /// reports that the specified message sequence number has been permanently removed from the 264 | /// mailbox. The message sequence number for each successive message in the mailbox is 265 | /// immediately decremented by 1, and this decrement is reflected in message sequence numbers 266 | /// in subsequent responses (including other untagged `EXPUNGE` responses). 267 | /// 268 | /// The EXPUNGE response also decrements the number of messages in the mailbox; it is not 269 | /// necessary to send an `EXISTS` response with the new value. 270 | /// 271 | /// As a result of the immediate decrement rule, message sequence numbers that appear in a set 272 | /// of successive `EXPUNGE` responses depend upon whether the messages are removed starting 273 | /// from lower numbers to higher numbers, or from higher numbers to lower numbers. For 274 | /// example, if the last 5 messages in a 9-message mailbox are expunged, a "lower to higher" 275 | /// server will send five untagged `EXPUNGE` responses for message sequence number 5, whereas a 276 | /// "higher to lower server" will send successive untagged `EXPUNGE` responses for message 277 | /// sequence numbers 9, 8, 7, 6, and 5. 278 | // TODO: the spec doesn't seem to say anything about when these may be received as unsolicited? 279 | Expunge(u32), 280 | /// Any other kind of unsolicted response. 281 | Other(ResponseData), 282 | } 283 | -------------------------------------------------------------------------------- /src/types/name.rs: -------------------------------------------------------------------------------- 1 | pub use imap_proto::types::NameAttribute; 2 | use imap_proto::{MailboxDatum, Response}; 3 | use self_cell::self_cell; 4 | 5 | use crate::types::ResponseData; 6 | 7 | self_cell!( 8 | /// A name that matches a `LIST` or `LSUB` command. 9 | pub struct Name { 10 | owner: Box, 11 | 12 | #[covariant] 13 | dependent: InnerName, 14 | } 15 | 16 | impl { Debug } 17 | ); 18 | 19 | #[derive(PartialEq, Eq, Debug)] 20 | pub struct InnerName<'a> { 21 | attributes: Vec>, 22 | delimiter: Option<&'a str>, 23 | name: &'a str, 24 | } 25 | 26 | impl Name { 27 | pub(crate) fn from_mailbox_data(resp: ResponseData) -> Self { 28 | Name::new(Box::new(resp), |response| match response.parsed() { 29 | Response::MailboxData(MailboxDatum::List { 30 | name_attributes, 31 | delimiter, 32 | name, 33 | }) => InnerName { 34 | attributes: name_attributes.to_owned(), 35 | delimiter: delimiter.as_deref(), 36 | name, 37 | }, 38 | _ => panic!("cannot construct from non mailbox data"), 39 | }) 40 | } 41 | 42 | /// Attributes of this name. 43 | pub fn attributes(&self) -> &[NameAttribute<'_>] { 44 | &self.borrow_dependent().attributes[..] 45 | } 46 | 47 | /// The hierarchy delimiter is a character used to delimit levels of hierarchy in a mailbox 48 | /// name. A client can use it to create child mailboxes, and to search higher or lower levels 49 | /// of naming hierarchy. All children of a top-level hierarchy node use the same 50 | /// separator character. `None` means that no hierarchy exists; the name is a "flat" name. 51 | pub fn delimiter(&self) -> Option<&str> { 52 | self.borrow_dependent().delimiter 53 | } 54 | 55 | /// The name represents an unambiguous left-to-right hierarchy, and are valid for use as a 56 | /// reference in `LIST` and `LSUB` commands. Unless [`NameAttribute::NoSelect`] is indicated, 57 | /// the name is also valid as an argument for commands, such as `SELECT`, that accept mailbox 58 | /// names. 59 | pub fn name(&self) -> &str { 60 | self.borrow_dependent().name 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/types/quota.rs: -------------------------------------------------------------------------------- 1 | use imap_proto::types::Quota as QuotaRef; 2 | use imap_proto::types::QuotaResource as QuotaResourceRef; 3 | use imap_proto::types::QuotaResourceName as QuotaResourceNameRef; 4 | use imap_proto::types::QuotaRoot as QuotaRootRef; 5 | 6 | /// 7 | #[derive(Debug, Eq, PartialEq, Hash, Clone)] 8 | pub enum QuotaResourceName { 9 | /// Sum of messages' RFC822.SIZE, in units of 1024 octets 10 | Storage, 11 | /// Number of messages 12 | Message, 13 | /// A different/custom resource 14 | Atom(String), 15 | } 16 | 17 | impl From> for QuotaResourceName { 18 | fn from(name: QuotaResourceNameRef<'_>) -> Self { 19 | match name { 20 | QuotaResourceNameRef::Message => QuotaResourceName::Message, 21 | QuotaResourceNameRef::Storage => QuotaResourceName::Storage, 22 | QuotaResourceNameRef::Atom(v) => QuotaResourceName::Atom(v.to_string()), 23 | } 24 | } 25 | } 26 | 27 | /// 5.1. QUOTA Response () 28 | #[derive(Debug, Eq, PartialEq, Hash, Clone)] 29 | pub struct QuotaResource { 30 | /// name of the resource 31 | pub name: QuotaResourceName, 32 | /// current usage of the resource 33 | pub usage: u64, 34 | /// resource limit 35 | pub limit: u64, 36 | } 37 | 38 | impl From> for QuotaResource { 39 | fn from(resource: QuotaResourceRef<'_>) -> Self { 40 | Self { 41 | name: resource.name.into(), 42 | usage: resource.usage, 43 | limit: resource.limit, 44 | } 45 | } 46 | } 47 | 48 | impl QuotaResource { 49 | /// Returns the usage percentage of a QuotaResource. 50 | pub fn get_usage_percentage(&self) -> u64 { 51 | self.usage 52 | .saturating_mul(100) 53 | .checked_div(self.limit) 54 | // Assume that if `limit` is 0, this means that storage is unlimited: 55 | .unwrap_or(0) 56 | } 57 | } 58 | 59 | /// 5.1. QUOTA Response () 60 | #[derive(Debug, Eq, PartialEq, Hash, Clone)] 61 | pub struct Quota { 62 | /// quota root name 63 | pub root_name: String, 64 | /// quota resources for this quota 65 | pub resources: Vec, 66 | } 67 | 68 | impl From> for Quota { 69 | fn from(quota: QuotaRef<'_>) -> Self { 70 | Self { 71 | root_name: quota.root_name.to_string(), 72 | resources: quota.resources.iter().map(|r| r.clone().into()).collect(), 73 | } 74 | } 75 | } 76 | 77 | /// 5.2. QUOTAROOT Response () 78 | #[derive(Debug, Eq, PartialEq, Hash, Clone)] 79 | pub struct QuotaRoot { 80 | /// mailbox name 81 | pub mailbox_name: String, 82 | /// zero or more quota root names 83 | pub quota_root_names: Vec, 84 | } 85 | 86 | impl From> for QuotaRoot { 87 | fn from(root: QuotaRootRef<'_>) -> Self { 88 | Self { 89 | mailbox_name: root.mailbox_name.to_string(), 90 | quota_root_names: root 91 | .quota_root_names 92 | .iter() 93 | .map(|n| n.to_string()) 94 | .collect(), 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/types/request.rs: -------------------------------------------------------------------------------- 1 | use imap_proto::RequestId; 2 | 3 | #[derive(Debug, Eq, PartialEq)] 4 | pub struct Request(pub Option, pub Vec); 5 | -------------------------------------------------------------------------------- /src/types/response_data.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use bytes::BytesMut; 4 | use imap_proto::{RequestId, Response}; 5 | use self_cell::self_cell; 6 | 7 | self_cell!( 8 | pub struct ResponseData { 9 | owner: BytesMut, 10 | 11 | #[covariant] 12 | dependent: Response, 13 | } 14 | ); 15 | 16 | impl std::cmp::PartialEq for ResponseData { 17 | fn eq(&self, other: &Self) -> bool { 18 | self.parsed() == other.parsed() 19 | } 20 | } 21 | 22 | impl std::cmp::Eq for ResponseData {} 23 | 24 | impl fmt::Debug for ResponseData { 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | f.debug_struct("ResponseData") 27 | .field("raw", &self.borrow_owner().len()) 28 | .field("response", self.borrow_dependent()) 29 | .finish() 30 | } 31 | } 32 | 33 | impl ResponseData { 34 | pub fn request_id(&self) -> Option<&RequestId> { 35 | match self.borrow_dependent() { 36 | Response::Done { ref tag, .. } => Some(tag), 37 | _ => None, 38 | } 39 | } 40 | 41 | pub fn parsed(&self) -> &Response<'_> { 42 | self.borrow_dependent() 43 | } 44 | } 45 | --------------------------------------------------------------------------------